Merge branch 'master' into mining-api-docs

This commit is contained in:
wiz 2022-05-19 20:45:20 +09:00 committed by GitHub
commit 6562a35f14
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
81 changed files with 1868 additions and 1847 deletions

View file

@ -21,7 +21,7 @@ jobs:
- name: Setup node
uses: actions/setup-node@v2
with:
node-version: 16.10.0
node-version: 16.15.0
cache: 'npm'
cache-dependency-path: frontend/package-lock.json
- name: ${{ matrix.browser }} browser tests (Mempool)

2
.nvmrc
View file

@ -1 +1 @@
v16.10.0
v16.15.0

View file

@ -2,7 +2,7 @@ import { IEsploraApi } from './esplora-api.interface';
export interface AbstractBitcoinApi {
$getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]>;
$getRawTransaction(txId: string, skipConversion?: boolean, addPrevout?: boolean, blockHash?: string): Promise<IEsploraApi.Transaction>;
$getRawTransaction(txId: string, skipConversion?: boolean, addPrevout?: boolean): Promise<IEsploraApi.Transaction>;
$getBlockHeightTip(): Promise<number>;
$getTxIdsForBlock(hash: string): Promise<string[]>;
$getBlockHash(height: number): Promise<string>;

View file

@ -14,14 +14,31 @@ class BitcoinApi implements AbstractBitcoinApi {
this.bitcoindClient = bitcoinClient;
}
$getRawTransaction(txId: string, skipConversion = false, addPrevout = false, blockHash?: string): Promise<IEsploraApi.Transaction> {
static convertBlock(block: IBitcoinApi.Block): IEsploraApi.Block {
return {
id: block.hash,
height: block.height,
version: block.version,
timestamp: block.time,
bits: parseInt(block.bits, 16),
nonce: block.nonce,
difficulty: block.difficulty,
merkle_root: block.merkleroot,
tx_count: block.nTx,
size: block.size,
weight: block.weight,
previousblockhash: block.previousblockhash,
};
}
$getRawTransaction(txId: string, skipConversion = false, addPrevout = false): Promise<IEsploraApi.Transaction> {
// If the transaction is in the mempool we already converted and fetched the fee. Only prevouts are missing
const txInMempool = mempool.getMempool()[txId];
if (txInMempool && addPrevout) {
return this.$addPrevouts(txInMempool);
}
return this.bitcoindClient.getRawTransaction(txId, true, blockHash)
return this.bitcoindClient.getRawTransaction(txId, true)
.then((transaction: IBitcoinApi.Transaction) => {
if (skipConversion) {
transaction.vout.forEach((vout) => {
@ -174,35 +191,18 @@ class BitcoinApi implements AbstractBitcoinApi {
};
}
if (transaction.confirmations) {
esploraTransaction = await this.$calculateFeeFromInputs(esploraTransaction, addPrevout);
} else {
esploraTransaction = await this.$appendMempoolFeeData(esploraTransaction);
if (addPrevout) {
esploraTransaction = await this.$calculateFeeFromInputs(esploraTransaction, addPrevout);
if (addPrevout) {
if (transaction.confirmations) {
esploraTransaction = await this.$calculateFeeFromInputs(esploraTransaction);
} else {
esploraTransaction = await this.$appendMempoolFeeData(esploraTransaction);
esploraTransaction = await this.$calculateFeeFromInputs(esploraTransaction);
}
}
return esploraTransaction;
}
static convertBlock(block: IBitcoinApi.Block): IEsploraApi.Block {
return {
id: block.hash,
height: block.height,
version: block.version,
timestamp: block.time,
bits: parseInt(block.bits, 16),
nonce: block.nonce,
difficulty: block.difficulty,
merkle_root: block.merkleroot,
tx_count: block.nTx,
size: block.size,
weight: block.weight,
previousblockhash: block.previousblockhash,
};
}
private translateScriptPubKeyType(outputType: string): string {
const map = {
'pubkey': 'p2pk',
@ -245,7 +245,7 @@ class BitcoinApi implements AbstractBitcoinApi {
if (vin.prevout) {
continue;
}
const innerTx = await this.$getRawTransaction(vin.txid, false);
const innerTx = await this.$getRawTransaction(vin.txid, false, false);
vin.prevout = innerTx.vout[vin.vout];
this.addInnerScriptsToVin(vin);
}
@ -271,18 +271,16 @@ class BitcoinApi implements AbstractBitcoinApi {
return this.bitcoindClient.getRawMemPool(true);
}
private async $calculateFeeFromInputs(transaction: IEsploraApi.Transaction, addPrevout: boolean): Promise<IEsploraApi.Transaction> {
private async $calculateFeeFromInputs(transaction: IEsploraApi.Transaction): Promise<IEsploraApi.Transaction> {
if (transaction.vin[0].is_coinbase) {
transaction.fee = 0;
return transaction;
}
let totalIn = 0;
for (const vin of transaction.vin) {
const innerTx = await this.$getRawTransaction(vin.txid, !addPrevout);
if (addPrevout) {
vin.prevout = innerTx.vout[vin.vout];
this.addInnerScriptsToVin(vin);
}
const innerTx = await this.$getRawTransaction(vin.txid, false, false);
vin.prevout = innerTx.vout[vin.vout];
this.addInnerScriptsToVin(vin);
totalIn += innerTx.vout[vin.vout].value;
}
const totalOut = transaction.vout.reduce((p, output) => p + output.value, 0);

View file

@ -134,7 +134,7 @@ class Blocks {
blockExtended.extras.avgFeeRate = stats.avgfeerate;
}
if (Common.indexingEnabled()) {
if (['mainnet', 'testnet', 'signet', 'regtest'].includes(config.MEMPOOL.NETWORK)) {
let pool: PoolTag;
if (blockExtended.extras?.coinbaseTx !== undefined) {
pool = await this.$findBlockMiner(blockExtended.extras?.coinbaseTx);
@ -143,7 +143,8 @@ class Blocks {
}
if (!pool) { // We should never have this situation in practise
logger.warn(`Cannot assign pool to block ${blockExtended.height} and 'unknown' pool does not exist. Check your "pools" table entries`);
logger.warn(`Cannot assign pool to block ${blockExtended.height} and 'unknown' pool does not exist. ` +
`Check your "pools" table entries`);
return blockExtended;
}
@ -389,6 +390,42 @@ class Blocks {
return prepareBlock(blockExtended);
}
/**
* Index a block by hash if it's missing from the database. Returns the block after indexing
*/
public async $getBlock(hash: string): Promise<BlockExtended | IEsploraApi.Block> {
// Check the memory cache
const blockByHash = this.getBlocks().find((b) => b.id === hash);
if (blockByHash) {
return blockByHash;
}
// Block has already been indexed
if (Common.indexingEnabled()) {
const dbBlock = await blocksRepository.$getBlockByHash(hash);
if (dbBlock != null) {
return prepareBlock(dbBlock);
}
}
const block = await bitcoinApi.$getBlock(hash);
// Not Bitcoin network, return the block as it
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false) {
return block;
}
// Bitcoin network, add our custom data on top
const transactions = await this.$getTransactionsExtended(hash, block.height, true);
const blockExtended = await this.$getBlockExtended(block, transactions);
if (Common.indexingEnabled()) {
delete(blockExtended['coinbaseTx']);
await blocksRepository.$saveBlockInDatabase(blockExtended);
}
return blockExtended;
}
public async $getBlocksExtras(fromHeight?: number, limit: number = 15): Promise<BlockExtended[]> {
// Note - This API is breaking if indexing is not available. For now it is okay because we only
// use it for the mining pages, and mining pages should not be available if indexing is turned off.

View file

@ -169,12 +169,12 @@ export class Common {
default: return null;
}
}
static indexingEnabled(): boolean {
return (
['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) &&
['mainnet', 'testnet', 'signet', 'regtest'].includes(config.MEMPOOL.NETWORK) &&
config.DATABASE.ENABLED === true &&
config.MEMPOOL.INDEXING_BLOCKS_AMOUNT != 0
config.MEMPOOL.INDEXING_BLOCKS_AMOUNT !== 0
);
}
}

View file

@ -4,7 +4,7 @@ import logger from '../logger';
import { Common } from './common';
class DatabaseMigration {
private static currentVersion = 17;
private static currentVersion = 18;
private queryTimeout = 120000;
private statisticsAddedIndexed = false;
@ -180,6 +180,10 @@ class DatabaseMigration {
if (databaseSchemaVersion < 17 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `pools` ADD `slug` CHAR(50) NULL');
}
if (databaseSchemaVersion < 18 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `blocks` ADD INDEX `hash` (`hash`);');
}
} catch (e) {
throw e;
}

View file

@ -32,7 +32,7 @@ class DifficultyAdjustmentApi {
}
}
let timeAvgMins = blocksInEpoch ? diff / blocksInEpoch / 60 : 10;
let timeAvgMins = blocksInEpoch && blocksInEpoch > 146 ? diff / blocksInEpoch / 60 : 10;
// Testnet difficulty is set to 1 after 20 minutes of no blocks,
// therefore the time between blocks will always be below 20 minutes (1200s).

View file

@ -300,24 +300,12 @@ class Server {
if (Common.indexingEnabled()) {
this.app
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools/24h', routes.$getPools.bind(routes, '24h'))
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools/3d', routes.$getPools.bind(routes, '3d'))
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools/1w', routes.$getPools.bind(routes, '1w'))
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools/1m', routes.$getPools.bind(routes, '1m'))
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools/3m', routes.$getPools.bind(routes, '3m'))
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools/6m', routes.$getPools.bind(routes, '6m'))
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools/1y', routes.$getPools.bind(routes, '1y'))
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools/2y', routes.$getPools.bind(routes, '2y'))
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools/3y', routes.$getPools.bind(routes, '3y'))
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools/all', routes.$getPools.bind(routes, 'all'))
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pools/:interval', routes.$getPools)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:slug/hashrate', routes.$getPoolHistoricalHashrate)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:slug/blocks', routes.$getPoolBlocks)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:slug/blocks/:height', routes.$getPoolBlocks)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:slug', routes.$getPool)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:slug/:interval', routes.$getPool)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/hashrate/pools', routes.$getPoolsHistoricalHashrate)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/hashrate/pools/:interval', routes.$getPoolsHistoricalHashrate)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/hashrate', routes.$getHistoricalHashrate)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/hashrate/:interval', routes.$getHistoricalHashrate)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/reward-stats/:blockCount', routes.$getRewardStats)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/fees/:interval', routes.$getHistoricalBlockFees)
@ -350,7 +338,8 @@ class Server {
this.app
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks-extras', routes.getBlocksExtras)
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks-extras/:height', routes.getBlocksExtras);
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks-extras/:height', routes.getBlocksExtras)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash', routes.getBlock);
if (config.MEMPOOL.BACKEND !== 'esplora') {
this.app
@ -362,7 +351,6 @@ class Server {
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/hex', routes.getRawTransaction)
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/status', routes.getTransactionStatus)
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/outspends', routes.getTransactionOutspends)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash', routes.getBlock)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/header', routes.getBlockHeader)
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks', routes.getBlocks)
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/:height', routes.getBlocks)

View file

@ -287,6 +287,7 @@ class BlocksRepository {
return null;
}
rows[0].fee_span = JSON.parse(rows[0].fee_span);
return rows[0];
} catch (e) {
logger.err(`Cannot get indexed block ${height}. Reason: ` + (e instanceof Error ? e.message : e));
@ -294,6 +295,34 @@ class BlocksRepository {
}
}
/**
* Get one block by hash
*/
public async $getBlockByHash(hash: string): Promise<object | null> {
try {
const query = `
SELECT *, UNIX_TIMESTAMP(blocks.blockTimestamp) as blockTimestamp, hash as id,
pools.id as pool_id, pools.name as pool_name, pools.link as pool_link, pools.slug as pool_slug,
pools.addresses as pool_addresses, pools.regexes as pool_regexes,
previous_block_hash as previousblockhash
FROM blocks
JOIN pools ON blocks.pool_id = pools.id
WHERE hash = '${hash}';
`;
const [rows]: any[] = await DB.query(query);
if (rows.length <= 0) {
return null;
}
rows[0].fee_span = JSON.parse(rows[0].fee_span);
return rows[0];
} catch (e) {
logger.err(`Cannot get indexed block ${hash}. Reason: ` + (e instanceof Error ? e.message : e));
throw e;
}
}
/**
* Return blocks difficulty
*/
@ -397,18 +426,28 @@ class BlocksRepository {
const [blocks]: any[] = await DB.query(`SELECT height, hash, previous_block_hash,
UNIX_TIMESTAMP(blockTimestamp) as timestamp FROM blocks ORDER BY height`);
let currentHeight = 1;
while (currentHeight < blocks.length) {
if (blocks[currentHeight].previous_block_hash !== blocks[currentHeight - 1].hash) {
logger.warn(`Chain divergence detected at block ${blocks[currentHeight - 1].height}, re-indexing newer blocks and hashrates`);
await this.$deleteBlocksFrom(blocks[currentHeight - 1].height);
await HashratesRepository.$deleteHashratesFromTimestamp(blocks[currentHeight - 1].timestamp - 604800);
let partialMsg = false;
let idx = 1;
while (idx < blocks.length) {
if (blocks[idx].height - 1 !== blocks[idx - 1].height) {
if (partialMsg === false) {
logger.info('Some blocks are not indexed, skipping missing blocks during chain validation');
partialMsg = true;
}
++idx;
continue;
}
if (blocks[idx].previous_block_hash !== blocks[idx - 1].hash) {
logger.warn(`Chain divergence detected at block ${blocks[idx - 1].height}, re-indexing newer blocks and hashrates`);
await this.$deleteBlocksFrom(blocks[idx - 1].height);
await HashratesRepository.$deleteHashratesFromTimestamp(blocks[idx - 1].timestamp - 604800);
return false;
}
++currentHeight;
++idx;
}
logger.info(`${currentHeight} blocks hash validated in ${new Date().getTime() - start} ms`);
logger.info(`${idx} blocks hash validated in ${new Date().getTime() - start} ms`);
return true;
} catch (e) {
logger.err('Cannot validate chain of block hash. Reason: ' + (e instanceof Error ? e.message : e));
@ -457,7 +496,7 @@ class BlocksRepository {
/**
* Get the historical averaged block rewards
*/
public async $getHistoricalBlockRewards(div: number, interval: string | null): Promise<any> {
public async $getHistoricalBlockRewards(div: number, interval: string | null): Promise<any> {
try {
let query = `SELECT
CAST(AVG(height) as INT) as avg_height,

View file

@ -572,9 +572,9 @@ class Routes {
}
}
public async $getPools(interval: string, req: Request, res: Response) {
public async $getPools(req: Request, res: Response) {
try {
const stats = await miningStats.$getPoolsStats(interval);
const stats = await miningStats.$getPoolsStats(req.params.interval);
const blockCount = await BlocksRepository.$blockCount(null, null);
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
@ -588,7 +588,7 @@ class Routes {
public async $getPoolsHistoricalHashrate(req: Request, res: Response) {
try {
const hashrates = await HashratesRepository.$getPoolsWeeklyHashrate(req.params.interval ?? null);
const hashrates = await HashratesRepository.$getPoolsWeeklyHashrate(req.params.interval);
const blockCount = await BlocksRepository.$blockCount(null, null);
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
@ -620,8 +620,8 @@ class Routes {
public async $getHistoricalHashrate(req: Request, res: Response) {
try {
const hashrates = await HashratesRepository.$getNetworkDailyHashrate(req.params.interval ?? null);
const difficulty = await BlocksRepository.$getBlocksDifficulty(req.params.interval ?? null);
const hashrates = await HashratesRepository.$getNetworkDailyHashrate(req.params.interval);
const difficulty = await BlocksRepository.$getBlocksDifficulty(req.params.interval);
const blockCount = await BlocksRepository.$blockCount(null, null);
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
@ -640,7 +640,7 @@ class Routes {
public async $getHistoricalBlockFees(req: Request, res: Response) {
try {
const blockFees = await mining.$getHistoricalBlockFees(req.params.interval ?? null);
const blockFees = await mining.$getHistoricalBlockFees(req.params.interval);
const blockCount = await BlocksRepository.$blockCount(null, null);
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
@ -654,7 +654,7 @@ class Routes {
public async $getHistoricalBlockRewards(req: Request, res: Response) {
try {
const blockRewards = await mining.$getHistoricalBlockRewards(req.params.interval ?? null);
const blockRewards = await mining.$getHistoricalBlockRewards(req.params.interval);
const blockCount = await BlocksRepository.$blockCount(null, null);
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
@ -668,7 +668,7 @@ class Routes {
public async $getHistoricalBlockFeeRates(req: Request, res: Response) {
try {
const blockFeeRates = await mining.$getHistoricalBlockFeeRates(req.params.interval ?? null);
const blockFeeRates = await mining.$getHistoricalBlockFeeRates(req.params.interval);
const oldestIndexedBlockTimestamp = await BlocksRepository.$oldestBlockTimestamp();
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
@ -684,8 +684,8 @@ class Routes {
public async $getHistoricalBlockSizeAndWeight(req: Request, res: Response) {
try {
const blockSizes = await mining.$getHistoricalBlockSizes(req.params.interval ?? null);
const blockWeights = await mining.$getHistoricalBlockWeights(req.params.interval ?? null);
const blockSizes = await mining.$getHistoricalBlockSizes(req.params.interval);
const blockWeights = await mining.$getHistoricalBlockWeights(req.params.interval);
const blockCount = await BlocksRepository.$blockCount(null, null);
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
@ -702,8 +702,9 @@ class Routes {
public async getBlock(req: Request, res: Response) {
try {
const result = await bitcoinApi.$getBlock(req.params.hash);
res.json(result);
const block = await blocks.$getBlock(req.params.hash);
res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString());
res.json(block);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
@ -727,7 +728,7 @@ class Routes {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
public async getBlocks(req: Request, res: Response) {
try {
loadingIndicators.setProgress('blocks', 0);

View file

@ -1,8 +1,10 @@
const https = require('https');
import axios from 'axios';
import poolsParser from '../api/pools-parser';
import config from '../config';
import DB from '../database';
import logger from '../logger';
import { SocksProxyAgent } from 'socks-proxy-agent';
import * as https from 'https';
/**
* Maintain the most recent version of pools.json
@ -28,6 +30,13 @@ class PoolsUpdater {
this.lastRun = now;
logger.info('Updating latest mining pools from Github');
if (config.SOCKS5PROXY.ENABLED) {
logger.info('List of public pools will be queried over the Tor network');
} else {
logger.info('List of public pools will be queried over clearnet');
}
try {
const dbSha = await this.getShaFromDb();
const githubSha = await this.fetchPoolsSha(); // Fetch pools.json sha from github
@ -41,7 +50,10 @@ class PoolsUpdater {
}
logger.warn('Pools.json is outdated, fetch latest from github');
const poolsJson = await this.fetchPools();
const poolsJson = await this.query('https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json');
if (poolsJson === undefined) {
return;
}
await poolsParser.migratePoolsJson(poolsJson);
await this.updateDBSha(githubSha);
logger.notice('PoolsUpdater completed');
@ -52,14 +64,6 @@ class PoolsUpdater {
}
}
/**
* Fetch pools.json from github repo
*/
private async fetchPools(): Promise<object> {
const response = await this.query('/repos/mempool/mining-pools/contents/pools.json');
return JSON.parse(Buffer.from(response['content'], 'base64').toString('utf8'));
}
/**
* Fetch our latest pools.json sha from the db
*/
@ -90,11 +94,13 @@ class PoolsUpdater {
* Fetch our latest pools.json sha from github
*/
private async fetchPoolsSha(): Promise<string | undefined> {
const response = await this.query('/repos/mempool/mining-pools/git/trees/master');
const response = await this.query('https://api.github.com/repos/mempool/mining-pools/git/trees/master');
for (const file of response['tree']) {
if (file['path'] === 'pools.json') {
return file['sha'];
if (response !== undefined) {
for (const file of response['tree']) {
if (file['path'] === 'pools.json') {
return file['sha'];
}
}
}
@ -105,35 +111,45 @@ class PoolsUpdater {
/**
* Http request wrapper
*/
private query(path): Promise<string> {
return new Promise((resolve, reject) => {
const options = {
host: 'api.github.com',
path: path,
method: 'GET',
headers: { 'user-agent': 'node.js' }
private async query(path): Promise<object | undefined> {
type axiosOptions = {
httpsAgent?: https.Agent;
}
const setDelay = (secs: number = 1): Promise<void> => new Promise(resolve => setTimeout(() => resolve(), secs * 1000));
const axiosOptions: axiosOptions = {};
let retry = 0;
if (config.SOCKS5PROXY.ENABLED) {
const socksOptions: any = {
agentOptions: {
keepAlive: true,
},
hostname: config.SOCKS5PROXY.HOST,
port: config.SOCKS5PROXY.PORT
};
logger.debug('Querying: api.github.com' + path);
if (config.SOCKS5PROXY.USERNAME && config.SOCKS5PROXY.PASSWORD) {
socksOptions.username = config.SOCKS5PROXY.USERNAME;
socksOptions.password = config.SOCKS5PROXY.PASSWORD;
}
const request = https.get(options, (response) => {
const chunks_of_data: any[] = [];
response.on('data', (fragments) => {
chunks_of_data.push(fragments);
});
response.on('end', () => {
resolve(JSON.parse(Buffer.concat(chunks_of_data).toString()));
});
response.on('error', (error) => {
reject(error);
});
});
axiosOptions.httpsAgent = new SocksProxyAgent(socksOptions);
}
request.on('error', (error) => {
logger.err('Github API query failed. Reason: ' + error);
reject(error);
});
});
while(retry < 5) {
try {
const data = await axios.get(path, axiosOptions);
if (data.statusText !== 'OK' || !data.data) {
throw new Error(`Could not fetch data from Github, Error: ${data.status}`);
}
return data.data;
} catch (e) {
logger.err('Could not connect to Github. Reason: ' + (e instanceof Error ? e.message : e));
retry++;
}
await setDelay();
}
return undefined;
}
}

View file

@ -1,4 +1,4 @@
import { BlockExtended } from "../mempool.interfaces";
import { BlockExtended } from '../mempool.interfaces';
export function prepareBlock(block: any): BlockExtended {
return <BlockExtended>{
@ -17,9 +17,11 @@ export function prepareBlock(block: any): BlockExtended {
extras: {
coinbaseRaw: block.coinbase_raw ?? block.extras.coinbaseRaw,
medianFee: block.medianFee ?? block.median_fee ?? block.extras?.medianFee,
feeRange: block.feeRange ?? block.fee_range ?? block?.extras?.feeSpan,
feeRange: block.feeRange ?? block.fee_span,
reward: block.reward ?? block?.extras?.reward,
totalFees: block.totalFees ?? block?.fees ?? block?.extras.totalFees,
totalFees: block.totalFees ?? block?.fees ?? block?.extras?.totalFees,
avgFee: block?.extras?.avgFee ?? block.avg_fee,
avgFeeRate: block?.avgFeeRate ?? block.avg_fee_rate,
pool: block?.extras?.pool ?? (block?.pool_id ? {
id: block.pool_id,
name: block.pool_name,

View file

@ -0,0 +1,3 @@
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of May 15, 2022.
Signed: ayanamidev

View file

@ -1,4 +1,4 @@
FROM node:16.10.0-buster-slim AS builder
FROM node:16.15.0-buster-slim AS builder
ARG commitHash
ENV DOCKER_COMMIT_HASH=${commitHash}
@ -11,7 +11,7 @@ RUN apt-get install -y build-essential python3 pkg-config
RUN npm install
RUN npm run build
FROM node:16.10.0-buster-slim
FROM node:16.15.0-buster-slim
WORKDIR /backend

View file

@ -1,4 +1,4 @@
FROM node:16.10.0-buster-slim AS builder
FROM node:16.15.0-buster-slim AS builder
ARG commitHash
ENV DOCKER_COMMIT_HASH=${commitHash}

View file

@ -70,7 +70,7 @@
},
"optionalDependencies": {
"@cypress/schematic": "^1.3.0",
"cypress": "^9.5.2",
"cypress": "^9.6.1",
"cypress-fail-on-console-error": "^2.1.3",
"cypress-wait-until": "^1.7.1",
"mock-socket": "^9.0.3",
@ -6996,9 +6996,9 @@
"devOptional": true
},
"node_modules/cypress": {
"version": "9.5.2",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-9.5.2.tgz",
"integrity": "sha512-gYiQYvJozMzDOriUV1rCt6CeRM/pRK4nhwGJj3nJQyX2BoUdTCVwp30xDMKc771HiNVhBtgj5o5/iBdVDVXQUg==",
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-9.6.1.tgz",
"integrity": "sha512-ECzmV7pJSkk+NuAhEw6C3D+RIRATkSb2VAHXDY6qGZbca/F9mv5pPsj2LO6Ty6oIFVBTrwCyL9agl28MtJMe2g==",
"hasInstallScript": true,
"optional": true,
"dependencies": {
@ -7034,7 +7034,7 @@
"listr2": "^3.8.3",
"lodash": "^4.17.21",
"log-symbols": "^4.0.0",
"minimist": "^1.2.5",
"minimist": "^1.2.6",
"ospath": "^1.2.2",
"pretty-bytes": "^5.6.0",
"proxy-from-env": "1.0.0",
@ -22641,9 +22641,9 @@
"devOptional": true
},
"cypress": {
"version": "9.5.2",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-9.5.2.tgz",
"integrity": "sha512-gYiQYvJozMzDOriUV1rCt6CeRM/pRK4nhwGJj3nJQyX2BoUdTCVwp30xDMKc771HiNVhBtgj5o5/iBdVDVXQUg==",
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-9.6.1.tgz",
"integrity": "sha512-ECzmV7pJSkk+NuAhEw6C3D+RIRATkSb2VAHXDY6qGZbca/F9mv5pPsj2LO6Ty6oIFVBTrwCyL9agl28MtJMe2g==",
"optional": true,
"requires": {
"@cypress/request": "^2.88.10",
@ -22678,7 +22678,7 @@
"listr2": "^3.8.3",
"lodash": "^4.17.21",
"log-symbols": "^4.0.0",
"minimist": "^1.2.5",
"minimist": "^1.2.6",
"ospath": "^1.2.2",
"pretty-bytes": "^5.6.0",
"proxy-from-env": "1.0.0",

View file

@ -122,7 +122,7 @@
},
"optionalDependencies": {
"@cypress/schematic": "^1.3.0",
"cypress": "^9.5.2",
"cypress": "^9.6.1",
"cypress-fail-on-console-error": "^2.1.3",
"cypress-wait-until": "^1.7.1",
"mock-socket": "^9.0.3",

View file

@ -40,7 +40,6 @@ import { AssetComponent } from './components/asset/asset.component';
import { AssetsComponent } from './components/assets/assets.component';
import { AssetsNavComponent } from './components/assets/assets-nav/assets-nav.component';
import { StatusViewComponent } from './components/status-view/status-view.component';
import { MinerComponent } from './components/miner/miner.component';
import { SharedModule } from './shared/shared.module';
import { NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstrap';
import { FeesBoxComponent } from './components/fees-box/fees-box.component';
@ -68,6 +67,7 @@ import { HashrateChartPoolsComponent } from './components/hashrates-chart-pools/
import { MiningStartComponent } from './components/mining-start/mining-start.component';
import { AmountShortenerPipe } from './shared/pipes/amount-shortener.pipe';
import { ShortenStringPipe } from './shared/pipes/shorten-string-pipe/shorten-string.pipe';
import { CapAddressPipe } from './shared/pipes/cap-address-pipe/cap-address-pipe';
import { GraphsComponent } from './components/graphs/graphs.component';
import { DifficultyAdjustmentsTable } from './components/difficulty-adjustments-table/difficulty-adjustments-table.components';
import { BlocksList } from './components/blocks-list/blocks-list.component';
@ -108,7 +108,6 @@ import { BlockSizesWeightsGraphComponent } from './components/block-sizes-weight
LbtcPegsGraphComponent,
AssetComponent,
AssetsComponent,
MinerComponent,
StatusViewComponent,
FeesBoxComponent,
DashboardComponent,
@ -163,6 +162,7 @@ import { BlockSizesWeightsGraphComponent } from './components/block-sizes-weight
StorageService,
LanguageService,
ShortenStringPipe,
CapAddressPipe,
{ provide: HTTP_INTERCEPTORS, useClass: HttpCacheInterceptor, multi: true }
],
bootstrap: [AppComponent]

View file

@ -20,7 +20,7 @@
<td><a [routerLink]="['/block/' | relativeUrl, block.hash]" title="{{ block.hash }}">{{ block.hash | shortenString : 13 }}</a> <app-clipboard class="d-none d-sm-inline-block" [text]="block.hash"></app-clipboard></td>
</tr>
<tr>
<td i18n="transaction.timestamp|Transaction Timestamp">Timestamp</td>
<td i18n="block.timestamp">Timestamp</td>
<td>
&lrm;{{ block.time | date:'yyyy-MM-dd HH:mm' }}
<div class="lg-inline">
@ -83,7 +83,7 @@
<td><span class="skeleton-loader"></span></td>
</tr>
<tr>
<td i18n="transaction.timestamp|Transaction Timestamp">Timestamp</td>
<td i18n="block.timestamp">Timestamp</td>
<td><span class="skeleton-loader"></span></td>
</tr>
</table>

View file

@ -89,7 +89,7 @@
</table>
</div>
<div class="text-center"><a href="" [routerLink]="['/markets' | relativeUrl]" i18n="dashboard.view-all">View all &raquo;</a></div>
<div class="text-center"><a href="" [routerLink]="['/markets' | relativeUrl]" i18n="dashboard.view-more">View more &raquo;</a></div>
</div>
</div>
</div>
@ -98,7 +98,7 @@
<div class="card-body">
<h5 class="card-title text-center" i18n="Latest Trades header">Latest Trades</h5>
<app-bisq-trades [trades$]="trades$" view="small"></app-bisq-trades>
<div class="text-center"><a href="" [routerLink]="['/markets' | relativeUrl]" i18n="dashboard.view-all">View all &raquo;</a></div>
<div class="text-center"><a href="" [routerLink]="['/markets' | relativeUrl]" i18n="dashboard.view-more">View more &raquo;</a></div>
</div>
</div>
</div>

View file

@ -31,7 +31,7 @@
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td i18n="transaction.timestamp|Transaction Timestamp">Timestamp</td>
<td i18n="block.timestamp">Timestamp</td>
<td>
&lrm;{{ bisqTx.time | date:'yyyy-MM-dd HH:mm' }}
<div class="lg-inline">

View file

@ -18,7 +18,7 @@
<th style="width: 20%;" i18n>TXID</th>
<th class="d-none d-md-block" style="width: 100%;" i18n>Type</th>
<th style="width: 20%;" i18n>Amount</th>
<th style="width: 20%;" i18n>Confirmed</th>
<th style="width: 20%;" i18n="transaction.confirmed|Transaction Confirmed state">Confirmed</th>
<th class="d-none d-md-block" i18n>Height</th>
</thead>
<tbody *ngIf="transactions.value; else loadingTmpl">

View file

@ -118,7 +118,7 @@ export class AddressLabelsComponent implements OnInit {
}
const m = parseInt(opM.match(/[0-9]+/)[0], 10);
this.label = `multisig ${m} of ${n}`;
this.label = $localize`:@@address-label.multisig:Multisig ${m}:multisigM: of ${n}:multisigN:`
}
handleVout() {

View file

@ -1,11 +1,13 @@
<div *ngIf="group$ | async as group; else loading">
<div *ngIf="group$ | async as group; else loading;">
<div class="main-title">
<h2>{{ group.group.name }}</h2>
<div class="sub-title" i18n>Group of {{ group.group.assets.length | number }} assets</div>
</div>
<ng-container *ngTemplateOutlet="title; context: group.group"></ng-container>
<ng-template #title>
<div class="main-title">
<h2>{{ group.name }}</h2>
<div class="sub-title" i18n>Group of {{ group.assets.length | number }} assets</div>
</div>
</ng-template>
<div class="clearfix"></div>

View file

@ -2,11 +2,10 @@
<div class="full-container">
<div class="card-header mb-0 mb-md-4">
<span i18n="mining.block-fee-rates">Block fee rates</span>
<span i18n="mining.block-fee-rates">Block Fee Rates</span>
<button class="btn" style="margin: 0 0 4px 0px" (click)="onSaveChart()">
<fa-icon [icon]="['fas', 'download']" [fixedWidth]="true"></fa-icon>
</button>
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(statsObservable$ | async) as stats">
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan">
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.availableTimespanDay >= 1">

View file

@ -62,7 +62,7 @@ export class BlockFeeRatesGraphComponent implements OnInit {
}
ngOnInit(): void {
this.seoService.setTitle($localize`:@@mining.block-fee-rates:Block Fee Rates`);
this.seoService.setTitle($localize`:@@ed8e33059967f554ff06b4f5b6049c465b92d9b3:Block Fee Rates`);
this.miningWindowPreference = this.miningService.getDefaultTimespan('24h');
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);

View file

@ -2,11 +2,10 @@
<div class="full-container">
<div class="card-header mb-0 mb-md-4">
<span i18n="mining.block-fees">Block fees</span>
<button class="btn" style="margin: 0 0 4px 0px" (click)="onSaveChart()">
<span i18n="mining.block-fees">Block Fees</span>
<button class="btn" style="margin: 0 0 4px 0px" (click)="onSaveChart()">
<fa-icon [icon]="['fas', 'download']" [fixedWidth]="true"></fa-icon>
</button>
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(statsObservable$ | async) as stats">
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan">
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 144">

View file

@ -55,7 +55,7 @@ export class BlockFeesGraphComponent implements OnInit {
}
ngOnInit(): void {
this.seoService.setTitle($localize`:@@mining.block-fees:Block Fees`);
this.seoService.setTitle($localize`:@@6c453b11fd7bd159ae30bc381f367bc736d86909:Block Fees`);
this.miningWindowPreference = this.miningService.getDefaultTimespan('24h');
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);
@ -157,7 +157,7 @@ export class BlockFeesGraphComponent implements OnInit {
series: [
{
zlevel: 0,
name: 'Fees',
name: $localize`:@@c20172223f84462032664d717d739297e5a9e2fe:Fees`,
showSymbol: false,
symbol: 'none',
data: data.blockFees,

View file

@ -3,11 +3,10 @@
<div class="full-container">
<div class="card-header mb-0 mb-md-4">
<span i18n="mining.block-rewards">Block rewards</span>
<span i18n="mining.block-rewards">Block Rewards</span>
<button class="btn" style="margin: 0 0 4px 0px" (click)="onSaveChart()">
<fa-icon [icon]="['fas', 'download']" [fixedWidth]="true"></fa-icon>
</button>
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(statsObservable$ | async) as stats">
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan">
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 144">

View file

@ -53,7 +53,7 @@ export class BlockRewardsGraphComponent implements OnInit {
}
ngOnInit(): void {
this.seoService.setTitle($localize`:@@mining.block-reward:Block Reward`);
this.seoService.setTitle($localize`:@@8ba8fe810458280a83df7fdf4c614dfc1a826445:Block Rewards`);
this.miningWindowPreference = this.miningService.getDefaultTimespan('24h');
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);
@ -157,7 +157,7 @@ export class BlockRewardsGraphComponent implements OnInit {
series: [
{
zlevel: 0,
name: 'Reward',
name: $localize`:@@12f86e6747a5ad39e62d3480ddc472b1aeab5b76:Reward`,
showSymbol: false,
symbol: 'none',
data: data.blockRewards,

View file

@ -1,7 +1,7 @@
<div class="full-container">
<div class="card-header mb-0 mb-md-4">
<span i18n="mining.block-size-weight">Block Sizes and Weights</span>
<span i18n="mining.block-sizes-weights">Block Sizes and Weights</span>
<button class="btn" style="margin: 0 0 4px 0px" (click)="onSaveChart()">
<fa-icon [icon]="['fas', 'download']" [fixedWidth]="true"></fa-icon>
</button>

View file

@ -62,7 +62,7 @@ export class BlockSizesWeightsGraphComponent implements OnInit {
ngOnInit(): void {
let firstRun = true;
this.seoService.setTitle($localize`:@@mining.hashrate-difficulty:Hashrate and Weight`);
this.seoService.setTitle($localize`:@@56fa1cd221491b6478998679cba2dc8d55ba330d:Block Sizes and Weights`);
this.miningWindowPreference = this.miningService.getDefaultTimespan('24h');
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);
@ -178,7 +178,7 @@ export class BlockSizesWeightsGraphComponent implements OnInit {
padding: 10,
data: [
{
name: 'Size',
name: $localize`:@@7faaaa08f56427999f3be41df1093ce4089bbd75:Size`,
inactiveColor: 'rgb(110, 112, 121)',
textStyle: {
color: 'white',
@ -186,7 +186,7 @@ export class BlockSizesWeightsGraphComponent implements OnInit {
icon: 'roundRect',
},
{
name: 'Weight',
name: $localize`:@@919f2fd60a898850c24b1584362bbf18a4628bcb:Weight`,
inactiveColor: 'rgb(110, 112, 121)',
textStyle: {
color: 'white',
@ -224,7 +224,7 @@ export class BlockSizesWeightsGraphComponent implements OnInit {
series: data.sizes.length === 0 ? [] : [
{
zlevel: 1,
name: 'Size',
name: $localize`:@@7faaaa08f56427999f3be41df1093ce4089bbd75:Size`,
showSymbol: false,
symbol: 'none',
data: data.sizes,
@ -255,7 +255,7 @@ export class BlockSizesWeightsGraphComponent implements OnInit {
{
zlevel: 1,
yAxisIndex: 0,
name: 'Weight',
name: $localize`:@@919f2fd60a898850c24b1584362bbf18a4628bcb:Weight`,
showSymbol: false,
symbol: 'none',
data: data.weights,

View file

@ -81,15 +81,26 @@
<ng-template [ngIf]="fees !== undefined" [ngIfElse]="loadingFees">
<tr>
<td i18n="block.total-fees|Total fees in a block">Total fees</td>
<td *ngIf="network !== 'liquid' && network !== 'liquidtestnet'; else liquidTotalFees"><app-amount [satoshis]="fees * 100000000" digitsInfo="1.2-2" [noFiat]="true"></app-amount> <span class="fiat"><app-fiat [value]="fees * 100000000" digitsInfo="1.0-0"></app-fiat></span></td>
<td *ngIf="network !== 'liquid' && network !== 'liquidtestnet'; else liquidTotalFees">
<app-amount [satoshis]="block.extras.totalFees" digitsInfo="1.2-3" [noFiat]="true"></app-amount>
<span class="fiat">
<app-fiat [value]="block.extras.totalFees" digitsInfo="1.0-0"></app-fiat>
</span>
</td>
<ng-template #liquidTotalFees>
<td><app-amount [satoshis]="fees * 100000000" digitsInfo="1.2-2" [noFiat]="true"></app-amount>&nbsp; <app-fiat [value]="fees * 100000000" digitsInfo="1.2-2"></app-fiat></td>
<td>
<app-amount [satoshis]="fees * 100000000" digitsInfo="1.2-2" [noFiat]="true"></app-amount>&nbsp; <app-fiat
[value]="fees * 100000000" digitsInfo="1.2-2"></app-fiat>
</td>
</ng-template>
</tr>
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
<td i18n="block.subsidy-and-fees|Total subsidy and fees in a block">Subsidy + fees:</td>
<td>
<app-amount [satoshis]="(blockSubsidy + fees) * 100000000" digitsInfo="1.2-2" [noFiat]="true"></app-amount> <span class="fiat"><app-fiat [value]="(blockSubsidy + fees) * 100000000" digitsInfo="1.0-0"></app-fiat></span>
<app-amount [satoshis]="block.extras.reward" digitsInfo="1.2-3" [noFiat]="true"></app-amount>
<span class="fiat">
<app-fiat [value]="(blockSubsidy + fees) * 100000000" digitsInfo="1.0-0"></app-fiat>
</span>
</td>
</tr>
</ng-template>
@ -105,7 +116,18 @@
</ng-template>
<tr>
<td i18n="block.miner">Miner</td>
<td><app-miner [coinbaseTransaction]="coinbaseTx"></app-miner></td>
<td *ngIf="stateService.env.MINING_DASHBOARD">
<a placement="bottom" [routerLink]="['/mining/pool' | relativeUrl, block.extras.pool.slug]" class="badge"
[class]="block.extras.pool.name === 'Unknown' ? 'badge-secondary' : 'badge-primary'">
{{ block.extras.pool.name }}
</a>
</td>
<td *ngIf="!stateService.env.MINING_DASHBOARD && stateService.env.BASE_MODULE === 'mempool'">
<span placement="bottom" class="badge"
[class]="block.extras.pool.name === 'Unknown' ? 'badge-secondary' : 'badge-primary'">
{{ block.extras.pool.name }}
</span>
</td>
</tr>
</tbody>
</table>
@ -173,7 +195,7 @@
</div>
<div class="clearfix"></div>
<app-transactions-list [transactions]="transactions"></app-transactions-list>
<app-transactions-list [transactions]="transactions" [paginated]="true"></app-transactions-list>
<ng-template [ngIf]="isLoadingTransactions">
<div class="text-center mb-4" class="tx-skeleton">

View file

@ -10,6 +10,7 @@ import { SeoService } from 'src/app/services/seo.service';
import { WebsocketService } from 'src/app/services/websocket.service';
import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe';
import { BlockExtended } from 'src/app/interfaces/node-api.interface';
import { ApiService } from 'src/app/services/api.service';
@Component({
selector: 'app-block',
@ -31,7 +32,6 @@ export class BlockComponent implements OnInit, OnDestroy {
blockSubsidy: number;
fees: number;
paginationMaxSize: number;
coinbaseTx: Transaction;
page = 1;
itemsPerPage: number;
txsLoadingStatus$: Observable<number>;
@ -50,10 +50,11 @@ export class BlockComponent implements OnInit, OnDestroy {
private location: Location,
private router: Router,
private electrsApiService: ElectrsApiService,
private stateService: StateService,
public stateService: StateService,
private seoService: SeoService,
private websocketService: WebsocketService,
private relativeUrlPipe: RelativeUrlPipe,
private apiService: ApiService
) { }
ngOnInit() {
@ -88,7 +89,6 @@ export class BlockComponent implements OnInit, OnDestroy {
const blockHash: string = params.get('id') || '';
this.block = undefined;
this.page = 1;
this.coinbaseTx = undefined;
this.error = undefined;
this.fees = undefined;
this.stateService.markBlock$.next({});
@ -124,7 +124,7 @@ export class BlockComponent implements OnInit, OnDestroy {
this.location.replaceState(
this.router.createUrlTree([(this.network ? '/' + this.network : '') + '/block/', hash]).toString()
);
return this.electrsApiService.getBlock$(hash);
return this.apiService.getBlock$(hash);
})
);
}
@ -134,7 +134,7 @@ export class BlockComponent implements OnInit, OnDestroy {
return of(blockInCache);
}
return this.electrsApiService.getBlock$(blockHash);
return this.apiService.getBlock$(blockHash);
}
}),
tap((block: BlockExtended) => {
@ -145,7 +145,6 @@ export class BlockComponent implements OnInit, OnDestroy {
this.seoService.setTitle($localize`:@@block.component.browser-title:Block ${block.height}:BLOCK_HEIGHT:: ${block.id}:BLOCK_ID:`);
this.isLoadingBlock = false;
this.coinbaseTx = block?.extras?.coinbaseTx;
this.setBlockSubsidy();
if (block?.extras?.reward !== undefined) {
this.fees = block.extras.reward / 100000000 - this.blockSubsidy;
@ -167,9 +166,6 @@ export class BlockComponent implements OnInit, OnDestroy {
if (this.fees === undefined && transactions[0]) {
this.fees = transactions[0].vout.reduce((acc: number, curr: Vout) => acc + curr.value, 0) / 100000000 - this.blockSubsidy;
}
if (!this.coinbaseTx && transactions[0]) {
this.coinbaseTx = transactions[0];
}
this.transactions = transactions;
this.isLoadingTransactions = false;
},
@ -212,13 +208,10 @@ export class BlockComponent implements OnInit, OnDestroy {
this.queryParamsSubscription.unsubscribe();
}
// TODO - Refactor this.fees/this.reward for liquid because it is not
// used anymore on Bitcoin networks (we use block.extras directly)
setBlockSubsidy() {
if (this.network === 'liquid' || this.network === 'liquidtestnet') {
this.blockSubsidy = 0;
return;
}
const halvings = Math.floor(this.block.height / 210000);
this.blockSubsidy = 50 * 2 ** -halvings;
this.blockSubsidy = 0;
}
pageChange(page: number, target: HTMLElement) {

View file

@ -1,27 +1,25 @@
<app-indexing-progress *ngIf="!widget"></app-indexing-progress>
<div class="container-xl" [class]="widget ? 'widget' : 'full-height'">
<h1 *ngIf="!widget" class="float-left" i18n="latest-blocks.blocks">Blocks</h1>
<h1 *ngIf="!widget" class="float-left" i18n="master-page.blocks">Blocks</h1>
<div class="clearfix"></div>
<div style="min-height: 295px">
<table class="table table-borderless">
<thead>
<th class="height" [class]="widget ? 'widget' : ''" i18n="latest-blocks.height">Height</th>
<th class="pool text-left" [class]="widget ? 'widget' : ''" i18n="mining.pool-name">
Pool</th>
<th class="height text-left" [class]="widget ? 'widget' : ''" i18n="latest-blocks.height">Height</th>
<th class="pool text-left" [class]="widget ? 'widget' : ''" i18n="mining.pool-name">Pool</th>
<th class="timestamp" i18n="latest-blocks.timestamp" *ngIf="!widget">Timestamp</th>
<th class="mined" i18n="latest-blocks.mined" *ngIf="!widget">Mined</th>
<th class="reward text-right" i18n="latest-blocks.reward" [class]="widget ? 'widget' : ''">
Reward</th>
<th class="reward text-right" i18n="latest-blocks.reward" [class]="widget ? 'widget' : ''">Reward</th>
<th class="fees text-right" i18n="latest-blocks.fees" *ngIf="!widget">Fees</th>
<th class="txs text-right" i18n="latest-blocks.transactions" [class]="widget ? 'widget' : ''">Txs</th>
<th class="txs text-right" i18n="dashboard.txs" [class]="widget ? 'widget' : ''">TXs</th>
<th class="size" i18n="latest-blocks.size" *ngIf="!widget">Size</th>
</thead>
<tbody *ngIf="blocks$ | async as blocks; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''">
<tr *ngFor="let block of blocks; let i= index; trackBy: trackByBlock">
<td [class]="widget ? 'widget' : ''">
<td class="text-left" [class]="widget ? 'widget' : ''">
<a [routerLink]="['/block' | relativeUrl, block.height]">{{ block.height
}}</a>
</td>
@ -29,7 +27,7 @@
<div class="tooltip-custom">
<a class="clear-link" [routerLink]="['/mining/pool' | relativeUrl, block.extras.pool.slug]">
<img width="22" height="22" src="{{ block.extras.pool['logo'] }}"
onError="this.src = './resources/mining-pools/default.svg'">
onError="this.src = './resources/mining-pools/default.svg'" [alt]="'Logo of ' + block.extras.pool.name + ' mining pool'">
<span class="pool-name">{{ block.extras.pool.name }}</span>
</a>
<span *ngIf="!widget" class="tooltiptext badge badge-secondary scriptmessage">{{ block.extras.coinbaseRaw | hex2ascii }}</span>
@ -42,10 +40,10 @@
<app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since>
</td>
<td class="reward text-right" [class]="widget ? 'widget' : ''">
<app-amount [satoshis]="block.extras.reward" digitsInfo="1.2-2"></app-amount>
<app-amount [satoshis]="block.extras.reward" [noFiat]="true" digitsInfo="1.2-2"></app-amount>
</td>
<td class="fees text-right" *ngIf="!widget">
<app-amount [satoshis]="block.extras.totalFees" digitsInfo="1.2-2"></app-amount>
<app-amount [satoshis]="block.extras.totalFees" [noFiat]="true" digitsInfo="1.2-2"></app-amount>
</td>
<td class="txs text-right" [class]="widget ? 'widget' : ''">
{{ block.tx_count | number }}
@ -62,7 +60,7 @@
<ng-template #skeleton>
<tbody>
<tr *ngFor="let item of skeletonLines">
<td class="height" [class]="widget ? 'widget' : ''">
<td class="height text-left" [class]="widget ? 'widget' : ''">
<span class="skeleton-loader" style="max-width: 75px"></span>
</td>
<td class="pool text-left" [class]="widget ? 'widget' : ''">

View file

@ -11,13 +11,10 @@
max-width: 100%;
}
td {
padding-top: 0.7rem !important;
tr, td, th {
border: 0px;
padding-top: 0.65rem !important;
padding-bottom: 0.7rem !important;
@media (max-width: 376px) {
padding-top: 0.73rem !important;
padding-bottom: 0.73rem !important;
}
}
.clear-link {
@ -41,7 +38,7 @@ td {
}
.pool.widget {
width: 40%;
padding-left: 30px;
padding-left: 24px;
@media (max-width: 376px) {
width: 60%;
}
@ -56,7 +53,7 @@ td {
width: 10%;
}
.height.widget {
width: 20%;
width: 15%;
@media (max-width: 576px) {
width: 10%;
}

View file

@ -39,7 +39,7 @@ export class BlocksList implements OnInit {
this.websocketService.want(['blocks']);
}
this.skeletonLines = this.widget === true ? [...Array(5).keys()] : [...Array(15).keys()];
this.skeletonLines = this.widget === true ? [...Array(6).keys()] : [...Array(15).keys()];
this.paginationMaxSize = window.matchMedia('(max-width: 670px)').matches ? 3 : 5;
this.blocks$ = combineLatest([
@ -61,7 +61,7 @@ export class BlocksList implements OnInit {
block.extras.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg';
}
if (this.widget) {
return blocks.slice(0, 5);
return blocks.slice(0, 6);
}
return blocks;
}),
@ -85,7 +85,7 @@ export class BlocksList implements OnInit {
blocks[1][0].extras.pool.logo = `./resources/mining-pools/` +
blocks[1][0].extras.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg';
acc.unshift(blocks[1][0]);
acc = acc.slice(0, this.widget ? 5 : 15);
acc = acc.slice(0, this.widget ? 6 : 15);
return acc;
}, [])
);

View file

@ -22,7 +22,7 @@
</tr>
</tbody>
<tbody *ngIf="isLoading">
<tr *ngFor="let item of [1,2,3,4,5]">
<tr *ngFor="let item of [1,2,3,4,5,6]">
<td class="d-none d-md-block w-75"><span class="skeleton-loader"></span></td>
<td class="text-left"><span class="skeleton-loader w-75"></span></td>
<td class="text-right"><span class="skeleton-loader w-75"></span></td>

View file

@ -2,10 +2,15 @@
width: 100%;
text-align: left;
table-layout:fixed;
tr, td, th {
tr, th {
border: 0px;
padding-top: 0.65rem !important;
padding-bottom: 0.7rem !important;
}
td {
border: 0px;
padding-top: 0.71rem !important;
padding-bottom: 0.75rem !important;
width: 25%;
@media (max-width: 376px) {
padding: 0.85rem;

View file

@ -54,7 +54,7 @@ export class DifficultyAdjustmentsTable implements OnInit {
return {
availableTimespanDay: availableTimespanDay,
difficulty: tableData.slice(0, 5),
difficulty: tableData.slice(0, 6),
};
}),
);

View file

@ -5,33 +5,19 @@
<button class="btn btn-primary w-100" id="dropdownBasic1" ngbDropdownToggle i18n="mining">Mining</button>
<div ngbDropdownMenu aria-labelledby="dropdownBasic1">
<a class="dropdown-item" routerLinkActive="active" [routerLink]="['/graphs/mining/pools' | relativeUrl]"
i18n="mining.pools">
Pools ranking
</a>
i18n="mining.pools">Pools Ranking</a>
<a class="dropdown-item" routerLinkActive="active" [routerLink]="['/graphs/mining/pools-dominance' | relativeUrl]"
i18n="mining.pools-dominance">
Pools dominance
</a>
i18n="mining.pools-dominance">Pools Dominance</a>
<a class="dropdown-item" routerLinkActive="active"
[routerLink]="['/graphs/mining/hashrate-difficulty' | relativeUrl]" i18n="mining.hashrate-difficulty">
Hashrate & Difficulty
</a>
[routerLink]="['/graphs/mining/hashrate-difficulty' | relativeUrl]" i18n="mining.hashrate-difficulty">Hashrate & Difficulty</a>
<a class="dropdown-item" routerLinkActive="active"
[routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]" i18n="mining.block-fee-rates">
Block Fee Rates
</a>
[routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]" i18n="mining.block-fee-rates">Block Fee Rates</a>
<a class="dropdown-item" routerLinkActive="active"
[routerLink]="['/graphs/mining/block-fees' | relativeUrl]" i18n="mining.block-fees">
Block Fees
</a>
[routerLink]="['/graphs/mining/block-fees' | relativeUrl]" i18n="mining.block-fees">Block Fees</a>
<a class="dropdown-item" routerLinkActive="active"
[routerLink]="['/graphs/mining/block-rewards' | relativeUrl]" i18n="mining.block-rewards">
Block Rewards
</a>
[routerLink]="['/graphs/mining/block-rewards' | relativeUrl]" i18n="mining.block-rewards">Block Rewards</a>
<a class="dropdown-item" routerLinkActive="active"
[routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]" i18n="mining.block-sizes-weights">
Block Sizes and Weights
</a>
[routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]" i18n="mining.block-sizes-weights">Block Sizes and Weights</a>
</div>
</div>
</div>

View file

@ -11,7 +11,7 @@
</p>
</div>
<div class="item">
<h5 class="card-title" i18n="master-page.blocks">Difficulty</h5>
<h5 class="card-title" i18n="block.difficulty">Difficulty</h5>
<p class="card-text">
{{ hashrates.currentDifficulty | amountShortener }}
</p>
@ -64,13 +64,13 @@
<ng-template #loadingStats>
<div class="pool-distribution">
<div class="item">
<h5 class="card-title" i18n="mining.miners-luck">Hashrate</h5>
<h5 class="card-title" i18n="mining.hashrate">Hashrate</h5>
<p class="card-text">
<span class="skeleton-loader skeleton-loader-big"></span>
</p>
</div>
<div class="item">
<h5 class="card-title" i18n="master-page.blocks">Difficulty</h5>
<h5 class="card-title" i18n="block.difficulty">Difficulty</h5>
<p class="card-text">
<span class="skeleton-loader skeleton-loader-big"></span>
</p>

View file

@ -52,7 +52,7 @@
.chart-widget {
width: 100%;
height: 100%;
max-height: 270px;
height: 240px;
}
.formRadioGroup {

View file

@ -64,7 +64,7 @@ export class HashrateChartComponent implements OnInit {
if (this.widget) {
this.miningWindowPreference = '1y';
} else {
this.seoService.setTitle($localize`:@@mining.hashrate-difficulty:Hashrate and Difficulty`);
this.seoService.setTitle($localize`:@@3510fc6daa1d975f331e3a717bdf1a34efa06dff:Hashrate & Difficulty`);
this.miningWindowPreference = this.miningService.getDefaultTimespan('1m');
}
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
@ -223,7 +223,7 @@ export class HashrateChartComponent implements OnInit {
legend: (this.widget || data.hashrates.length === 0) ? undefined : {
data: [
{
name: 'Hashrate',
name: $localize`:@@79a9dc5b1caca3cbeb1733a19515edacc5fc7920:Hashrate`,
inactiveColor: 'rgb(110, 112, 121)',
textStyle: {
color: 'white',
@ -234,9 +234,9 @@ export class HashrateChartComponent implements OnInit {
},
},
{
name: 'Difficulty',
name: $localize`:@@25148835d92465353fc5fe8897c27d5369978e5a:Difficulty`,
inactiveColor: 'rgb(110, 112, 121)',
textStyle: {
textStyle: {
color: 'white',
},
icon: 'roundRect',
@ -290,7 +290,7 @@ export class HashrateChartComponent implements OnInit {
series: data.hashrates.length === 0 ? [] : [
{
zlevel: 0,
name: 'Hashrate',
name: $localize`:@@79a9dc5b1caca3cbeb1733a19515edacc5fc7920:Hashrate`,
showSymbol: false,
symbol: 'none',
data: data.hashrates,
@ -302,7 +302,7 @@ export class HashrateChartComponent implements OnInit {
{
zlevel: 1,
yAxisIndex: 1,
name: 'Difficulty',
name: $localize`:@@25148835d92465353fc5fe8897c27d5369978e5a:Difficulty`,
showSymbol: false,
symbol: 'none',
data: data.difficulty,

View file

@ -3,11 +3,10 @@
<div class="full-container">
<div class="card-header mb-0 mb-md-4">
<span i18n="mining.pools-dominance">Mining pools dominance</span>
<span i18n="mining.pools-dominance">Pools Dominance</span>
<button class="btn" style="margin: 0 0 4px 0px" (click)="onSaveChart()">
<fa-icon [icon]="['fas', 'download']" [fixedWidth]="true"></fa-icon>
</button>
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(hashrateObservable$ | async) as stats">
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan">
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 4320">

View file

@ -1,5 +1,5 @@
<div class="container-xl">
<h1 class="float-left" i18n="latest-blocks.blocks">Blocks</h1>
<h1 class="float-left" i18n="master-page.blocks">Blocks</h1>
<br>
<div class="clearfix"></div>

View file

@ -36,7 +36,7 @@ export class LatestBlocksComponent implements OnInit, OnDestroy {
) { }
ngOnInit() {
this.seoService.setTitle($localize`:@@f4cba7faeb126346f09cc6af30124f9a343f7a28:Blocks`);
this.seoService.setTitle($localize`:@@8a7b4bd44c0ac71b2e72de0398b303257f7d2f54:Blocks`);
this.websocketService.want(['blocks']);
this.network$ = merge(of(''), this.stateService.networkChanged$);

View file

@ -3,7 +3,7 @@
<nav class="navbar navbar-expand-md navbar-dark bg-dark">
<a class="navbar-brand" [routerLink]="['/' | relativeUrl]" style="position: relative;">
<ng-container *ngIf="{ val: connectionState$ | async } as connectionState">
<img [src]="officialMempoolSpace ? './resources/mempool-space-logo.png' : './resources/mempool-logo.png'" height="35" width="140" class="logo" [ngStyle]="{'opacity': connectionState.val === 2 ? 1 : 0.5 }">
<img [src]="officialMempoolSpace ? './resources/mempool-space-logo.png' : './resources/mempool-logo.png'" height="35" width="140" class="logo" [ngStyle]="{'opacity': connectionState.val === 2 ? 1 : 0.5 }" alt="The Mempool Open Source Project logo">
<div class="connection-badge">
<div class="badge badge-warning" *ngIf="connectionState.val === 0" i18n="master-page.offline">Offline</div>
<div class="badge badge-warning" *ngIf="connectionState.val === 1" i18n="master-page.reconnecting">Reconnecting...</div>
@ -13,16 +13,16 @@
<div (window:resize)="onResize($event)" ngbDropdown class="dropdown-container" *ngIf="env.TESTNET_ENABLED || env.SIGNET_ENABLED || env.LIQUID_ENABLED || env.BISQ_ENABLED || env.LIQUID_TESTNET_ENABLED">
<button ngbDropdownToggle type="button" class="btn btn-secondary dropdown-toggle-split" aria-haspopup="true">
<img src="./resources/{{ network.val === '' ? 'bitcoin' : network.val }}-logo.png" style="width: 25px; height: 25px;" class="mr-1">
<img src="./resources/{{ network.val === '' ? 'bitcoin' : network.val }}-logo.png" style="width: 25px; height: 25px;" class="mr-1" [alt]="(network.val === '' ? 'bitcoin' : network.val) + ' logo'">
</button>
<div ngbDropdownMenu [ngClass]="{'dropdown-menu-right' : isMobile}">
<button ngbDropdownItem class="mainnet" routerLink="/"><img src="./resources/bitcoin-logo.png" style="width: 30px;" class="mr-1"> Mainnet</button>
<button ngbDropdownItem *ngIf="env.SIGNET_ENABLED" class="signet" [class.active]="network.val === 'signet'" routerLink="/signet"><img src="./resources/signet-logo.png" style="width: 30px;" class="mr-1"> Signet</button>
<button ngbDropdownItem *ngIf="env.TESTNET_ENABLED" class="testnet" [class.active]="network.val === 'testnet'" routerLink="/testnet"><img src="./resources/testnet-logo.png" style="width: 30px;" class="mr-1"> Testnet</button>
<button ngbDropdownItem class="mainnet" routerLink="/"><img src="./resources/bitcoin-logo.png" style="width: 30px;" class="mr-1" alt="bitcoin logo"> Mainnet</button>
<button ngbDropdownItem *ngIf="env.SIGNET_ENABLED" class="signet" [class.active]="network.val === 'signet'" routerLink="/signet"><img src="./resources/signet-logo.png" style="width: 30px;" class="mr-1" alt="signet logo"> Signet</button>
<button ngbDropdownItem *ngIf="env.TESTNET_ENABLED" class="testnet" [class.active]="network.val === 'testnet'" routerLink="/testnet"><img src="./resources/testnet-logo.png" style="width: 30px;" class="mr-1" alt="testnet logo"> Testnet</button>
<h6 *ngIf="env.LIQUID_ENABLED || env.BISQ_ENABLED" class="dropdown-header" i18n="master-page.layer2-networks-header">Layer 2 Networks</h6>
<a [href]="env.BISQ_WEBSITE_URL + urlLanguage" ngbDropdownItem *ngIf="env.BISQ_ENABLED" class="bisq"><img src="./resources/bisq-logo.png" style="width: 30px;" class="mr-1"> Bisq</a>
<a [href]="env.LIQUID_WEBSITE_URL + urlLanguage" ngbDropdownItem *ngIf="env.LIQUID_ENABLED" class="liquid" [class.active]="network.val === 'liquid'"><img src="./resources/liquid-logo.png" style="width: 30px;" class="mr-1"> Liquid</a>
<a [href]="env.LIQUID_WEBSITE_URL + urlLanguage + '/testnet'" ngbDropdownItem *ngIf="env.LIQUID_TESTNET_ENABLED" class="liquidtestnet" [class.active]="network.val === 'liquid'"><img src="./resources/liquidtestnet-logo.png" style="width: 30px;" class="mr-1"> Liquid Testnet</a>
<a [href]="env.BISQ_WEBSITE_URL + urlLanguage" ngbDropdownItem *ngIf="env.BISQ_ENABLED" class="bisq"><img src="./resources/bisq-logo.png" style="width: 30px;" class="mr-1" alt="bisq logo"> Bisq</a>
<a [href]="env.LIQUID_WEBSITE_URL + urlLanguage" ngbDropdownItem *ngIf="env.LIQUID_ENABLED" class="liquid" [class.active]="network.val === 'liquid'"><img src="./resources/liquid-logo.png" style="width: 30px;" class="mr-1" alt="liquid mainnet logo"> Liquid</a>
<a [href]="env.LIQUID_WEBSITE_URL + urlLanguage + '/testnet'" ngbDropdownItem *ngIf="env.LIQUID_TESTNET_ENABLED" class="liquidtestnet" [class.active]="network.val === 'liquid'"><img src="./resources/liquidtestnet-logo.png" style="width: 30px;" class="mr-1" alt="liquid testnet logo"> Liquid Testnet</a>
</div>
</div>
@ -32,7 +32,7 @@
<a class="nav-link" [routerLink]="['/' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'tachometer-alt']" [fixedWidth]="true" i18n-title="master-page.dashboard" title="Dashboard"></fa-icon></a>
</li>
<li class="nav-item" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}" id="btn-pools" *ngIf="stateService.env.MINING_DASHBOARD">
<a class="nav-link" [routerLink]="['/mining' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'hammer']" [fixedWidth]="true" i18n-title="master-page.mining-dashboard" title="Mining Dashboard"></fa-icon></a>
<a class="nav-link" [routerLink]="['/mining' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'hammer']" [fixedWidth]="true" i18n-title="mining.mining-dashboard" title="Mining Dashboard"></fa-icon></a>
</li>
<li class="nav-item" routerLinkActive="active" id="btn-blocks" *ngIf="!stateService.env.MINING_DASHBOARD">
<a class="nav-link" [routerLink]="['/blocks' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'cubes']" [fixedWidth]="true" i18n-title="master-page.blocks" title="Blocks"></fa-icon></a>

View file

@ -21,7 +21,7 @@
<td><span class="yellow-color">{{ mempoolBlock.feeRange[0] | number:'1.0-0' }} - {{ mempoolBlock.feeRange[mempoolBlock.feeRange.length - 1] | number:'1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></span></td>
</tr>
<tr>
<td i18n="mempool-block.total-fees">Total fees</td>
<td i18n="block.total-fees|Total fees in a block">Total fees</td>
<td><app-amount [satoshis]="mempoolBlock.totalFees" [digitsInfo]="'1.2-2'" [noFiat]="true"></app-amount> <span class="fiat"><app-fiat [value]="mempoolBlock.totalFees" digitsInfo="1.0-0"></app-fiat></span></td>
</tr>
<tr>

View file

@ -68,7 +68,7 @@ export class MempoolBlockComponent implements OnInit, OnDestroy {
getOrdinal(mempoolBlock: MempoolBlock): string {
const blocksInBlock = Math.ceil(mempoolBlock.blockVSize / this.stateService.blockVSize);
if (this.mempoolBlockIndex === 0) {
return $localize`:@@mempool-block.next.block:Next block`;
return $localize`:@@bdf0e930eb22431140a2eaeacd809cc5f8ebd38c:Next Block`;
} else if (this.mempoolBlockIndex === this.stateService.env.KEEP_BLOCKS_AMOUNT - 1 && blocksInBlock > 1) {
return $localize`:@@mempool-block.stack.of.blocks:Stack of ${blocksInBlock}:INTERPOLATION: mempool blocks`;
} else {

View file

@ -1,12 +0,0 @@
<ng-template [ngIf]="loading" [ngIfElse]="done">
<span class="skeleton-loader"></span>
</ng-template>
<ng-template #done>
<ng-template [ngIf]="miner" [ngIfElse]="unknownMiner">
<a placement="bottom" [ngbTooltip]="title" [href]="url" [target]="target" class="badge badge-primary">{{ miner }}</a>
</ng-template>
<ng-template #unknownMiner>
<span class="badge badge-secondary" i18n="miner.tag.unknown-miner">Unknown</span>
</ng-template>
</ng-template>

View file

@ -1,3 +0,0 @@
.badge {
font-size: 14px;
}

View file

@ -1,94 +0,0 @@
import { Component, Input, OnChanges, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
import { AssetsService } from 'src/app/services/assets.service';
import { Transaction } from 'src/app/interfaces/electrs.interface';
import { StateService } from 'src/app/services/state.service';
import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe';
@Component({
selector: 'app-miner',
templateUrl: './miner.component.html',
styleUrls: ['./miner.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MinerComponent implements OnChanges {
@Input() coinbaseTransaction: Transaction;
miner = '';
title = '';
url = '';
target = '_blank';
loading = true;
constructor(
private assetsService: AssetsService,
private cd: ChangeDetectorRef,
public stateService: StateService,
private relativeUrlPipe: RelativeUrlPipe,
) { }
ngOnChanges() {
this.miner = '';
if (this.stateService.env.MINING_DASHBOARD) {
this.miner = 'Unknown';
this.url = this.relativeUrlPipe.transform(`/mining/pool/unknown`);
this.target = '';
}
this.loading = true;
this.findMinerFromCoinbase();
}
findMinerFromCoinbase() {
if (this.coinbaseTransaction == null || this.coinbaseTransaction.vin == null || this.coinbaseTransaction.vin.length === 0) {
return null;
}
this.assetsService.getMiningPools$.subscribe((pools) => {
for (const vout of this.coinbaseTransaction.vout) {
if (!vout.scriptpubkey_address) {
continue;
}
if (pools.payout_addresses[vout.scriptpubkey_address]) {
this.miner = pools.payout_addresses[vout.scriptpubkey_address].name;
this.title = $localize`:@@miner-identified-by-payout:Identified by payout address: '${vout.scriptpubkey_address}:PAYOUT_ADDRESS:'`;
const pool = pools.payout_addresses[vout.scriptpubkey_address];
if (this.stateService.env.MINING_DASHBOARD && pools.slugs && pools.slugs[pool.name] !== undefined) {
this.url = this.relativeUrlPipe.transform(`/mining/pool/${pools.slugs[pool.name]}`);
this.target = '';
} else {
this.url = pool.link;
}
break;
}
for (const tag in pools.coinbase_tags) {
if (pools.coinbase_tags.hasOwnProperty(tag)) {
const coinbaseAscii = this.hex2ascii(this.coinbaseTransaction.vin[0].scriptsig);
if (coinbaseAscii.indexOf(tag) > -1) {
const pool = pools.coinbase_tags[tag];
this.miner = pool.name;
this.title = $localize`:@@miner-identified-by-coinbase:Identified by coinbase tag: '${tag}:TAG:'`;
if (this.stateService.env.MINING_DASHBOARD && pools.slugs && pools.slugs[pool.name] !== undefined) {
this.url = this.relativeUrlPipe.transform(`/mining/pool/${pools.slugs[pool.name]}`);
this.target = '';
} else {
this.url = pool.link;
}
break;
}
}
}
}
this.loading = false;
this.cd.markForCheck();
});
}
hex2ascii(hex: string) {
let str = '';
for (let i = 0; i < hex.length; i += 2) {
str += String.fromCharCode(parseInt(hex.substr(i, 2), 16));
}
return str;
}
}

View file

@ -11,7 +11,7 @@
<span style="font-size: xx-small" i18n="mining.144-blocks">(144 blocks)</span>
</div>
<div class="card-wrapper">
<div class="card" style="height: 123px">
<div class="card">
<div class="card-body more-padding">
<app-reward-stats></app-reward-stats>
</div>
@ -22,29 +22,25 @@
<!-- difficulty adjustment -->
<div class="col">
<div class="main-title" i18n="dashboard.difficulty-adjustment">Difficulty Adjustment</div>
<div class="card" style="height: 123px">
<app-difficulty [showTitle]="false" [showProgress]="false" [showHalving]="true"></app-difficulty>
</div>
<app-difficulty [showTitle]="false" [showProgress]="false" [showHalving]="true"></app-difficulty>
</div>
<!-- pool distribution -->
<div class="col">
<div class="card">
<div class="card-body">
<div class="col" style="margin-bottom: 1.47rem">
<div class="card graph-card">
<div class="card-body pl-2 pr-2">
<app-pool-ranking [widget]=true></app-pool-ranking>
<div class="mt-1"><a [routerLink]="['/graphs/mining/pools' | relativeUrl]" i18n="dashboard.view-more">View more
&raquo;</a></div>
<div class="mt-1"><a [routerLink]="['/graphs/mining/pools' | relativeUrl]" i18n="dashboard.view-more">View more &raquo;</a></div>
</div>
</div>
</div>
<!-- hashrate -->
<div class="col">
<div class="col" style="margin-bottom: 1.47rem">
<div class="card">
<div class="card-body">
<app-hashrate-chart [widget]=true></app-hashrate-chart>
<div class="mt-1"><a [routerLink]="['/graphs/mining/hashrate-difficulty' | relativeUrl]" i18n="dashboard.view-more">View more
&raquo;</a></div>
<div class="card-body pl-lg-3 pr-lg-3 pl-2 pr-2">
<app-hashrate-chart [widget]="true"></app-hashrate-chart>
<div class="mt-1"><a [routerLink]="['/graphs/mining/hashrate-difficulty' | relativeUrl]" i18n="dashboard.view-more">View more &raquo;</a></div>
</div>
</div>
</div>
@ -53,12 +49,9 @@
<div class="col">
<div class="card">
<div class="card-body">
<h5 class="card-title">
Latest blocks
</h5>
<h5 class="card-title" i18n="dashboard.latest-blocks">Latest blocks</h5>
<app-blocks-list [widget]=true></app-blocks-list>
<div><a [routerLink]="['/mining/blocks' | relativeUrl]" i18n="dashboard.view-more">View
more &raquo;</a></div>
<div><a [routerLink]="['/mining/blocks' | relativeUrl]" i18n="dashboard.view-more">View more &raquo;</a></div>
</div>
</div>
</div>
@ -67,12 +60,9 @@
<div class="col">
<div class="card">
<div class="card-body">
<h5 class="card-title">
Adjustments
</h5>
<h5 class="card-title" i18n="dashboard.adjustments">Adjustments</h5>
<app-difficulty-adjustments-table></app-difficulty-adjustments-table>
<div><a [routerLink]="['/graphs/mining/hashrate-difficulty' | relativeUrl]" i18n="dashboard.view-more">View more
&raquo;</a></div>
<div><a [routerLink]="['/graphs/mining/hashrate-difficulty' | relativeUrl]" i18n="dashboard.view-more">View more &raquo;</a></div>
</div>
</div>
</div>

View file

@ -14,6 +14,13 @@
background-color: #1d1f31;
}
.graph-card {
height: 100%;
@media (min-width: 992px) {
height: 385px;
}
}
.card-title {
font-size: 1rem;
color: #4a68b9;
@ -22,9 +29,6 @@
color: #4a68b9;
}
.card-body {
padding: 1.25rem 1rem 0.75rem 1rem;
}
.card-body.pool-ranking {
padding: 1.25rem 0.25rem 0.75rem 0.25rem;
}

View file

@ -13,7 +13,7 @@ export class MiningDashboardComponent implements OnInit {
private seoService: SeoService,
private websocketService: WebsocketService,
) {
this.seoService.setTitle($localize`:@@mining.mining-dashboard:Mining Dashboard`);
this.seoService.setTitle($localize`:@@a681a4e2011bb28157689dbaa387de0dd0aa0c11:Mining Dashboard`);
}
ngOnInit(): void {

View file

@ -5,7 +5,7 @@
<div *ngIf="widget">
<div class="pool-distribution" *ngIf="(miningStatsObservable$ | async) as miningStats; else loadingReward">
<div class="item">
<h5 class="card-title" i18n="mining.miners-luck">Pools luck (1w)</h5>
<h5 class="card-title" i18n="mining.miners-luck">Pools Luck (1w)</h5>
<p class="card-text">
{{ miningStats['minersLuck'] }}%
</p>
@ -17,7 +17,7 @@
</p>
</div>
<div class="item">
<h5 class="card-title" i18n="mining.miners-count">Pools count (1w)</h5>
<h5 class="card-title" i18n="mining.miners-count">Pools Count (1w)</h5>
<p class="card-text">
{{ miningStats.pools.length }}
</p>
@ -26,11 +26,10 @@
</div>
<div class="card-header" *ngIf="!widget">
<span i18n="mining.mining-pool-share">Mining pools share</span>
<span i18n="mining.pools">Pools Ranking</span>
<button class="btn" style="margin: 0 0 4px 0px" (click)="onSaveChart()">
<fa-icon [icon]="['fas', 'download']" [fixedWidth]="true"></fa-icon>
</button>
<form [formGroup]="radioGroupForm" class="formRadioGroup"
*ngIf="!widget && (miningStatsObservable$ | async) as stats">
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan">
@ -62,7 +61,7 @@
<input ngbButton type="radio" [value]="'3y'" fragment="3y"> 3Y
</label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.totalBlockCount > 157680">
<input ngbButton type="radio" [value]="'all'" fragment="all"> ALL
<input ngbButton type="radio" [value]="'all'" fragment="all"><span i18n>All</span>
</label>
</div>
</form>
@ -87,14 +86,15 @@
<th class="" i18n="mining.pool-name">Pool</th>
<th class="" *ngIf="this.miningWindowPreference === '24h'" i18n="mining.hashrate">Hashrate</th>
<th class="" i18n="master-page.blocks">Blocks</th>
<th class="d-none d-md-block" i18n="mining.empty-blocks">Empty Blocks</th>
<th class="d-none d-md-block" i18n="mining.empty-blocks">Empty blocks</th>
</tr>
</thead>
<tbody *ngIf="(miningStatsObservable$ | async) as miningStats">
<tr *ngFor="let pool of miningStats.pools">
<td class="d-none d-md-block">{{ pool.rank }}</td>
<td class="text-right"><img width="25" height="25" src="{{ pool.logo }}"
onError="this.src = './resources/mining-pools/default.svg'"></td>
<td class="text-right">
<img width="25" height="25" src="{{ pool.logo }}" [alt]="pool.name + ' mining pool logo'" onError="this.src = './resources/mining-pools/default.svg'">
</td>
<td class=""><a [routerLink]="[('/mining/pool/' + pool.slug) | relativeUrl]">{{ pool.name }}</a></td>
<td class="" *ngIf="this.miningWindowPreference === '24h' && !isLoading">{{ pool.lastEstimatedHashrate }} {{
miningStats.miningUnits.hashrateUnit }}</td>
@ -104,7 +104,7 @@
<tr style="border-top: 1px solid #555">
<td class="d-none d-md-block"></td>
<td class="text-right"></td>
<td class="" i18n="mining.all-miners"><b>All miners</b></td>
<td class=""><b i18n="mining.all-miners">All miners</b></td>
<td class="" *ngIf="this.miningWindowPreference === '24h'"><b>{{ miningStats.lastEstimatedHashrate}} {{
miningStats.miningUnits.hashrateUnit }}</b></td>
<td class=""><b>{{ miningStats.blockCount }}</b></td>
@ -121,7 +121,7 @@
<ng-template #loadingReward>
<div class="pool-distribution">
<div class="item">
<h5 class="card-title" i18n="mining.miners-luck">Pools luck (1w)</h5>
<h5 class="card-title" i18n="mining.miners-luck">Pools Luck (1w)</h5>
<p class="card-text">
<span class="skeleton-loader skeleton-loader-big"></span>
</p>
@ -133,7 +133,7 @@
</p>
</div>
<div class="item">
<h5 class="card-title" i18n="mining.miners-count">Pools count (1w)</h5>
<h5 class="card-title" i18n="mining.miners-count">Pools Count (1w)</h5>
<p class="card-text">
<span class="skeleton-loader skeleton-loader-big"></span>
</p>

View file

@ -27,7 +27,7 @@
.chart-widget {
width: 100%;
height: 100%;
max-height: 270px;
height: 240px;
@media (max-width: 485px) {
max-height: 200px;
}

View file

@ -153,13 +153,14 @@ export class PoolRankingComponent implements OnInit {
},
borderColor: '#000',
formatter: () => {
const i = pool.blockCount.toString();
if (this.miningWindowPreference === '24h') {
return `<b style="color: white">${pool.name} (${pool.share}%)</b><br>` +
pool.lastEstimatedHashrate.toString() + ' PH/s' +
`<br>` + pool.blockCount.toString() + ` blocks`;
`<br>` + $localize`${i} blocks`;
} else {
return `<b style="color: white">${pool.name} (${pool.share}%)</b><br>` +
pool.blockCount.toString() + ` blocks`;
$localize`${i} blocks`;
}
}
},

View file

@ -5,7 +5,7 @@
<!-- Pool overview -->
<div *ngIf="poolStats$ | async as poolStats; else loadingMain">
<div style="display:flex" class="mb-3">
<img width="50" height="50" src="{{ poolStats['logo'] }}"
<img width="50" height="50" src="{{ poolStats['logo'] }}" [alt]="poolStats.pool.name + ' mining pool logo'"
onError="this.src = './resources/mining-pools/default.svg'" class="mr-3">
<h1 class="m-0 pt-1 pt-md-0">{{ poolStats.pool.name }}</h1>
</div>
@ -93,10 +93,8 @@
<table class="table table-xs table-data">
<thead>
<tr>
<th scope="col" class="block-count-title" style="width: 37%" i18n="mining.estimated">Estimated
</th>
<th scope="col" class="block-count-title" style="width: 37%" i18n="mining.reported">Reported
</th>
<th scope="col" class="block-count-title" style="width: 37%" i18n="mining.estimated">Estimated</th>
<th scope="col" class="block-count-title" style="width: 37%" i18n="mining.reported">Reported</th>
<th scope="col" class="block-count-title" style="width: 26%" i18n="mining.luck">Luck</th>
</tr>
</thead>
@ -117,10 +115,8 @@
<table class="table table-xs table-data">
<thead>
<tr>
<th scope="col" class="block-count-title" style="width: 33%" i18n="mining.estimated">Estimated
</th>
<th scope="col" class="block-count-title" style="width: 37%" i18n="mining.reported">Reported
</th>
<th scope="col" class="block-count-title" style="width: 33%" i18n="mining.estimated">Estimated</th>
<th scope="col" class="block-count-title" style="width: 37%" i18n="mining.reported">Reported</th>
<th scope="col" class="block-count-title" style="width: 30%" i18n="mining.luck">Luck</th>
</tr>
</thead>
@ -142,7 +138,7 @@
<!-- Mined blocks desktop -->
<tr *ngIf="!isMobile()" class="taller-row">
<td class="label" i18n="mining.mined-blocks">Mined Blocks</td>
<td class="label" i18n="mining.mined-blocks">Mined blocks</td>
<td class="data">
<table class="table table-xs table-data">
<thead>
@ -166,7 +162,7 @@
<!-- Mined blocks mobile -->
<tr *ngIf="isMobile()">
<td colspan=2>
<span class="label" i18n="mining.mined-blocks">Mined Blocks</span>
<span class="label" i18n="mining.mined-blocks">Mined blocks</span>
<table class="table table-xs table-data">
<thead>
<tr>
@ -216,12 +212,10 @@
<th class="height" i18n="latest-blocks.height">Height</th>
<th class="timestamp" i18n="latest-blocks.timestamp">Timestamp</th>
<th class="mined" i18n="latest-blocks.mined">Mined</th>
<th class="coinbase text-left" i18n="latest-blocks.coinbasetag">
Coinbase Tag</th>
<th class="reward text-right" i18n="latest-blocks.reward">
Reward</th>
<th class="coinbase text-left" i18n="latest-blocks.coinbasetag">Coinbase tag</th>
<th class="reward text-right" i18n="latest-blocks.reward">Reward</th>
<th class="fees text-right" i18n="latest-blocks.fees">Fees</th>
<th class="txs text-right" i18n="latest-blocks.transactions">Txs</th>
<th class="txs text-right" i18n="dashboard.txs">TXs</th>
<th class="size" i18n="latest-blocks.size">Size</th>
</thead>
<tbody [style]="isLoading ? 'opacity: 0.75' : ''">
@ -266,12 +260,10 @@
<th class="height" i18n="latest-blocks.height">Height</th>
<th class="timestamp" i18n="latest-blocks.timestamp">Timestamp</th>
<th class="mined" i18n="latest-blocks.mined">Mined</th>
<th class="coinbase text-left" i18n="latest-blocks.coinbasetag">
Coinbase Tag</th>
<th class="reward text-right" i18n="latest-blocks.reward">
Reward</th>
<th class="coinbase text-left" i18n="latest-blocks.coinbasetag">Coinbase tag</th>
<th class="reward text-right" i18n="latest-blocks.reward">Reward</th>
<th class="fees text-right" i18n="latest-blocks.fees">Fees</th>
<th class="txs text-right" i18n="latest-blocks.transactions">Txs</th>
<th class="txs text-right" i18n="dashboard.txs">TXs</th>
<th class="size" i18n="latest-blocks.size">Size</th>
</thead>
<tbody>
@ -378,10 +370,8 @@
<table class="table table-xs table-data text-center">
<thead>
<tr>
<th scope="col" class="block-count-title" style="width: 37%" i18n="mining.estimated">Estimated
</th>
<th scope="col" class="block-count-title" style="width: 37%" i18n="mining.reported">Reported
</th>
<th scope="col" class="block-count-title" style="width: 37%" i18n="mining.estimated">Estimated</th>
<th scope="col" class="block-count-title" style="width: 37%" i18n="mining.reported">Reported</th>
<th scope="col" class="block-count-title" style="width: 26%" i18n="mining.luck">Luck</th>
</tr>
</thead>
@ -406,10 +396,8 @@
<table class="table table-xs table-data text-center">
<thead>
<tr>
<th scope="col" class="block-count-title" style="width: 33%" i18n="mining.estimated">Estimated
</th>
<th scope="col" class="block-count-title" style="width: 37%" i18n="mining.reported">Reported
</th>
<th scope="col" class="block-count-title" style="width: 33%" i18n="mining.estimated">Estimated</th>
<th scope="col" class="block-count-title" style="width: 37%" i18n="mining.reported">Reported</th>
<th scope="col" class="block-count-title" style="width: 30%" i18n="mining.luck">Luck</th>
</tr>
</thead>
@ -430,13 +418,13 @@
<!-- Mined blocks desktop -->
<tr *ngIf="!isMobile()" class="taller-row">
<td class="label" i18n="mining.mined-blocks">Mined Blocks</td>
<td class="label" i18n="mining.mined-blocks">Mined blocks</td>
<td class="data">
<table class="table table-xs table-data text-center">
<thead>
<tr>
<th scope="col" class="block-count-title" style="width: 37%" i18n="24h">24h</th>
<th scope="col" class="block-count-title" style="width: 37%" i18n="1w">1w</th>
<th scope="col" class="block-count-title" style="width: 37%">24h</th>
<th scope="col" class="block-count-title" style="width: 37%">1w</th>
<th scope="col" class="block-count-title" style="width: 26%" i18n="all">All</th>
</tr>
</thead>
@ -457,12 +445,12 @@
<!-- Mined blocks mobile -->
<tr *ngIf="isMobile()">
<td colspan=2>
<span class="label" i18n="mining.mined-blocks">Mined Blocks</span>
<span class="label" i18n="mining.mined-blocks">Mined blocks</span>
<table class="table table-xs table-data text-center">
<thead>
<tr>
<th scope="col" class="block-count-title" style="width: 33%" i18n="24h">24h</th>
<th scope="col" class="block-count-title" style="width: 37%" i18n="1w">1w</th>
<th scope="col" class="block-count-title" style="width: 33%">24h</th>
<th scope="col" class="block-count-title" style="width: 37%">1w</th>
<th scope="col" class="block-count-title" style="width: 30%" i18n="all">All</th>
</tr>
</thead>

View file

@ -3,7 +3,7 @@
<form [formGroup]="pushTxForm" (submit)="pushTxForm.valid && postTx()" novalidate>
<div class="mb-3">
<textarea formControlName="txHash" class="form-control" rows="5" i18n-placeholder="transaction.hex" placeholder="Transaction Hex"></textarea>
<textarea formControlName="txHash" class="form-control" rows="5" i18n-placeholder="transaction.hex" placeholder="Transaction hex"></textarea>
</div>
<button [disabled]="isLoading" type="submit" class="btn btn-primary mr-2" i18n="shared.broadcast-transaction|Broadcast Transaction">Broadcast Transaction</button>
<p class="red-color d-inline">{{ error }}</p> <a *ngIf="txId" [routerLink]="['/tx/' | relativeUrl, txId]">{{ txId }}</a>

View file

@ -26,7 +26,7 @@
</div>
</div>
<div class="item">
<h5 class="card-title" i18n="mining.average-fee">Average Fee</h5>
<h5 class="card-title" i18n="mining.average-fee">Reward Per Tx</h5>
<div class="card-text" i18n-ngbTooltip="mining.average-fee"
ngbTooltip="Fee paid on average for each transaction in the past 144 blocks" placement="bottom">
<div class="fee-text">{{ rewardStats.feePerTx | amountShortener: 2 }}
@ -57,7 +57,7 @@
</div>
</div>
<div class="item">
<h5 class="card-title" i18n="mining.average-fee">Average Fee</h5>
<h5 class="card-title" i18n="mining.average-fee">Reward Per Tx</h5>
<div class="card-text">
<div class="skeleton-loader"></div>
<div class="skeleton-loader"></div>

View file

@ -46,7 +46,7 @@
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td i18n="transaction.timestamp|Transaction Timestamp">Timestamp</td>
<td i18n="block.timestamp">Timestamp</td>
<td>
&lrm;{{ tx.status.block_time * 1000 | date:'yyyy-MM-dd HH:mm' }}
<div class="lg-inline">
@ -209,7 +209,7 @@
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td i18n="transaction.size|Transaction Size">Size</td>
<td i18n="block.size">Size</td>
<td [innerHTML]="'&lrm;' + (tx.size | bytes: 2)"></td>
</tr>
<tr>
@ -217,7 +217,7 @@
<td [innerHTML]="'&lrm;' + (tx.weight / 4 | vbytes: 2)"></td>
</tr>
<tr>
<td i18n="transaction.weight|Transaction Weight">Weight</td>
<td i18n="block.weight">Weight</td>
<td [innerHTML]="'&lrm;' + (tx.weight | wuBytes: 2)"></td>
</tr>
</tbody>
@ -235,7 +235,7 @@
<td [innerHTML]="'&lrm;' + (tx.locktime | number)"></td>
</tr>
<tr>
<td i18n="transaction.hex">Transaction Hex</td>
<td i18n="transaction.hex">Transaction hex</td>
<td><a target="_blank" href="{{ network === '' ? '' : '/' + network }}/api/tx/{{ txId }}/hex"><fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true"></fa-icon></a></td>
</tr>
</tbody>
@ -383,7 +383,7 @@
<tbody>
<tr>
<td class="td-width" i18n="transaction.fee|Transaction fee">Fee</td>
<td>{{ tx.fee | number }} <span class="symbol" i18n="transaction.fee.sat|Transaction Fee sat">sat</span> <span class="fiat"><app-fiat [value]="tx.fee"></app-fiat></span></td>
<td>{{ tx.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span> <span class="fiat"><app-fiat [value]="tx.fee"></app-fiat></span></td>
</tr>
<tr>
<td i18n="transaction.fee-rate|Transaction fee rate">Fee rate</td>

View file

@ -65,7 +65,10 @@
<ng-template #defaultAddress>
<a [routerLink]="['/address/' | relativeUrl, vin.prevout.scriptpubkey_address]" title="{{ vin.prevout.scriptpubkey_address }}">
<span class="d-block d-lg-none">{{ vin.prevout.scriptpubkey_address | shortenString : 16 }}</span>
<span class="d-none d-lg-block">{{ vin.prevout.scriptpubkey_address | shortenString : 35 }}</span>
<span class="d-none d-lg-flex justify-content-start">
<span class="addr-left flex-grow-1" [style]="vin.prevout.scriptpubkey_address.length > 40 ? 'max-width: 235px' : ''">{{ vin.prevout.scriptpubkey_address }}</span>
<span *ngIf="vin.prevout.scriptpubkey_address.length > 40" class="addr-right">{{ vin.prevout.scriptpubkey_address | capAddress: 40: 10 }}</span>
</span>
</a>
<div>
<app-address-labels [vin]="vin"></app-address-labels>
@ -138,7 +141,7 @@
</ng-template>
<tr *ngIf="tx.vin.length > 12 && tx['@vinLimit']">
<td colspan="3" class="text-center">
<button class="btn btn-sm btn-primary mt-2" (click)="tx['@vinLimit'] = false;"><span i18n="transactions-list.load-all">Load all</span> ({{ tx.vin.length - 10 }})</button>
<button class="btn btn-sm btn-primary mt-2" (click)="tx['@vinLimit'] = false;"><span i18n="show-all">Show all</span> ({{ tx.vin.length - 10 }})</button>
</td>
</tr>
</tbody>
@ -156,7 +159,10 @@
<td>
<a *ngIf="vout.scriptpubkey_address; else scriptpubkey_type" [routerLink]="['/address/' | relativeUrl, vout.scriptpubkey_address]" title="{{ vout.scriptpubkey_address }}">
<span class="d-block d-lg-none">{{ vout.scriptpubkey_address | shortenString : 16 }}</span>
<span class="d-none d-lg-block">{{ vout.scriptpubkey_address | shortenString : 35 }}</span>
<span class="d-none d-lg-flex justify-content-start">
<span class="addr-left flex-grow-1" [style]="vout.scriptpubkey_address.length > 40 ? 'max-width: 235px' : ''">{{ vout.scriptpubkey_address }}</span>
<span *ngIf="vout.scriptpubkey_address.length > 40" class="addr-right">{{ vout.scriptpubkey_address | capAddress: 40: 10 }}</span>
</span>
</a>
<div>
<app-address-labels [vout]="vout"></app-address-labels>
@ -232,7 +238,7 @@
<td style="text-align: left;">{{ vout.scriptpubkey }}</td>
</tr>
<tr *ngIf="vout.scriptpubkey_type == 'op_return'">
<td>OP_RETURN <span i18n="transactions-list.vout.scriptpubkey-type.data">data</span></td>
<td>OP_RETURN <span>data</span></td>
<td style="text-align: left;">{{ vout.scriptpubkey_asm | hex2ascii }}</td>
</tr>
<tr *ngIf="vout.scriptpubkey_type">
@ -246,7 +252,7 @@
</ng-template>
<tr *ngIf="tx.vout.length > 12 && tx['@voutLimit'] && !outputIndex">
<td colspan="3" class="text-center">
<button class="btn btn-sm btn-primary mt-2" (click)="tx['@voutLimit'] = false;"><span i18n="transactions-list.load-all">Load all</span> ({{ tx.vout.length - 10 }})</button>
<button class="btn btn-sm btn-primary mt-2" (click)="tx['@voutLimit'] = false;"><span i18n="show-all">Show all</span> ({{ tx.vout.length - 10 }})</button>
</td>
</tr>
</tbody>

View file

@ -129,3 +129,14 @@ h2 {
.summary {
margin-top: 10px;
}
.addr-left {
font-family: monospace;
overflow: hidden;
text-overflow: ellipsis;
margin-right: -7px
}
.addr-right {
font-family: monospace;
}

View file

@ -22,6 +22,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
@Input() showConfirmations = false;
@Input() transactionPage = false;
@Input() errorUnblinded = false;
@Input() paginated = false;
@Input() outputIndex: number;
@Input() address: string = '';
@ -84,6 +85,9 @@ export class TransactionsListComponent implements OnInit, OnChanges {
if (!this.transactions || !this.transactions.length) {
return;
}
if (this.paginated) {
this.outspends = [];
}
if (this.outputIndex) {
setTimeout(() => {
const assetBoxElements = document.getElementsByClassName('assetBox');

View file

@ -121,7 +121,7 @@
<td *ngIf="!stateService.env.MINING_DASHBOARD" class="table-cell-mined" ><app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since></td>
<td *ngIf="stateService.env.MINING_DASHBOARD" class="table-cell-mined pl-lg-4">
<a class="clear-link" [routerLink]="[('/mining/pool/' + block.extras.pool.slug) | relativeUrl]">
<img width="20" height="20" src="{{ block.extras.pool['logo'] }}"
<img width="22" height="22" src="{{ block.extras.pool['logo'] }}"
onError="this.src = './resources/mining-pools/default.svg'">
<span class="pool-name">{{ block.extras.pool.name }}</span>
</a>
@ -136,7 +136,7 @@
</tr>
</tbody>
</table>
<div class=""><a href="" [routerLink]="[(stateService.env.MINING_DASHBOARD ? '/mining/blocks' : '/blocks') | relativeUrl]" i18n="dashboard.view-all">View all &raquo;</a></div>
<div class=""><a href="" [routerLink]="[(stateService.env.MINING_DASHBOARD ? '/mining/blocks' : '/blocks') | relativeUrl]" i18n="dashboard.view-more">View more &raquo;</a></div>
</div>
</div>
</div>

View file

@ -134,6 +134,8 @@
table-layout:fixed;
tr, td, th {
border: 0px;
padding-top: 0.71rem !important;
padding-bottom: 0.75rem !important;
}
td {
overflow:hidden;
@ -182,16 +184,15 @@
text-align: left;
tr, td, th {
border: 0px;
padding-top: 0.65rem !important;
padding-bottom: 0.7rem !important;
}
.table-cell-height {
width: 15%;
}
.table-cell-mined {
width: 35%;
text-align: right;
@media (min-width: 376px) {
text-align: left;
}
text-align: left;
}
.table-cell-transaction-count {
display: none;

View file

@ -5990,6 +5990,14 @@ export const faqData = [
title: "What are mining pools?",
answer: "Mining pools are groups of miners that combine their computational power in order to increase the probability of finding new blocks."
},
{
type: "endpoint",
category: "basics",
showConditions: bitcoinNetworks,
fragment: "what-is-full-mempool",
title: "What does it mean for the mempool to be \"full\"?",
answer: "<p>When a Bitcoin transaction is made, it is stored in a Bitcoin node's mempool before it is confirmed into a block. When the rate of incoming transactions exceeds the rate transactions are confirmed, the mempool grows in size.</p><p>The default maximum size of a Bitcoin node's mempool is 300MB, so when there are 300MB of transactions in the mempool, we say it's \"full\".</p>"
},
{
type: "category",
category: "help",

View file

@ -39,7 +39,7 @@ export class ApiDocsComponent implements OnInit {
}
window.addEventListener('scroll', function() {
that.desktopDocsNavPosition = ( window.pageYOffset > 182 ) ? "fixed" : "relative";
});
}, { passive: true} );
}, 1 );
}

View file

@ -78,23 +78,23 @@ export class ApiService {
amount: amount,
orderId: orderId,
};
return this.httpClient.post<any>(this.apiBaseUrl + this.apiBasePath + '/api/v1/donations', params);
return this.httpClient.post<any>(this.apiBaseUrl + '/api/v1/donations', params);
}
getDonation$(): Observable<any[]> {
return this.httpClient.get<any[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/donations');
return this.httpClient.get<any[]>(this.apiBaseUrl + '/api/v1/donations');
}
getTranslators$(): Observable<ITranslators> {
return this.httpClient.get<ITranslators>(this.apiBaseUrl + this.apiBasePath + '/api/v1/translators');
return this.httpClient.get<ITranslators>(this.apiBaseUrl + '/api/v1/translators');
}
getContributor$(): Observable<any[]> {
return this.httpClient.get<any[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/contributors');
return this.httpClient.get<any[]>(this.apiBaseUrl + '/api/v1/contributors');
}
checkDonation$(orderId: string): Observable<any[]> {
return this.httpClient.get<any[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/donations/check?order_id=' + orderId);
return this.httpClient.get<any[]>(this.apiBaseUrl + '/api/v1/donations/check?order_id=' + orderId);
}
getInitData$(): Observable<WebsocketResponse> {
@ -154,6 +154,10 @@ export class ApiService {
);
}
getBlock$(hash: string): Observable<BlockExtended> {
return this.httpClient.get<BlockExtended>(this.apiBaseUrl + this.apiBasePath + '/api/v1/block/' + hash);
}
getHistoricalHashrate$(interval: string | undefined): Observable<any> {
return this.httpClient.get<any[]>(
this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/hashrate` +

View file

@ -14,7 +14,6 @@ export class AssetsService {
getAssetsJson$: Observable<{ array: AssetExtended[]; objects: any}>;
getAssetsMinimalJson$: Observable<any>;
getMiningPools$: Observable<any>;
constructor(
private httpClient: HttpClient,
@ -66,6 +65,5 @@ export class AssetsService {
}),
shareReplay(1),
);
this.getMiningPools$ = this.httpClient.get(apiBaseUrl + '/resources/pools.json').pipe(shareReplay(1));
}
}

View file

@ -9,9 +9,9 @@ import { take } from 'rxjs/operators';
import { TransferState, makeStateKey } from '@angular/platform-browser';
import { BlockExtended } from '../interfaces/node-api.interface';
const OFFLINE_RETRY_AFTER_MS = 10000;
const OFFLINE_PING_CHECK_AFTER_MS = 30000;
const EXPECT_PING_RESPONSE_AFTER_MS = 4000;
const OFFLINE_RETRY_AFTER_MS = 1000;
const OFFLINE_PING_CHECK_AFTER_MS = 10000;
const EXPECT_PING_RESPONSE_AFTER_MS = 5000;
const initData = makeStateKey('/api/v1/init-data');

View file

@ -0,0 +1,12 @@
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({ name: 'capAddress' })
export class CapAddressPipe implements PipeTransform {
transform(str: string, cap: number, leftover: number) {
if (!str) { return; }
if (str.length <= cap) {
return str;
}
return str.slice(-Math.max(cap - str.length, leftover));
}
}

View file

@ -29,6 +29,7 @@ import { MempoolBlocksComponent } from '../components/mempool-blocks/mempool-blo
import { BlockchainBlocksComponent } from '../components/blockchain-blocks/blockchain-blocks.component';
import { AmountComponent } from '../components/amount/amount.component';
import { RouterModule } from '@angular/router';
import { CapAddressPipe } from './pipes/cap-address-pipe/cap-address-pipe';
@NgModule({
declarations: [
@ -51,6 +52,7 @@ import { RouterModule } from '@angular/router';
WuBytesPipe,
CeilPipe,
ShortenStringPipe,
CapAddressPipe,
Decimal2HexPipe,
FeeRoundingPipe,
ColoredPriceDirective,
@ -103,6 +105,7 @@ import { RouterModule } from '@angular/router';
WuBytesPipe,
CeilPipe,
ShortenStringPipe,
CapAddressPipe,
Decimal2HexPipe,
FeeRoundingPipe,
ColoredPriceDirective,

File diff suppressed because it is too large Load diff

View file

@ -44,25 +44,6 @@
try_files $uri $uri/ /en-US/index.html =404;
}
# mainnet API
location /api/v1/donations {
proxy_pass https://mempool.space;
}
location /api/v1/donations/images {
proxy_pass https://mempool.space;
}
location /api/v1/contributors {
proxy_pass https://mempool.space;
}
location /api/v1/contributors/images {
proxy_pass https://mempool.space;
}
location /api/v1/translators {
proxy_pass https://mempool.space;
}
location /api/v1/translators/images {
proxy_pass https://mempool.space;
}
location /api/v1/ws {
proxy_pass http://127.0.0.1:8999/;
proxy_http_version 1.1;

View file

@ -82,11 +82,11 @@ pkg install -y zsh sudo git screen curl wget neovim rsync nginx openssl openssh-
### Node.js + npm
Build Node.js v16.10 and npm v7 from source using `nvm`:
Build Node.js v16.15 and npm v8 from source using `nvm`:
```
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | zsh
source $HOME/.zshrc
nvm install v16.10.0
nvm install v16.15.0
nvm alias default node
```

View file

@ -276,7 +276,7 @@ MINFEE_HOME=/minfee
MEMPOOL_REPO_URL=https://github.com/mempool/mempool
MEMPOOL_REPO_NAME=mempool
MEMPOOL_REPO_BRANCH=master
MEMPOOL_LATEST_RELEASE=v2.3.1
MEMPOOL_LATEST_RELEASE=master
BITCOIN_REPO_URL=https://github.com/bitcoin/bitcoin
BITCOIN_REPO_NAME=bitcoin
@ -330,7 +330,7 @@ DEBIAN_PKG+=(nodejs npm mariadb-server nginx-core python-certbot-nginx rsync ufw
# packages needed for mempool ecosystem
FREEBSD_PKG=()
FREEBSD_PKG+=(zsh sudo git screen curl wget calc neovim)
FREEBSD_PKG+=(openssh-portable py38-pip rust llvm90 jq)
FREEBSD_PKG+=(openssh-portable py38-pip rust llvm90 jq base64)
FREEBSD_PKG+=(boost-libs autoconf automake gmake gcc libevent libtool pkgconf)
FREEBSD_PKG+=(nginx rsync py38-certbot-nginx mariadb105-server keybase)
@ -854,7 +854,7 @@ echo "[*] Installing nvm.sh from GitHub"
osSudo "${MEMPOOL_USER}" sh -c 'curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | zsh'
echo "[*] Building NodeJS via nvm.sh"
osSudo "${MEMPOOL_USER}" zsh -c 'source ~/.zshrc ; nvm install v16.10.0'
osSudo "${MEMPOOL_USER}" zsh -c 'source ~/.zshrc ; nvm install v16.15.0'
####################
# Tor installation #
@ -1016,7 +1016,7 @@ osSudo "${BITCOIN_USER}" sh -c "cd ${BITCOIN_ELECTRS_HOME} && cargo run --releas
echo "[*] Patching Bitcoin Electrs code for FreeBSD"
osSudo "${BITCOIN_USER}" sh -c "cd \"${BITCOIN_HOME}/.cargo/registry/src/github.com-1ecc6299db9ec823/sysconf-0.3.4\" && patch -p1 < \"${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/freebsd/sysconf.patch\""
osSudo "${BITCOIN_USER}" sh -c "cd \"${BITCOIN_ELECTRS_HOME}/src/new_index/\" && sed -i .bak -e s/Snappy/None/ db.rs && rm db.rs.bak"
osSudo "${BITCOIN_USER}" sh -c "cd \"${BITCOIN_ELECTRS_HOME}/src/bin/\" && sed -i .bak -e s/from_secs(5)/from_secs(1)/ electrs.rs && rm electrs.rs.bak"
osSudo "${BITCOIN_USER}" sh -c "cd \"${BITCOIN_ELECTRS_HOME}/src/bin/\" && sed -i .bak -e 's/from_secs(5)/from_secs(1)/' electrs.rs && rm electrs.rs.bak"
echo "[*] Building Bitcoin Electrs release binary"
osSudo "${BITCOIN_USER}" sh -c "cd ${BITCOIN_ELECTRS_HOME} && cargo run --release --bin electrs -- --version"
@ -1299,7 +1299,7 @@ if [ "${ELEMENTS_LIQUIDTESTNET_ENABLE}" = ON ];then
osSudo "${MEMPOOL_USER}" sh -c "cd ${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME} && git checkout ${MEMPOOL_LATEST_RELEASE}"
fi
if [ "${BISQ_ENABLE}" = ON ];then
if [ "${BISQ_INSTALL}" = ON ];then
echo "[*] Creating Mempool instance for Bisq"
osSudo "${MEMPOOL_USER}" git config --global advice.detachedHead false
osSudo "${MEMPOOL_USER}" git clone --branch "${MEMPOOL_REPO_BRANCH}" "${MEMPOOL_REPO_URL}" "${MEMPOOL_HOME}/bisq"

View file

@ -1,4 +1,4 @@
local7.>=notice |/usr/local/bin/sudo -u mempool /usr/local/bin/mempool-logger mempool.ops alerts
local7.info |/usr/local/bin/sudo -u mempool /usr/local/bin/mempool-logger mempool.ops node100
local7.info /var/log/mempool
local7.>=info |/usr/local/bin/sudo -u mempool /usr/local/bin/mempool-logger mempool.ops node100
local7.>=info /var/log/mempool
local7.* /var/log/mempool.debug