Merge branch 'master' into simon/i18n-2.4

This commit is contained in:
wiz 2022-05-17 23:04:38 +09:00 committed by GitHub
commit 7cf01d6e34
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
41 changed files with 213 additions and 217 deletions

View file

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

2
.nvmrc
View file

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

View file

@ -134,7 +134,7 @@ class Blocks {
blockExtended.extras.avgFeeRate = stats.avgfeerate; blockExtended.extras.avgFeeRate = stats.avgfeerate;
} }
if (Common.indexingEnabled()) { if (['mainnet', 'testnet', 'signet', 'regtest'].includes(config.MEMPOOL.NETWORK)) {
let pool: PoolTag; let pool: PoolTag;
if (blockExtended.extras?.coinbaseTx !== undefined) { if (blockExtended.extras?.coinbaseTx !== undefined) {
pool = await this.$findBlockMiner(blockExtended.extras?.coinbaseTx); pool = await this.$findBlockMiner(blockExtended.extras?.coinbaseTx);
@ -143,7 +143,8 @@ class Blocks {
} }
if (!pool) { // We should never have this situation in practise 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; return blockExtended;
} }
@ -389,6 +390,42 @@ class Blocks {
return prepareBlock(blockExtended); 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[]> { 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 // 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. // 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; default: return null;
} }
} }
static indexingEnabled(): boolean { static indexingEnabled(): boolean {
return ( return (
['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) && ['mainnet', 'testnet', 'signet', 'regtest'].includes(config.MEMPOOL.NETWORK) &&
config.DATABASE.ENABLED === true && config.DATABASE.ENABLED === true &&
config.MEMPOOL.INDEXING_BLOCKS_AMOUNT != 0 config.MEMPOOL.INDEXING_BLOCKS_AMOUNT !== 0
); );
} }
} }

View file

@ -43,7 +43,7 @@ class FiatConversion {
agentOptions: { agentOptions: {
keepAlive: true, keepAlive: true,
}, },
host: config.SOCKS5PROXY.HOST, hostname: config.SOCKS5PROXY.HOST,
port: config.SOCKS5PROXY.PORT port: config.SOCKS5PROXY.PORT
}; };

View file

@ -350,7 +350,8 @@ class Server {
this.app this.app
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks-extras', routes.getBlocksExtras) .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') { if (config.MEMPOOL.BACKEND !== 'esplora') {
this.app this.app
@ -362,7 +363,6 @@ class Server {
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/hex', routes.getRawTransaction) .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/status', routes.getTransactionStatus)
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/outspends', routes.getTransactionOutspends) .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 + 'block/:hash/header', routes.getBlockHeader)
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks', routes.getBlocks) .get(config.MEMPOOL.API_URL_PREFIX + 'blocks', routes.getBlocks)
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/:height', routes.getBlocks) .get(config.MEMPOOL.API_URL_PREFIX + 'blocks/:height', routes.getBlocks)

View file

@ -287,6 +287,7 @@ class BlocksRepository {
return null; return null;
} }
rows[0].fee_span = JSON.parse(rows[0].fee_span);
return rows[0]; return rows[0];
} catch (e) { } catch (e) {
logger.err(`Cannot get indexed block ${height}. Reason: ` + (e instanceof Error ? e.message : 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 * Return blocks difficulty
*/ */
@ -397,18 +426,28 @@ class BlocksRepository {
const [blocks]: any[] = await DB.query(`SELECT height, hash, previous_block_hash, const [blocks]: any[] = await DB.query(`SELECT height, hash, previous_block_hash,
UNIX_TIMESTAMP(blockTimestamp) as timestamp FROM blocks ORDER BY height`); UNIX_TIMESTAMP(blockTimestamp) as timestamp FROM blocks ORDER BY height`);
let currentHeight = 1; let partialMsg = false;
while (currentHeight < blocks.length) { let idx = 1;
if (blocks[currentHeight].previous_block_hash !== blocks[currentHeight - 1].hash) { while (idx < blocks.length) {
logger.warn(`Chain divergence detected at block ${blocks[currentHeight - 1].height}, re-indexing newer blocks and hashrates`); if (blocks[idx].height - 1 !== blocks[idx - 1].height) {
await this.$deleteBlocksFrom(blocks[currentHeight - 1].height); if (partialMsg === false) {
await HashratesRepository.$deleteHashratesFromTimestamp(blocks[currentHeight - 1].timestamp - 604800); 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; 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; return true;
} catch (e) { } catch (e) {
logger.err('Cannot validate chain of block hash. Reason: ' + (e instanceof Error ? e.message : 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 * 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 { try {
let query = `SELECT let query = `SELECT
CAST(AVG(height) as INT) as avg_height, CAST(AVG(height) as INT) as avg_height,

View file

@ -702,8 +702,8 @@ class Routes {
public async getBlock(req: Request, res: Response) { public async getBlock(req: Request, res: Response) {
try { try {
const result = await bitcoinApi.$getBlock(req.params.hash); const block = await blocks.$getBlock(req.params.hash);
res.json(result); res.json(block);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); res.status(500).send(e instanceof Error ? e.message : e);
} }
@ -727,7 +727,7 @@ class Routes {
res.status(500).send(e instanceof Error ? e.message : e); res.status(500).send(e instanceof Error ? e.message : e);
} }
} }
public async getBlocks(req: Request, res: Response) { public async getBlocks(req: Request, res: Response) {
try { try {
loadingIndicators.setProgress('blocks', 0); loadingIndicators.setProgress('blocks', 0);

View file

@ -29,7 +29,7 @@ class SyncAssets {
agentOptions: { agentOptions: {
keepAlive: true, keepAlive: true,
}, },
host: config.SOCKS5PROXY.HOST, hostname: config.SOCKS5PROXY.HOST,
port: config.SOCKS5PROXY.PORT port: config.SOCKS5PROXY.PORT
}; };

View file

@ -1,4 +1,4 @@
import { BlockExtended } from "../mempool.interfaces"; import { BlockExtended } from '../mempool.interfaces';
export function prepareBlock(block: any): BlockExtended { export function prepareBlock(block: any): BlockExtended {
return <BlockExtended>{ return <BlockExtended>{
@ -17,9 +17,11 @@ export function prepareBlock(block: any): BlockExtended {
extras: { extras: {
coinbaseRaw: block.coinbase_raw ?? block.extras.coinbaseRaw, coinbaseRaw: block.coinbase_raw ?? block.extras.coinbaseRaw,
medianFee: block.medianFee ?? block.median_fee ?? block.extras?.medianFee, 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, 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 ? { pool: block?.extras?.pool ?? (block?.pool_id ? {
id: block.pool_id, id: block.pool_id,
name: block.pool_name, name: block.pool_name,

View file

@ -155,7 +155,7 @@ Corresponding `docker-compose.yml` overrides:
environment: environment:
ELECTRUM_HOST: "" ELECTRUM_HOST: ""
ELECTRUM_PORT: "" ELECTRUM_PORT: ""
ELECTRUM_TLS: "" ELECTRUM_TLS_ENABLED: ""
... ...
``` ```

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 ARG commitHash
ENV DOCKER_COMMIT_HASH=${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 install
RUN npm run build RUN npm run build
FROM node:16.10.0-buster-slim FROM node:16.15.0-buster-slim
WORKDIR /backend WORKDIR /backend

View file

@ -28,7 +28,7 @@ __CORE_RPC_PASSWORD__=${CORE_RPC_PASSWORD:=mempool}
# ELECTRUM # ELECTRUM
__ELECTRUM_HOST__=${ELECTRUM_HOST:=127.0.0.1} __ELECTRUM_HOST__=${ELECTRUM_HOST:=127.0.0.1}
__ELECTRUM_PORT__=${ELECTRUM_PORT:=50002} __ELECTRUM_PORT__=${ELECTRUM_PORT:=50002}
__ELECTRUM_TLS_ENABLED__=${ELECTRUM_TLS:=false} __ELECTRUM_TLS_ENABLED__=${ELECTRUM_TLS_ENABLED:=false}
# ESPLORA # ESPLORA
__ESPLORA_REST_API_URL__=${ESPLORA_REST_API_URL:=http://127.0.0.1:3000} __ESPLORA_REST_API_URL__=${ESPLORA_REST_API_URL:=http://127.0.0.1:3000}

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 ARG commitHash
ENV DOCKER_COMMIT_HASH=${commitHash} ENV DOCKER_COMMIT_HASH=${commitHash}

View file

@ -40,7 +40,6 @@ import { AssetComponent } from './components/asset/asset.component';
import { AssetsComponent } from './components/assets/assets.component'; import { AssetsComponent } from './components/assets/assets.component';
import { AssetsNavComponent } from './components/assets/assets-nav/assets-nav.component'; import { AssetsNavComponent } from './components/assets/assets-nav/assets-nav.component';
import { StatusViewComponent } from './components/status-view/status-view.component'; import { StatusViewComponent } from './components/status-view/status-view.component';
import { MinerComponent } from './components/miner/miner.component';
import { SharedModule } from './shared/shared.module'; import { SharedModule } from './shared/shared.module';
import { NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstrap'; import { NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstrap';
import { FeesBoxComponent } from './components/fees-box/fees-box.component'; import { FeesBoxComponent } from './components/fees-box/fees-box.component';
@ -108,7 +107,6 @@ import { BlockSizesWeightsGraphComponent } from './components/block-sizes-weight
LbtcPegsGraphComponent, LbtcPegsGraphComponent,
AssetComponent, AssetComponent,
AssetsComponent, AssetsComponent,
MinerComponent,
StatusViewComponent, StatusViewComponent,
FeesBoxComponent, FeesBoxComponent,
DashboardComponent, DashboardComponent,

View file

@ -81,15 +81,26 @@
<ng-template [ngIf]="fees !== undefined" [ngIfElse]="loadingFees"> <ng-template [ngIf]="fees !== undefined" [ngIfElse]="loadingFees">
<tr> <tr>
<td i18n="block.total-fees|Total fees in a block">Total fees</td> <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> <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> </ng-template>
</tr> </tr>
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'"> <tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
<td i18n="block.subsidy-and-fees|Total subsidy and fees in a block">Subsidy + fees:</td> <td i18n="block.subsidy-and-fees|Total subsidy and fees in a block">Subsidy + fees:</td>
<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> </td>
</tr> </tr>
</ng-template> </ng-template>
@ -105,7 +116,18 @@
</ng-template> </ng-template>
<tr> <tr>
<td i18n="block.miner">Miner</td> <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> </tr>
</tbody> </tbody>
</table> </table>

View file

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

View file

@ -8,7 +8,7 @@
<div style="min-height: 295px"> <div style="min-height: 295px">
<table class="table table-borderless"> <table class="table table-borderless">
<thead> <thead>
<th class="height" [class]="widget ? 'widget' : ''" i18n="latest-blocks.height">Height</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="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="timestamp" i18n="latest-blocks.timestamp" *ngIf="!widget">Timestamp</th>
<th class="mined" i18n="latest-blocks.mined" *ngIf="!widget">Mined</th> <th class="mined" i18n="latest-blocks.mined" *ngIf="!widget">Mined</th>
@ -19,7 +19,7 @@
</thead> </thead>
<tbody *ngIf="blocks$ | async as blocks; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''"> <tbody *ngIf="blocks$ | async as blocks; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''">
<tr *ngFor="let block of blocks; let i= index; trackBy: trackByBlock"> <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 [routerLink]="['/block' | relativeUrl, block.height]">{{ block.height
}}</a> }}</a>
</td> </td>
@ -27,7 +27,7 @@
<div class="tooltip-custom"> <div class="tooltip-custom">
<a class="clear-link" [routerLink]="['/mining/pool' | relativeUrl, block.extras.pool.slug]"> <a class="clear-link" [routerLink]="['/mining/pool' | relativeUrl, block.extras.pool.slug]">
<img width="22" height="22" src="{{ block.extras.pool['logo'] }}" <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> <span class="pool-name">{{ block.extras.pool.name }}</span>
</a> </a>
<span *ngIf="!widget" class="tooltiptext badge badge-secondary scriptmessage">{{ block.extras.coinbaseRaw | hex2ascii }}</span> <span *ngIf="!widget" class="tooltiptext badge badge-secondary scriptmessage">{{ block.extras.coinbaseRaw | hex2ascii }}</span>
@ -60,7 +60,7 @@
<ng-template #skeleton> <ng-template #skeleton>
<tbody> <tbody>
<tr *ngFor="let item of skeletonLines"> <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> <span class="skeleton-loader" style="max-width: 75px"></span>
</td> </td>
<td class="pool text-left" [class]="widget ? 'widget' : ''"> <td class="pool text-left" [class]="widget ? 'widget' : ''">

View file

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

View file

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

View file

@ -22,7 +22,7 @@
</tr> </tr>
</tbody> </tbody>
<tbody *ngIf="isLoading"> <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="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-left"><span class="skeleton-loader w-75"></span></td>
<td class="text-right"><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%; width: 100%;
text-align: left; text-align: left;
table-layout:fixed; table-layout:fixed;
tr, td, th { tr, th {
border: 0px; border: 0px;
padding-top: 0.65rem !important;
padding-bottom: 0.7rem !important;
} }
td { td {
border: 0px;
padding-top: 0.71rem !important;
padding-bottom: 0.75rem !important;
width: 25%; width: 25%;
@media (max-width: 376px) { @media (max-width: 376px) {
padding: 0.85rem; padding: 0.85rem;

View file

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

View file

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

View file

@ -3,7 +3,7 @@
<nav class="navbar navbar-expand-md navbar-dark bg-dark"> <nav class="navbar navbar-expand-md navbar-dark bg-dark">
<a class="navbar-brand" [routerLink]="['/' | relativeUrl]" style="position: relative;"> <a class="navbar-brand" [routerLink]="['/' | relativeUrl]" style="position: relative;">
<ng-container *ngIf="{ val: connectionState$ | async } as connectionState"> <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="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 === 0" i18n="master-page.offline">Offline</div>
<div class="badge badge-warning" *ngIf="connectionState.val === 1" i18n="master-page.reconnecting">Reconnecting...</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"> <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"> <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> </button>
<div ngbDropdownMenu [ngClass]="{'dropdown-menu-right' : isMobile}"> <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 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"> Signet</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"> Testnet</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> <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.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"> Liquid</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"> Liquid Testnet</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>
</div> </div>

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> <span style="font-size: xx-small" i18n="mining.144-blocks">(144 blocks)</span>
</div> </div>
<div class="card-wrapper"> <div class="card-wrapper">
<div class="card" style="height: 123px"> <div class="card">
<div class="card-body more-padding"> <div class="card-body more-padding">
<app-reward-stats></app-reward-stats> <app-reward-stats></app-reward-stats>
</div> </div>
@ -22,15 +22,13 @@
<!-- difficulty adjustment --> <!-- difficulty adjustment -->
<div class="col"> <div class="col">
<div class="main-title" i18n="dashboard.difficulty-adjustment">Difficulty Adjustment</div> <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>
<app-difficulty [showTitle]="false" [showProgress]="false" [showHalving]="true"></app-difficulty>
</div>
</div> </div>
<!-- pool distribution --> <!-- pool distribution -->
<div class="col"> <div class="col" style="margin-bottom: 1.47rem">
<div class="card"> <div class="card graph-card">
<div class="card-body"> <div class="card-body pl-2 pr-2">
<app-pool-ranking [widget]=true></app-pool-ranking> <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>
@ -38,9 +36,9 @@
</div> </div>
<!-- hashrate --> <!-- hashrate -->
<div class="col"> <div class="col" style="margin-bottom: 1.47rem">
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body pl-lg-3 pr-lg-3 pl-2 pr-2">
<app-hashrate-chart [widget]="true"></app-hashrate-chart> <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="mt-1"><a [routerLink]="['/graphs/mining/hashrate-difficulty' | relativeUrl]" i18n="dashboard.view-more">View more &raquo;</a></div>
</div> </div>

View file

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

View file

@ -92,8 +92,9 @@
<tbody *ngIf="(miningStatsObservable$ | async) as miningStats"> <tbody *ngIf="(miningStatsObservable$ | async) as miningStats">
<tr *ngFor="let pool of miningStats.pools"> <tr *ngFor="let pool of miningStats.pools">
<td class="d-none d-md-block">{{ pool.rank }}</td> <td class="d-none d-md-block">{{ pool.rank }}</td>
<td class="text-right"><img width="25" height="25" src="{{ pool.logo }}" <td class="text-right">
onError="this.src = './resources/mining-pools/default.svg'"></td> <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=""><a [routerLink]="[('/mining/pool/' + pool.slug) | relativeUrl]">{{ pool.name }}</a></td>
<td class="" *ngIf="this.miningWindowPreference === '24h' && !isLoading">{{ pool.lastEstimatedHashrate }} {{ <td class="" *ngIf="this.miningWindowPreference === '24h' && !isLoading">{{ pool.lastEstimatedHashrate }} {{
miningStats.miningUnits.hashrateUnit }}</td> miningStats.miningUnits.hashrateUnit }}</td>

View file

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

View file

@ -5,7 +5,7 @@
<!-- Pool overview --> <!-- Pool overview -->
<div *ngIf="poolStats$ | async as poolStats; else loadingMain"> <div *ngIf="poolStats$ | async as poolStats; else loadingMain">
<div style="display:flex" class="mb-3"> <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"> onError="this.src = './resources/mining-pools/default.svg'" class="mr-3">
<h1 class="m-0 pt-1 pt-md-0">{{ poolStats.pool.name }}</h1> <h1 class="m-0 pt-1 pt-md-0">{{ poolStats.pool.name }}</h1>
</div> </div>

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" ><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"> <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]"> <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'"> onError="this.src = './resources/mining-pools/default.svg'">
<span class="pool-name">{{ block.extras.pool.name }}</span> <span class="pool-name">{{ block.extras.pool.name }}</span>
</a> </a>

View file

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

View file

@ -4467,6 +4467,14 @@ export const faqData = [
title: "What are mining pools?", 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." 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", type: "category",
category: "help", category: "help",

View file

@ -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> { getHistoricalHashrate$(interval: string | undefined): Observable<any> {
return this.httpClient.get<any[]>( return this.httpClient.get<any[]>(
this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/hashrate` + this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/hashrate` +

View file

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

View file

@ -82,11 +82,11 @@ pkg install -y zsh sudo git screen curl wget neovim rsync nginx openssl openssh-
### Node.js + npm ### 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 curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | zsh
source $HOME/.zshrc source $HOME/.zshrc
nvm install v16.10.0 nvm install v16.15.0
nvm alias default node nvm alias default node
``` ```

View file

@ -2,10 +2,8 @@ datadir=/minfee
server=1 server=1
txindex=0 txindex=0
listen=1 listen=1
discover=0
daemon=1 daemon=1
prune=1337 prune=1337
maxconnections=1
rpcallowip=127.0.0.1 rpcallowip=127.0.0.1
rpcuser=__BITCOIN_RPC_USER__ rpcuser=__BITCOIN_RPC_USER__
rpcpassword=__BITCOIN_RPC_PASS__ rpcpassword=__BITCOIN_RPC_PASS__
@ -15,4 +13,4 @@ rpcpassword=__BITCOIN_RPC_PASS__
bind=127.0.0.1:8303 bind=127.0.0.1:8303
rpcbind=127.0.0.1:8302 rpcbind=127.0.0.1:8302
rpcport=8302 rpcport=8302
connect=127.0.0.1:8333 addnode=127.0.0.1:8333

View file

@ -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' 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" 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 # # Tor installation #