Merge branch 'master' into recvd-htlcs

This commit is contained in:
wiz 2022-05-10 23:11:28 +09:00 committed by GitHub
commit b17f882c64
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
44 changed files with 478 additions and 117 deletions

View File

@ -41,10 +41,10 @@ You will need [Bitcoin Core](https://github.com/bitcoin/bitcoin), [Electrum Serv
Clone the Mempool repo, and checkout the latest release tag:
```bash
$ git clone https://github.com/mempool/mempool
$ cd mempool
$ latestrelease=$(curl -s https://api.github.com/repos/mempool/mempool/releases/latest|grep tag_name|head -1|cut -d '"' -f4)
$ git checkout $latestrelease
git clone https://github.com/mempool/mempool
cd mempool
latestrelease=$(curl -s https://api.github.com/repos/mempool/mempool/releases/latest|grep tag_name|head -1|cut -d '"' -f4)
git checkout $latestrelease
```
### 2. Configure Bitcoin Core
@ -63,11 +63,11 @@ Install MariaDB from your OS package manager:
```bash
# Debian, Ubuntu, etc.
$ apt-get install mariadb-server mariadb-client
apt-get install mariadb-server mariadb-client
# macOS
$ brew install mariadb
$ mysql.server start
brew install mariadb
mysql.server start
```
Create a database and grant privileges:
@ -88,15 +88,15 @@ Query OK, 0 rows affected (0.00 sec)
Install Mempool dependencies with npm and build the backend:
```bash
$ cd backend
$ npm install --prod
$ npm run build
cd backend
npm install --prod
npm run build
```
In the `backend` folder, make a copy of the sample config:
```bash
$ cp mempool-config.sample.json mempool-config.json
cp mempool-config.sample.json mempool-config.json
```
Edit `mempool-config.json` with your Bitcoin Core node RPC credentials:
@ -133,7 +133,7 @@ Edit `mempool-config.json` with your Bitcoin Core node RPC credentials:
Start the backend:
```bash
$ npm run start
npm run start
```
When it's running, you should see output like this:
@ -164,15 +164,15 @@ Updating mempool
Install the Mempool dependencies with npm and build the frontend:
```bash
$ cd frontend
$ npm install --prod
$ npm run build
cd frontend
npm install --prod
npm run build
```
Install the output into the nginx webroot folder:
```bash
$ sudo rsync -av --delete dist/ /var/www/
sudo rsync -av --delete dist/ /var/www/
```
### 6. `nginx` + `certbot`
@ -181,13 +181,13 @@ Install the supplied `nginx.conf` and `nginx-mempool.conf` in `/etc/nginx`:
```bash
# install nginx and certbot
$ apt-get install -y nginx python3-certbot-nginx
apt-get install -y nginx python3-certbot-nginx
# install the mempool configuration for nginx
$ cp nginx.conf nginx-mempool.conf /etc/nginx/
cp nginx.conf nginx-mempool.conf /etc/nginx/
# replace example.com with your domain name
$ certbot --nginx -d example.com
certbot --nginx -d example.com
```
If everything went well, you should see the beautiful mempool :grin:

View File

@ -221,9 +221,10 @@ class Blocks {
const lastBlockToIndex = Math.max(0, currentBlockHeight - indexingBlockAmount + 1);
logger.debug(`Indexing blocks from #${currentBlockHeight} to #${lastBlockToIndex}`);
loadingIndicators.setProgress('block-indexing', 0);
const chunkSize = 10000;
let totaIndexed = await blocksRepository.$blockCount(null, null);
let totalIndexed = await blocksRepository.$blockCountBetweenHeight(currentBlockHeight, lastBlockToIndex);
let indexedThisRun = 0;
let newlyIndexed = 0;
const startedAt = new Date().getTime() / 1000;
@ -246,16 +247,17 @@ class Blocks {
break;
}
++indexedThisRun;
++totaIndexed;
++totalIndexed;
const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - timer));
if (elapsedSeconds > 5 || blockHeight === lastBlockToIndex) {
const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
const blockPerSeconds = Math.max(1, Math.round(indexedThisRun / elapsedSeconds));
const progress = Math.round(totaIndexed / indexingBlockAmount * 100);
const timeLeft = Math.round((indexingBlockAmount - totaIndexed) / blockPerSeconds);
logger.debug(`Indexing block #${blockHeight} | ~${blockPerSeconds} blocks/sec | total: ${totaIndexed}/${indexingBlockAmount} (${progress}%) | elapsed: ${runningFor} seconds | left: ~${timeLeft} seconds`);
const progress = Math.round(totalIndexed / indexingBlockAmount * 10000) / 100;
const timeLeft = Math.round((indexingBlockAmount - totalIndexed) / blockPerSeconds);
logger.debug(`Indexing block #${blockHeight} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${totalIndexed}/${indexingBlockAmount} (${progress}%) | elapsed: ${runningFor} seconds | left: ~${timeLeft} seconds`);
timer = new Date().getTime() / 1000;
indexedThisRun = 0;
loadingIndicators.setProgress('block-indexing', progress, false);
}
const blockHash = await bitcoinApi.$getBlockHash(blockHeight);
const block = BitcoinApi.convertBlock(await bitcoinClient.getBlock(blockHash));
@ -269,9 +271,11 @@ class Blocks {
currentBlockHeight -= chunkSize;
}
logger.info(`Indexed ${newlyIndexed} blocks`);
loadingIndicators.setProgress('block-indexing', 100);
} catch (e) {
logger.err('Block indexing failed. Trying again later. Reason: ' + (e instanceof Error ? e.message : e));
this.blockIndexingStarted = false;
loadingIndicators.setProgress('block-indexing', 100);
return;
}

View File

@ -12,8 +12,8 @@ class LoadingIndicators {
this.progressChangedCallback = fn;
}
public setProgress(name: string, progressPercent: number) {
const newProgress = Math.round(progressPercent);
public setProgress(name: string, progressPercent: number, rounded: boolean = true) {
const newProgress = rounded === true ? Math.round(progressPercent) : progressPercent;
if (newProgress >= 100) {
delete this.loadingIndicators[name];
} else {

View File

@ -6,6 +6,7 @@ import bitcoinClient from './bitcoin/bitcoin-client';
import logger from '../logger';
import blocks from './blocks';
import { Common } from './common';
import loadingIndicators from './loading-indicators';
class Mining {
hashrateIndexingStarted = false;
@ -131,7 +132,7 @@ class Mining {
* [INDEXING] Generate weekly mining pool hashrate history
*/
public async $generatePoolHashrateHistory(): Promise<void> {
if (!blocks.blockIndexingCompleted || this.weeklyHashrateIndexingStarted) {
if (!blocks.blockIndexingCompleted || this.hashrateIndexingStarted || this.weeklyHashrateIndexingStarted) {
return;
}
@ -167,7 +168,10 @@ class Mining {
let indexedThisRun = 0;
let totalIndexed = 0;
let newlyIndexed = 0;
let startedAt = new Date().getTime();
const startedAt = new Date().getTime() / 1000;
let timer = new Date().getTime() / 1000;
loadingIndicators.setProgress('weekly-hashrate-indexing', 0);
while (toTimestamp > genesisTimestamp) {
const fromTimestamp = toTimestamp - 604800000;
@ -214,14 +218,17 @@ class Mining {
await HashratesRepository.$saveHashrates(hashrates);
hashrates.length = 0;
const elapsedSeconds = Math.max(1, Math.round((new Date().getTime()) - startedAt)) / 1000;
const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - timer));
if (elapsedSeconds > 1) {
const weeksPerSeconds = (indexedThisRun / elapsedSeconds).toFixed(2);
const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
const weeksPerSeconds = Math.max(1, Math.round(indexedThisRun / elapsedSeconds));
const progress = Math.round(totalIndexed / totalWeekIndexed * 10000) / 100;
const timeLeft = Math.round((totalWeekIndexed - totalIndexed) / weeksPerSeconds);
const formattedDate = new Date(fromTimestamp).toUTCString();
const weeksLeft = Math.round(totalWeekIndexed - totalIndexed);
logger.debug(`Getting weekly pool hashrate for ${formattedDate} | ~${weeksPerSeconds} weeks/sec | ~${weeksLeft} weeks left to index`);
startedAt = new Date().getTime();
logger.debug(`Getting weekly pool hashrate for ${formattedDate} | ~${weeksPerSeconds.toFixed(2)} weeks/sec | total: ~${totalIndexed}/${Math.round(totalWeekIndexed)} (${progress}%) | elapsed: ${runningFor} seconds | left: ~${timeLeft} seconds`);
timer = new Date().getTime() / 1000;
indexedThisRun = 0;
loadingIndicators.setProgress('weekly-hashrate-indexing', progress, false);
}
toTimestamp -= 604800000;
@ -233,7 +240,9 @@ class Mining {
if (newlyIndexed > 0) {
logger.info(`Indexed ${newlyIndexed} pools weekly hashrate`);
}
loadingIndicators.setProgress('weekly-hashrate-indexing', 100);
} catch (e) {
loadingIndicators.setProgress('weekly-hashrate-indexing', 100);
this.weeklyHashrateIndexingStarted = false;
throw e;
}
@ -273,7 +282,10 @@ class Mining {
let indexedThisRun = 0;
let totalIndexed = 0;
let newlyIndexed = 0;
let startedAt = new Date().getTime();
const startedAt = new Date().getTime() / 1000;
let timer = new Date().getTime() / 1000;
loadingIndicators.setProgress('daily-hashrate-indexing', 0);
while (toTimestamp > genesisTimestamp) {
const fromTimestamp = toTimestamp - 86400000;
@ -312,15 +324,17 @@ class Mining {
hashrates.length = 0;
}
const elapsedSeconds = Math.max(1, Math.round(new Date().getTime() - startedAt)) / 1000;
const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - timer));
if (elapsedSeconds > 1) {
const daysPerSeconds = (indexedThisRun / elapsedSeconds).toFixed(2);
const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
const daysPerSeconds = Math.max(1, Math.round(indexedThisRun / elapsedSeconds));
const progress = Math.round(totalIndexed / totalDayIndexed * 10000) / 100;
const timeLeft = Math.round((totalDayIndexed - totalIndexed) / daysPerSeconds);
const formattedDate = new Date(fromTimestamp).toUTCString();
const daysLeft = Math.round(totalDayIndexed - totalIndexed);
logger.debug(`Getting network daily hashrate for ${formattedDate} | ~${daysPerSeconds} days/sec | ` +
`~${daysLeft} days left to index`);
startedAt = new Date().getTime();
logger.debug(`Getting network daily hashrate for ${formattedDate} | ~${daysPerSeconds.toFixed(2)} days/sec | total: ~${totalIndexed}/${Math.round(totalDayIndexed)} (${progress}%) | elapsed: ${runningFor} seconds | left: ~${timeLeft} seconds`);
timer = new Date().getTime() / 1000;
indexedThisRun = 0;
loadingIndicators.setProgress('daily-hashrate-indexing', progress);
}
toTimestamp -= 86400000;
@ -346,7 +360,9 @@ class Mining {
if (newlyIndexed > 0) {
logger.info(`Indexed ${newlyIndexed} day of network hashrate`);
}
loadingIndicators.setProgress('daily-hashrate-indexing', 100);
} catch (e) {
loadingIndicators.setProgress('daily-hashrate-indexing', 100);
this.hashrateIndexingStarted = false;
throw e;
}

View File

@ -188,6 +188,24 @@ class BlocksRepository {
}
}
/**
* Get blocks count for a period
*/
public async $blockCountBetweenHeight(startHeight: number, endHeight: number): Promise<number> {
const params: any[] = [];
let query = `SELECT count(height) as blockCount
FROM blocks
WHERE height <= ${startHeight} AND height >= ${endHeight}`;
try {
const [rows] = await DB.query(query, params);
return <number>rows[0].blockCount;
} catch (e) {
logger.err(`Cannot count blocks for this pool (using offset). Reason: ` + (e instanceof Error ? e.message : e));
throw e;
}
}
/**
* Get the oldest indexed block
*/

View File

@ -34,7 +34,7 @@ The IP address in the example above refers to Docker's default gateway IP addres
Now, run:
```bash
$ docker-compose up
docker-compose up
```
Your Mempool instance should be running at http://localhost. The graphs will be populated as new transactions are detected.
@ -59,7 +59,7 @@ Of course, if your Docker host IP address is different, update accordingly.
With `bitcoind` and Electrum Server set up, run Mempool with:
```bash
$ docker-compose up
docker-compose up
```
## Further Configuration

View File

@ -48,7 +48,8 @@ import { DashboardComponent } from './dashboard/dashboard.component';
import { DifficultyComponent } from './components/difficulty/difficulty.component';
import { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontawesome';
import { faFilter, faAngleDown, faAngleUp, faAngleRight, faAngleLeft, faBolt, faChartArea, faCogs, faCubes, faHammer, faDatabase, faExchangeAlt, faInfoCircle,
faLink, faList, faSearch, faCaretUp, faCaretDown, faTachometerAlt, faThList, faTint, faTv, faAngleDoubleDown, faSortUp, faAngleDoubleUp, faChevronDown, faFileAlt, faRedoAlt, faArrowAltCircleRight, faExternalLinkAlt, faBook, faListUl } from '@fortawesome/free-solid-svg-icons';
faLink, faList, faSearch, faCaretUp, faCaretDown, faTachometerAlt, faThList, faTint, faTv, faAngleDoubleDown, faSortUp, faAngleDoubleUp, faChevronDown,
faFileAlt, faRedoAlt, faArrowAltCircleRight, faExternalLinkAlt, faBook, faListUl, faDownload } from '@fortawesome/free-solid-svg-icons';
import { TermsOfServiceComponent } from './components/terms-of-service/terms-of-service.component';
import { PrivacyPolicyComponent } from './components/privacy-policy/privacy-policy.component';
import { TrademarkPolicyComponent } from './components/trademark-policy/trademark-policy.component';
@ -75,6 +76,8 @@ import { DataCyDirective } from './data-cy.directive';
import { BlockFeesGraphComponent } from './components/block-fees-graph/block-fees-graph.component';
import { BlockRewardsGraphComponent } from './components/block-rewards-graph/block-rewards-graph.component';
import { BlockFeeRatesGraphComponent } from './components/block-fee-rates-graph/block-fee-rates-graph.component';
import { LoadingIndicatorComponent } from './components/loading-indicator/loading-indicator.component';
import { IndexingProgressComponent } from './components/indexing-progress/indexing-progress.component';
@NgModule({
declarations: [
@ -131,6 +134,8 @@ import { BlockFeeRatesGraphComponent } from './components/block-fee-rates-graph/
BlockFeesGraphComponent,
BlockRewardsGraphComponent,
BlockFeeRatesGraphComponent,
LoadingIndicatorComponent,
IndexingProgressComponent,
],
imports: [
BrowserModule.withServerTransition({ appId: 'serverApp' }),
@ -195,5 +200,6 @@ export class AppModule {
library.addIcons(faAngleLeft);
library.addIcons(faBook);
library.addIcons(faListUl);
library.addIcons(faDownload);
}
}

View File

@ -134,7 +134,7 @@
<ng-template #displayServerError><i class="small">({{ error.error }})</i></ng-template>
<ng-template [ngIf]="error.status === 413 || error.status === 405" [ngIfElse]="displayServerError">
<ng-container i18n="Electrum server limit exceeded error">
<i>The number of transactions on this address exceeds the Electrum server limit</i>
<i>There many transactions on this address, more than your backend can handle. See more on <a href="/docs/faq#address-lookup-issues">setting up a stronger backend</a>.</i>
<br><br>
Consider viewing this address on the official Mempool website instead:
</ng-container>

View File

@ -1,6 +1,12 @@
<app-indexing-progress></app-indexing-progress>
<div class="full-container">
<div class="card-header mb-0 mb-md-4">
<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

@ -59,11 +59,11 @@
margin-top: 6px;
display: flex;
flex-direction: column;
@media (min-width: 1130px) {
@media (min-width: 991px) {
position: relative;
top: -65px;
}
@media (min-width: 830px) and (max-width: 1130px) {
@media (min-width: 830px) and (max-width: 991px) {
position: relative;
top: 0px;
}

View File

@ -6,7 +6,7 @@ import { ApiService } from 'src/app/services/api.service';
import { SeoService } from 'src/app/services/seo.service';
import { formatNumber } from '@angular/common';
import { FormBuilder, FormGroup } from '@angular/forms';
import { formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from 'src/app/shared/graphs.utils';
import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from 'src/app/shared/graphs.utils';
import { StorageService } from 'src/app/services/storage.service';
import { MiningService } from 'src/app/services/mining.service';
import { selectPowerOfTen } from 'src/app/bitcoin.utils';
@ -297,4 +297,22 @@ export class BlockFeeRatesGraphComponent implements OnInit {
isMobile() {
return (window.innerWidth <= 767.98);
}
onSaveChart() {
// @ts-ignore
const prevBottom = this.chartOptions.grid.bottom;
const now = new Date();
// @ts-ignore
this.chartOptions.grid.bottom = 40;
this.chartOptions.backgroundColor = '#11131f';
this.chartInstance.setOption(this.chartOptions);
download(this.chartInstance.getDataURL({
pixelRatio: 2,
excludeComponents: ['dataZoom'],
}), `block-fee-rates-${this.timespan}-${Math.round(now.getTime() / 1000)}.svg`);
// @ts-ignore
this.chartOptions.grid.bottom = prevBottom;
this.chartOptions.backgroundColor = 'none';
this.chartInstance.setOption(this.chartOptions);
}
}

View File

@ -1,6 +1,12 @@
<app-indexing-progress></app-indexing-progress>
<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()">
<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">
@ -37,7 +43,8 @@
</form>
</div>
<div class="chart" echarts [initOpts]="chartInitOptions" [options]="chartOptions">
<div class="chart" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
(chartInit)="onChartInit($event)">
</div>
<div class="text-center loadingGraphs" *ngIf="isLoading">
<div class="spinner-border text-light"></div>

View File

@ -59,11 +59,11 @@
margin-top: 6px;
display: flex;
flex-direction: column;
@media (min-width: 1130px) {
@media (min-width: 991px) {
position: relative;
top: -65px;
}
@media (min-width: 830px) and (max-width: 1130px) {
@media (min-width: 830px) and (max-width: 991px) {
position: relative;
top: 0px;
}

View File

@ -6,7 +6,7 @@ import { ApiService } from 'src/app/services/api.service';
import { SeoService } from 'src/app/services/seo.service';
import { formatNumber } from '@angular/common';
import { FormBuilder, FormGroup } from '@angular/forms';
import { formatterXAxisLabel } from 'src/app/shared/graphs.utils';
import { download, formatterXAxisLabel } from 'src/app/shared/graphs.utils';
import { StorageService } from 'src/app/services/storage.service';
import { MiningService } from 'src/app/services/mining.service';
@ -40,6 +40,7 @@ export class BlockFeesGraphComponent implements OnInit {
isLoading = true;
formatNumber = formatNumber;
timespan = '';
chartInstance: any = undefined;
constructor(
@Inject(LOCALE_ID) public locale: string,
@ -194,7 +195,29 @@ export class BlockFeesGraphComponent implements OnInit {
};
}
onChartInit(ec) {
this.chartInstance = ec;
}
isMobile() {
return (window.innerWidth <= 767.98);
}
onSaveChart() {
// @ts-ignore
const prevBottom = this.chartOptions.grid.bottom;
const now = new Date();
// @ts-ignore
this.chartOptions.grid.bottom = 40;
this.chartOptions.backgroundColor = '#11131f';
this.chartInstance.setOption(this.chartOptions);
download(this.chartInstance.getDataURL({
pixelRatio: 2,
excludeComponents: ['dataZoom'],
}), `block-fees-${this.timespan}-${Math.round(now.getTime() / 1000)}.svg`);
// @ts-ignore
this.chartOptions.grid.bottom = prevBottom;
this.chartOptions.backgroundColor = 'none';
this.chartInstance.setOption(this.chartOptions);
}
}

View File

@ -1,7 +1,13 @@
<app-indexing-progress></app-indexing-progress>
<div class="full-container">
<div class="card-header mb-0 mb-md-4">
<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">
@ -38,7 +44,8 @@
</form>
</div>
<div class="chart" echarts [initOpts]="chartInitOptions" [options]="chartOptions">
<div class="chart" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
(chartInit)="onChartInit($event)">
</div>
<div class="text-center loadingGraphs" *ngIf="isLoading">
<div class="spinner-border text-light"></div>

View File

@ -59,11 +59,11 @@
margin-top: 6px;
display: flex;
flex-direction: column;
@media (min-width: 1130px) {
@media (min-width: 991px) {
position: relative;
top: -65px;
}
@media (min-width: 830px) and (max-width: 1130px) {
@media (min-width: 830px) and (max-width: 991px) {
position: relative;
top: 0px;
}

View File

@ -6,7 +6,7 @@ import { ApiService } from 'src/app/services/api.service';
import { SeoService } from 'src/app/services/seo.service';
import { formatNumber } from '@angular/common';
import { FormBuilder, FormGroup } from '@angular/forms';
import { formatterXAxisLabel } from 'src/app/shared/graphs.utils';
import { download, formatterXAxisLabel } from 'src/app/shared/graphs.utils';
import { MiningService } from 'src/app/services/mining.service';
import { StorageService } from 'src/app/services/storage.service';
@ -40,6 +40,7 @@ export class BlockRewardsGraphComponent implements OnInit {
isLoading = true;
formatNumber = formatNumber;
timespan = '';
chartInstance: any = undefined;
constructor(
@Inject(LOCALE_ID) public locale: string,
@ -194,7 +195,29 @@ export class BlockRewardsGraphComponent implements OnInit {
};
}
onChartInit(ec) {
this.chartInstance = ec;
}
isMobile() {
return (window.innerWidth <= 767.98);
}
onSaveChart() {
// @ts-ignore
const prevBottom = this.chartOptions.grid.bottom;
const now = new Date();
// @ts-ignore
this.chartOptions.grid.bottom = 40;
this.chartOptions.backgroundColor = '#11131f';
this.chartInstance.setOption(this.chartOptions);
download(this.chartInstance.getDataURL({
pixelRatio: 2,
excludeComponents: ['dataZoom'],
}), `block-rewards-${this.timespan}-${Math.round(now.getTime() / 1000)}.svg`);
// @ts-ignore
this.chartOptions.grid.bottom = prevBottom;
this.chartOptions.backgroundColor = 'none';
this.chartInstance.setOption(this.chartOptions);
}
}

View File

@ -1,3 +1,5 @@
<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>

View File

@ -1,3 +1,5 @@
<app-indexing-progress *ngIf="!widget"></app-indexing-progress>
<div [class]="widget === false ? 'full-container' : ''">
<div *ngIf="widget">
@ -19,6 +21,10 @@
<div class="card-header mb-0 mb-md-4" [style]="widget ? 'display:none' : ''">
<span i18n="mining.hashrate-difficulty">Hashrate & Difficulty</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">
@ -46,7 +52,8 @@
</form>
</div>
<div [class]="!widget ? 'chart' : 'chart-widget'" echarts [initOpts]="chartInitOptions" [options]="chartOptions">
<div [class]="!widget ? 'chart' : 'chart-widget'" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
(chartInit)="onChartInit($event)">
</div>
<div class="text-center loadingGraphs" *ngIf="isLoading">
<div class="spinner-border text-light"></div>

View File

@ -59,11 +59,11 @@
margin-top: 6px;
display: flex;
flex-direction: column;
@media (min-width: 1130px) {
@media (min-width: 991px) {
position: relative;
top: -65px;
}
@media (min-width: 830px) and (max-width: 1130px) {
@media (min-width: 830px) and (max-width: 991px) {
position: relative;
top: 0px;
}

View File

@ -9,6 +9,7 @@ import { FormBuilder, FormGroup } from '@angular/forms';
import { selectPowerOfTen } from 'src/app/bitcoin.utils';
import { StorageService } from 'src/app/services/storage.service';
import { MiningService } from 'src/app/services/mining.service';
import { download } from 'src/app/shared/graphs.utils';
@Component({
selector: 'app-hashrate-chart',
@ -43,6 +44,8 @@ export class HashrateChartComponent implements OnInit {
hashrateObservable$: Observable<any>;
isLoading = true;
formatNumber = formatNumber;
timespan = '';
chartInstance: any = undefined;
constructor(
@Inject(LOCALE_ID) public locale: string,
@ -74,6 +77,7 @@ export class HashrateChartComponent implements OnInit {
if (!this.widget && !firstRun) {
this.storageService.setValue('miningWindowPreference', timespan);
}
this.timespan = timespan;
firstRun = false;
this.miningWindowPreference = timespan;
this.isLoading = true;
@ -132,16 +136,12 @@ export class HashrateChartComponent implements OnInit {
prepareChartOptions(data) {
let title: object;
if (data.hashrates.length === 0) {
const lastBlock = new Date(data.timestamp * 1000);
const dd = String(lastBlock.getDate()).padStart(2, '0');
const mm = String(lastBlock.getMonth() + 1).padStart(2, '0'); // January is 0!
const yyyy = lastBlock.getFullYear();
title = {
textStyle: {
color: 'grey',
fontSize: 15
},
text: `Indexing in progess - ${yyyy}-${mm}-${dd}`,
text: `Indexing in progess`,
left: 'center',
top: 'center'
};
@ -340,7 +340,29 @@ export class HashrateChartComponent implements OnInit {
};
}
onChartInit(ec) {
this.chartInstance = ec;
}
isMobile() {
return (window.innerWidth <= 767.98);
}
onSaveChart() {
// @ts-ignore
const prevBottom = this.chartOptions.grid.bottom;
const now = new Date();
// @ts-ignore
this.chartOptions.grid.bottom = 30;
this.chartOptions.backgroundColor = '#11131f';
this.chartInstance.setOption(this.chartOptions);
download(this.chartInstance.getDataURL({
pixelRatio: 2,
excludeComponents: ['dataZoom'],
}), `hashrate-difficulty-${this.timespan}-${Math.round(now.getTime() / 1000)}.svg`);
// @ts-ignore
this.chartOptions.grid.bottom = prevBottom;
this.chartOptions.backgroundColor = 'none';
this.chartInstance.setOption(this.chartOptions);
}
}

View File

@ -1,7 +1,13 @@
<app-indexing-progress></app-indexing-progress>
<div class="full-container">
<div class="card-header mb-0 mb-md-4">
<span i18n="mining.pools-dominance">Mining 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">
@ -29,8 +35,9 @@
</form>
</div>
<div class="chart"
echarts [initOpts]="chartInitOptions" [options]="chartOptions"></div>
<div class="chart" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
(chartInit)="onChartInit($event)">
</div>
<div class="text-center loadingGraphs" *ngIf="isLoading">
<div class="spinner-border text-light"></div>
</div>

View File

@ -53,11 +53,11 @@
margin-top: 6px;
display: flex;
flex-direction: column;
@media (min-width: 1130px) {
@media (min-width: 991px) {
position: relative;
top: -65px;
}
@media (min-width: 830px) and (max-width: 1130px) {
@media (min-width: 830px) and (max-width: 991px) {
position: relative;
top: 0px;
}

View File

@ -8,6 +8,8 @@ import { FormBuilder, FormGroup } from '@angular/forms';
import { poolsColor } from 'src/app/app.constants';
import { StorageService } from 'src/app/services/storage.service';
import { MiningService } from 'src/app/services/mining.service';
import { download } from 'src/app/shared/graphs.utils';
import { time } from 'console';
@Component({
selector: 'app-hashrate-chart-pools',
@ -39,6 +41,8 @@ export class HashrateChartPoolsComponent implements OnInit {
hashrateObservable$: Observable<any>;
isLoading = true;
timespan = '';
chartInstance: any = undefined;
constructor(
@Inject(LOCALE_ID) public locale: string,
@ -68,6 +72,7 @@ export class HashrateChartPoolsComponent implements OnInit {
if (!firstRun) {
this.storageService.setValue('miningWindowPreference', timespan);
}
this.timespan = timespan;
firstRun = false;
this.isLoading = true;
return this.apiService.getHistoricalPoolsHashrate$(timespan)
@ -145,16 +150,12 @@ export class HashrateChartPoolsComponent implements OnInit {
prepareChartOptions(data) {
let title: object;
if (data.series.length === 0) {
const lastBlock = new Date(data.timestamp * 1000);
const dd = String(lastBlock.getDate()).padStart(2, '0');
const mm = String(lastBlock.getMonth() + 1).padStart(2, '0'); // January is 0!
const yyyy = lastBlock.getFullYear();
title = {
textStyle: {
color: 'grey',
fontSize: 15
},
text: `Indexing in progess - ${yyyy}-${mm}-${dd}`,
text: `Indexing in progess`,
left: 'center',
top: 'center',
};
@ -247,7 +248,29 @@ export class HashrateChartPoolsComponent implements OnInit {
};
}
onChartInit(ec) {
this.chartInstance = ec;
}
isMobile() {
return (window.innerWidth <= 767.98);
}
onSaveChart() {
// @ts-ignore
const prevBottom = this.chartOptions.grid.bottom;
const now = new Date();
// @ts-ignore
this.chartOptions.grid.bottom = 30;
this.chartOptions.backgroundColor = '#11131f';
this.chartInstance.setOption(this.chartOptions);
download(this.chartInstance.getDataURL({
pixelRatio: 2,
excludeComponents: ['dataZoom'],
}), `pools-dominance-${this.timespan}-${Math.round(now.getTime() / 1000)}.svg`);
// @ts-ignore
this.chartOptions.grid.bottom = prevBottom;
this.chartOptions.backgroundColor = 'none';
this.chartInstance.setOption(this.chartOptions);
}
}

View File

@ -1,4 +1,6 @@
<div class="echarts" echarts [initOpts]="mempoolStatsChartInitOption" [options]="mempoolStatsChartOption" (chartRendered)="rendered()"></div>
<div class="echarts" echarts [initOpts]="mempoolStatsChartInitOption" [options]="mempoolStatsChartOption" (chartRendered)="rendered()"
(chartInit)="onChartInit($event)">
</div>
<div class="text-center loadingGraphs" *ngIf="isLoading">
<div class="spinner-border text-light"></div>
</div>

View File

@ -2,7 +2,7 @@ import { Component, Input, Inject, LOCALE_ID, ChangeDetectionStrategy, OnInit }
import { EChartsOption } from 'echarts';
import { OnChanges } from '@angular/core';
import { StorageService } from 'src/app/services/storage.service';
import { formatterXAxis, formatterXAxisLabel } from 'src/app/shared/graphs.utils';
import { download, formatterXAxis, formatterXAxisLabel } from 'src/app/shared/graphs.utils';
import { formatNumber } from '@angular/common';
@Component({
@ -34,6 +34,7 @@ export class IncomingTransactionsGraphComponent implements OnInit, OnChanges {
renderer: 'svg'
};
windowPreference: string;
chartInstance: any = undefined;
constructor(
@Inject(LOCALE_ID) private locale: string,
@ -224,7 +225,29 @@ export class IncomingTransactionsGraphComponent implements OnInit, OnChanges {
};
}
onChartInit(ec) {
this.chartInstance = ec;
}
isMobile() {
return window.innerWidth <= 767.98;
}
onSaveChart(timespan) {
// @ts-ignore
const prevHeight = this.mempoolStatsChartOption.grid.height;
const now = new Date();
// @ts-ignore
this.mempoolStatsChartOption.grid.height = prevHeight + 20;
this.mempoolStatsChartOption.backgroundColor = '#11131f';
this.chartInstance.setOption(this.mempoolStatsChartOption);
download(this.chartInstance.getDataURL({
pixelRatio: 2,
excludeComponents: ['dataZoom'],
}), `incoming-vbytes-${timespan}-${Math.round(now.getTime() / 1000)}.svg`);
// @ts-ignore
this.mempoolStatsChartOption.grid.height = prevHeight;
this.mempoolStatsChartOption.backgroundColor = 'none';
this.chartInstance.setOption(this.mempoolStatsChartOption);
}
}

View File

@ -0,0 +1,3 @@
<app-loading-indicator [name]="'block-indexing'" i18n-label label="Indexing blocks"></app-loading-indicator>
<app-loading-indicator [name]="'daily-hashrate-indexing'" i18n-label label="Indexing network hashrate"></app-loading-indicator>
<app-loading-indicator [name]="'weekly-hashrate-indexing'" i18n-label label="Indexing pools hashrate"></app-loading-indicator>

View File

@ -0,0 +1,14 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
@Component({
selector: 'app-indexing-progress',
templateUrl: './indexing-progress.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class IndexingProgressComponent implements OnInit {
constructor(
) {}
ngOnInit() {
}
}

View File

@ -0,0 +1,3 @@
<div *ngIf="this.indexingProgress$ | async as progress" class="sticky-loading">
<span *ngIf="progress >= 0" class="mr-auto badge badge-pill badge-warning">{{ this.label }} ({{ progress }}%)</span>
</div>

View File

@ -0,0 +1,18 @@
.sticky-loading {
position: absolute;
right: 10px;
z-index: 100;
font-size: 14px;
@media (width >= 992px) {
left: 32px;
top: 55px;
}
@media (576px <= width < 992px ) {
left: 18px;
top: 55px;
}
@media (width <= 575px) {
left: 18px;
top: 100px;
}
}

View File

@ -0,0 +1,30 @@
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { StateService } from 'src/app/services/state.service';
import { WebsocketService } from 'src/app/services/websocket.service';
@Component({
selector: 'app-loading-indicator',
templateUrl: './loading-indicator.component.html',
styleUrls: ['./loading-indicator.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class LoadingIndicatorComponent implements OnInit {
@Input() name: string;
@Input() label: string;
public indexingProgress$: Observable<number>;
constructor(
private stateService: StateService,
private websocketService: WebsocketService
) {}
ngOnInit() {
this.indexingProgress$ = this.stateService.loadingIndicators$
.pipe(
map((indicators) => indicators[this.name] ?? -1)
);
}
}

View File

@ -1,12 +1,12 @@
import { Component, OnInit, Input, Inject, LOCALE_ID, ChangeDetectionStrategy, OnChanges } from '@angular/core';
import { VbytesPipe } from 'src/app/shared/pipes/bytes-pipe/vbytes.pipe';
import { formatNumber } from "@angular/common";
import { formatNumber } from '@angular/common';
import { OptimizedMempoolStats } from 'src/app/interfaces/node-api.interface';
import { StateService } from 'src/app/services/state.service';
import { StorageService } from 'src/app/services/storage.service';
import { EChartsOption } from 'echarts';
import { feeLevels, chartColors } from 'src/app/app.constants';
import { formatterXAxis, formatterXAxisLabel } from 'src/app/shared/graphs.utils';
import { download, formatterXAxis, formatterXAxisLabel } from 'src/app/shared/graphs.utils';
@Component({
selector: 'app-mempool-graph',
@ -45,6 +45,7 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
feeLevelsOrdered = [];
chartColorsOrdered = chartColors;
inverted: boolean;
chartInstance: any = undefined;
constructor(
private vbytesPipe: VbytesPipe,
@ -83,6 +84,7 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
this.hoverIndexSerie = e.target.parent.parent.__ecComponentInfo.index;
}
});
this.chartInstance = myChart;
}
handleNewMempoolData(mempoolStats: OptimizedMempoolStats[]) {
@ -99,7 +101,7 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
generateArray(mempoolStats: OptimizedMempoolStats[]) {
const finalArray: number[][][] = [];
let feesArray: number[][] = [];
let limitFeesTemplate = this.template === 'advanced' ? 26 : 20;
const limitFeesTemplate = this.template === 'advanced' ? 26 : 20;
for (let index = limitFeesTemplate; index > -1; index--) {
feesArray = [];
mempoolStats.forEach((stats) => {
@ -387,5 +389,23 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
isMobile() {
return window.innerWidth <= 767.98;
}
onSaveChart(timespan) {
// @ts-ignore
const prevHeight = this.mempoolVsizeFeesOptions.grid.height;
const now = new Date();
// @ts-ignore
this.mempoolVsizeFeesOptions.grid.height = prevHeight + 20;
this.mempoolVsizeFeesOptions.backgroundColor = '#11131f';
this.chartInstance.setOption(this.mempoolVsizeFeesOptions);
download(this.chartInstance.getDataURL({
pixelRatio: 2,
excludeComponents: ['dataZoom'],
}), `mempool-graph-${timespan}-${Math.round(now.getTime() / 1000)}.svg`);
// @ts-ignore
this.mempoolVsizeFeesOptions.grid.height = prevHeight;
this.mempoolVsizeFeesOptions.backgroundColor = 'none';
this.chartInstance.setOption(this.mempoolVsizeFeesOptions);
}
}

View File

@ -1,3 +1,5 @@
<app-indexing-progress></app-indexing-progress>
<div class="container-xl dashboard-container">
<div class="row row-cols-1 row-cols-md-2">

View File

@ -1,8 +1,5 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { map } from 'rxjs/operators';
import { SeoService } from 'src/app/services/seo.service';
import { StateService } from 'src/app/services/state.service';
import { Observable } from 'rxjs';
import { WebsocketService } from 'src/app/services/websocket.service';
@Component({
@ -12,8 +9,6 @@ import { WebsocketService } from 'src/app/services/websocket.service';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MiningDashboardComponent implements OnInit {
private blocks = [];
constructor(
private seoService: SeoService,
private websocketService: WebsocketService,

View File

@ -1,3 +1,5 @@
<app-indexing-progress *ngIf="!widget"></app-indexing-progress>
<div [class]="widget === false ? 'full-container' : ''">
<div *ngIf="widget">
@ -25,6 +27,10 @@
<div class="card-header" *ngIf="!widget">
<span i18n="mining.mining-pool-share">Mining pools share</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">

View File

@ -37,11 +37,11 @@
margin-top: 6px;
display: flex;
flex-direction: column;
@media (min-width: 1130px) {
@media (min-width: 991px) {
position: relative;
top: -65px;
}
@media (min-width: 830px) and (max-width: 1130px) {
@media (min-width: 830px) and (max-width: 991px) {
position: relative;
top: 0px;
}

View File

@ -11,6 +11,7 @@ import { MiningService, MiningStats } from '../../services/mining.service';
import { StateService } from '../../services/state.service';
import { chartColors, poolsColor } from 'src/app/app.constants';
import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe';
import { download } from 'src/app/shared/graphs.utils';
@Component({
selector: 'app-pool-ranking',
@ -29,6 +30,7 @@ export class PoolRankingComponent implements OnInit {
chartInitOptions = {
renderer: 'svg',
};
timespan = '';
chartInstance: any = undefined;
@HostBinding('attr.dir') dir = 'ltr';
@ -69,6 +71,7 @@ export class PoolRankingComponent implements OnInit {
.pipe(
startWith(this.miningWindowPreference), // (trigger when the page loads)
tap((value) => {
this.timespan = value;
if (!this.widget) {
this.storageService.setValue('miningWindowPreference', value);
}
@ -283,5 +286,17 @@ export class PoolRankingComponent implements OnInit {
},
};
}
onSaveChart() {
const now = new Date();
this.chartOptions.backgroundColor = '#11131f';
this.chartInstance.setOption(this.chartOptions);
download(this.chartInstance.getDataURL({
pixelRatio: 2,
excludeComponents: ['dataZoom'],
}), `pools-ranking-${this.timespan}-${Math.round(now.getTime() / 1000)}.svg`);
this.chartOptions.backgroundColor = 'none';
this.chartInstance.setOption(this.chartOptions);
}
}

View File

@ -1,3 +1,5 @@
<app-indexing-progress></app-indexing-progress>
<div class="container-xl">
<!-- Pool overview -->

View File

@ -111,7 +111,7 @@ export class PoolComponent implements OnInit {
color: 'grey',
fontSize: 15
},
text: `No data`,
text: `Indexing in progress`,
left: 'center',
top: 'center'
};
@ -164,14 +164,14 @@ export class PoolComponent implements OnInit {
`;
}.bind(this)
},
xAxis: {
xAxis: data.length === 0 ? undefined : {
type: 'time',
splitNumber: (this.isMobile()) ? 5 : 10,
axisLabel: {
hideOverlap: true,
}
},
yAxis: [
yAxis: data.length === 0 ? undefined : [
{
min: (value) => {
return value.min * 0.9;
@ -190,7 +190,7 @@ export class PoolComponent implements OnInit {
}
},
],
series: [
series: data.length === 0 ? undefined : [
{
zlevel: 0,
name: 'Hashrate',

View File

@ -3,14 +3,22 @@
<div>
<div class="card mb-3">
<div class="card-header">
<i class="fa fa-area-chart"></i> <span i18n="statistics.memory-by-vBytes">Mempool by vBytes (sat/vByte)</span>
<form [formGroup]="radioGroupForm" class="formRadioGroup" [class]="stateService.env.MINING_DASHBOARD ? 'mining' : ''" (click)="saveGraphPreference()">
<i class="fa fa-area-chart"></i>
<span i18n="statistics.memory-by-vBytes">Mempool by vBytes (sat/vByte)</span>
<button class="btn" style="margin: 0 0 4px 0px" (click)="onSaveChart('mempool')">
<fa-icon [icon]="['fas', 'download']" [fixedWidth]="true"></fa-icon>
</button>
<form [formGroup]="radioGroupForm" class="formRadioGroup"
[class]="stateService.env.MINING_DASHBOARD ? 'mining' : ''" (click)="saveGraphPreference()">
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan">
<label ngbButtonLabel class="btn-primary btn-sm">
<input ngbButton type="radio" [value]="'2h'" [routerLink]="['/graphs' | relativeUrl]" fragment="2h"> 2H (LIVE)
<input ngbButton type="radio" [value]="'2h'" [routerLink]="['/graphs' | relativeUrl]" fragment="2h"> 2H
(LIVE)
</label>
<label ngbButtonLabel class="btn-primary btn-sm">
<input ngbButton type="radio" [value]="'24h'" [routerLink]="['/graphs' | relativeUrl]" fragment="24h"> 24H
<input ngbButton type="radio" [value]="'24h'" [routerLink]="['/graphs' | relativeUrl]" fragment="24h">
24H
</label>
<label ngbButtonLabel class="btn-primary btn-sm">
<input ngbButton type="radio" [value]="'1w'" [routerLink]="['/graphs' | relativeUrl]" fragment="1w"> 1W
@ -37,39 +45,37 @@
<div class="small-buttons">
<div ngbDropdown #myDrop="ngbDropdown">
<button class="btn btn-primary btn-sm" id="dropdownFees" ngbDropdownAnchor (click)="myDrop.toggle()">
<fa-icon [icon]="['fas', 'filter']" [fixedWidth]="true" i18n-title="statistics.component-filter.title" title="Filter"></fa-icon>
<fa-icon [icon]="['fas', 'filter']" [fixedWidth]="true" i18n-title="statistics.component-filter.title"
title="Filter"></fa-icon>
</button>
<div class="dropdown-fees" ngbDropdownMenu aria-labelledby="dropdownFees">
<ul>
<ng-template ngFor let-feeData let-i="index" [ngForOf]="feeLevelDropdownData">
<ng-template [ngIf]="feeData.fee <= 400">
<li (click)="filterFeeIndex = feeData.fee" [class]="filterFeeIndex > feeData.fee ? 'inactive' : ''">
<li (click)="filterFeeIndex = feeData.fee"
[class]="filterFeeIndex > feeData.fee ? 'inactive' : ''">
<span class="square" [ngStyle]="{'backgroundColor': feeData.color}"></span>
<span class="fee-text">{{ feeData.range }}</span>
</li>
</li>
</ng-template>
</ng-template>
</ul>
</div>
</div>
<button (click)="invertGraph()" class="btn btn-primary btn-sm"><fa-icon [icon]="['fas', 'exchange-alt']" [rotate]="90" [fixedWidth]="true" i18n-title="statistics.component-invert.title" title="Invert"></fa-icon></button>
<button (click)="invertGraph()" class="btn btn-primary btn-sm">
<fa-icon [icon]="['fas', 'exchange-alt']" [rotate]="90" [fixedWidth]="true"
i18n-title="statistics.component-invert.title" title="Invert"></fa-icon>
</button>
</div>
</form>
<div class="spinner-border text-light bootstrap-spinner" *ngIf="spinnerLoading && mempoolStats.length"></div>
</div>
<div class="card-body">
<div class="incoming-transactions-graph">
<app-mempool-graph
dir="ltr"
[template]="'advanced'"
[limitFee]="500"
[limitFilterFee]="filterFeeIndex"
[height]="500"
[left]="65"
[right]="10"
[data]="mempoolStats && mempoolStats.length ? mempoolStats : null"
></app-mempool-graph>
<app-mempool-graph #mempoolgraph dir="ltr" [template]="'advanced'" [limitFee]="500"
[limitFilterFee]="filterFeeIndex" [height]="500" [left]="65" [right]="10"
[data]="mempoolStats && mempoolStats.length ? mempoolStats : null"></app-mempool-graph>
</div>
</div>
</div>
@ -78,19 +84,20 @@
<div>
<div class="card mb-3">
<div class="card-header">
<i class="fa fa-area-chart"></i> <span i18n="statistics.transaction-vbytes-per-second">Transaction vBytes per second (vB/s)</span>
<i class="fa fa-area-chart"></i>
<span i18n="statistics.transaction-vbytes-per-second">Transaction vBytes per second (vB/s)</span>
<button class="btn" style="margin: 0 0 4px 0px" (click)="onSaveChart('incoming')">
<fa-icon [icon]="['fas', 'download']" [fixedWidth]="true"></fa-icon>
</button>
</div>
<div class="card-body">
<div class="incoming-transactions-graph">
<app-incoming-transactions-graph
[height]="500"
[left]="65"
[template]="'advanced'"
[data]="mempoolTransactionsWeightPerSecondData"
></app-incoming-transactions-graph>
<app-incoming-transactions-graph #incominggraph [height]="500" [left]="65" [template]="'advanced'"
[data]="mempoolTransactionsWeightPerSecondData"></app-incoming-transactions-graph>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -53,11 +53,11 @@
}
}
.formRadioGroup.mining {
@media (min-width: 1130px) {
@media (min-width: 991px) {
position: relative;
top: -65px;
}
@media (min-width: 830px) and (max-width: 1130px) {
@media (min-width: 830px) and (max-width: 991px) {
position: relative;
top: 0px;
}

View File

@ -1,4 +1,4 @@
import { Component, OnInit, LOCALE_ID, Inject } from '@angular/core';
import { Component, OnInit, LOCALE_ID, Inject, ViewChild, ElementRef } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { FormGroup, FormBuilder } from '@angular/forms';
import { of, merge} from 'rxjs';
@ -12,6 +12,8 @@ import { StateService } from 'src/app/services/state.service';
import { SeoService } from 'src/app/services/seo.service';
import { StorageService } from 'src/app/services/storage.service';
import { feeLevels, chartColors } from 'src/app/app.constants';
import { MempoolGraphComponent } from '../mempool-graph/mempool-graph.component';
import { IncomingTransactionsGraphComponent } from '../incoming-transactions-graph/incoming-transactions-graph.component';
@Component({
selector: 'app-statistics',
@ -19,6 +21,9 @@ import { feeLevels, chartColors } from 'src/app/app.constants';
styleUrls: ['./statistics.component.scss']
})
export class StatisticsComponent implements OnInit {
@ViewChild('mempoolgraph') mempoolGraph: MempoolGraphComponent;
@ViewChild('incominggraph') incomingGraph: IncomingTransactionsGraphComponent;
network = '';
loading = true;
@ -38,6 +43,7 @@ export class StatisticsComponent implements OnInit {
graphWindowPreference: string;
inverted: boolean;
feeLevelDropdownData = [];
timespan = '';
constructor(
@Inject(LOCALE_ID) private locale: string,
@ -75,6 +81,7 @@ export class StatisticsComponent implements OnInit {
)
.pipe(
switchMap(() => {
this.timespan = this.radioGroupForm.controls.dateSpan.value;
this.spinnerLoading = true;
if (this.radioGroupForm.controls.dateSpan.value === '2h') {
this.websocketService.want(['blocks', 'live-2h-chart']);
@ -195,4 +202,12 @@ export class StatisticsComponent implements OnInit {
stat.vbytes_per_second = Math.min(median * capRatio, stat.vbytes_per_second);
}
}
onSaveChart(name) {
if (name === 'mempool') {
this.mempoolGraph.onSaveChart(this.timespan);
} else if (name === 'incoming') {
this.incomingGraph.onSaveChart(this.timespan);
}
}
}

View File

@ -4583,5 +4583,13 @@ export const faqData = [
fragment: "install-mempool-with-docker",
title: "Can I install Mempool using Docker?",
answer: "Yes, we publish Docker images (or you can build your own). Check out <a href='https://github.com/mempool/mempool/tree/master/docker' target='_blank'>the documentation</a> for details."
},
{
type: "endpoint",
category: "advanced",
showConditions: bitcoinNetworks,
fragment: "address-lookup-issues",
title: "Why do I get an error for certain address lookups on my Mempool instance?",
answer: "<p>If you're getting errors when doing address lookups, it's probably because of your Electrum server backend.</p><p>Mempool uses an Electrum server to do address lookups. There are several implementations of the Electrum server protocol, and Mempool can use any of them, but the implementation you use affects performance:</p><ol><li><a href='https://github.com/romanz/electrs' target='_blank'>romanz/electrs</a>. This is a common choice for its low resource requirements, and most full-node distros use it. But while this implementation works great for basic queries, it will struggle with heavier ones (e.g. looking up addresses with many transactions)—especially when running on low-power hardware like a Raspberry Pi.</li><li><a href='https://github.com/cculianu/Fulcrum' target='_blank'>Fulcrum</a>. Fulcrum requires more resources than romanz/electrs but it can still run on a Raspberry Pi, and it handles heavy queries much more efficiently. If you're having issues with romanz/electrs, Fulcrum is worth a try.</li><li><a href='https://github.com/Blockstream/electrs' target='_blank'>blockstream/electrs</a>. If you have stronger hardware, consider running Blockstream's electrs implementation. It's the backend mempool.space uses, and is also what powers blockstream.info.</li></ol>"
}
];

View File

@ -77,3 +77,12 @@ export const formatterXAxisTimeCategory = (
return date.toLocaleDateString(locale, { year: 'numeric', month: 'long' });
}
};
export const download = (href, name) => {
const a = document.createElement('a');
a.download = name;
a.href = href;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
};