mirror of
https://github.com/mempool/mempool.git
synced 2025-04-02 08:58:17 +02:00
Merge branch 'master' of https://github.com/mempool/mempool
This commit is contained in:
commit
411b75471c
@ -21,11 +21,10 @@
|
||||
"main": "index.ts",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"start": "npm run build && node --max-old-space-size=4096 dist/index.js",
|
||||
"start": "node --max-old-space-size=4096 dist/index.js",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"dependencies": {
|
||||
"compression": "^1.7.4",
|
||||
"express": "^4.17.1",
|
||||
"locutus": "^2.0.12",
|
||||
"mysql2": "^1.6.1",
|
||||
|
@ -26,7 +26,7 @@ class BackendInfo {
|
||||
try {
|
||||
this.gitCommitHash = fs.readFileSync('../.git/refs/heads/master').toString().trim();
|
||||
} catch (e) {
|
||||
logger.err('Could not load git commit info, skipping.');
|
||||
logger.err('Could not load git commit info: ' + e.message || e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -139,7 +139,7 @@ class Bisq {
|
||||
|
||||
private updatePrice() {
|
||||
request('https://markets.bisq.network/api/trades/?market=bsq_btc', { json: true }, (err, res, trades: BisqTrade[]) => {
|
||||
if (err) { return logger.err('Error updating Bisq market price: ' + err); }
|
||||
if (err || !Array.isArray(trades)) { return logger.err('Error updating Bisq market price: ' + err); }
|
||||
|
||||
const prices: number[] = [];
|
||||
trades.forEach((trade) => {
|
||||
@ -160,7 +160,7 @@ class Bisq {
|
||||
this.buildIndex();
|
||||
this.calculateStats();
|
||||
} catch (e) {
|
||||
logger.err('loadBisqDumpFile() error.' + e.message);
|
||||
logger.err('loadBisqDumpFile() error.' + e.message || e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -98,7 +98,7 @@ class Bisq {
|
||||
logger.debug('Bisq market data updated in ' + time + ' ms');
|
||||
}
|
||||
} catch (e) {
|
||||
logger.err('loadBisqMarketDataDumpFile() error.' + e.message);
|
||||
logger.err('loadBisqMarketDataDumpFile() error.' + e.message || e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,6 +3,7 @@ import logger from '../logger';
|
||||
import memPool from './mempool';
|
||||
import { Block, TransactionExtended, TransactionMinerInfo } from '../interfaces';
|
||||
import { Common } from './common';
|
||||
import diskCache from './disk-cache';
|
||||
|
||||
class Blocks {
|
||||
private static INITIAL_BLOCK_AMOUNT = 8;
|
||||
@ -99,6 +100,7 @@ class Blocks {
|
||||
if (this.newBlockCallbacks.length) {
|
||||
this.newBlockCallbacks.forEach((cb) => cb(block, txIds, transactions));
|
||||
}
|
||||
diskCache.$saveCacheToDiskAsync();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -6,10 +6,16 @@ import blocks from './blocks';
|
||||
import logger from '../logger';
|
||||
|
||||
class DiskCache {
|
||||
static FILE_NAME = './cache.json';
|
||||
private static FILE_NAME = './cache.json';
|
||||
private enabled = true;
|
||||
|
||||
constructor() {
|
||||
if (process.env.workerId === '0' || !config.MEMPOOL.SPAWN_CLUSTER_PROCS) {
|
||||
if (!fs.existsSync(DiskCache.FILE_NAME)) {
|
||||
fs.closeSync(fs.openSync(DiskCache.FILE_NAME, 'w'));
|
||||
logger.info('Disk cache file created');
|
||||
}
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
this.saveCacheToDisk();
|
||||
process.exit(2);
|
||||
@ -19,19 +25,28 @@ class DiskCache {
|
||||
this.saveCacheToDisk();
|
||||
process.exit(2);
|
||||
});
|
||||
} else {
|
||||
this.enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
saveCacheToDisk() {
|
||||
this.saveData(JSON.stringify({
|
||||
mempool: memPool.getMempool(),
|
||||
blocks: blocks.getBlocks(),
|
||||
}));
|
||||
logger.info('Mempool and blocks data saved to disk cache');
|
||||
async $saveCacheToDiskAsync(): Promise<void> {
|
||||
if (!this.enabled) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this.$saveDataAsync(JSON.stringify({
|
||||
mempool: memPool.getMempool(),
|
||||
blocks: blocks.getBlocks(),
|
||||
}));
|
||||
logger.debug('Mempool and blocks data saved to disk cache');
|
||||
} catch (e) {
|
||||
logger.warn('Error writing to cache file asynchronously: ' + e.message || e);
|
||||
}
|
||||
}
|
||||
|
||||
loadMempoolCache() {
|
||||
const cacheData = this.loadData();
|
||||
const cacheData = this.loadDataSync();
|
||||
if (cacheData) {
|
||||
logger.info('Restoring mempool and blocks data from disk cache');
|
||||
const data = JSON.parse(cacheData);
|
||||
@ -40,11 +55,30 @@ class DiskCache {
|
||||
}
|
||||
}
|
||||
|
||||
private saveData(dataBlob: string) {
|
||||
private saveCacheToDisk() {
|
||||
this.saveDataSync(JSON.stringify({
|
||||
mempool: memPool.getMempool(),
|
||||
blocks: blocks.getBlocks(),
|
||||
}));
|
||||
logger.info('Mempool and blocks data saved to disk cache');
|
||||
}
|
||||
|
||||
private $saveDataAsync(dataBlob: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.writeFile(DiskCache.FILE_NAME, dataBlob, (err) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private saveDataSync(dataBlob: string) {
|
||||
fs.writeFileSync(DiskCache.FILE_NAME, dataBlob, 'utf8');
|
||||
}
|
||||
|
||||
private loadData(): string {
|
||||
private loadDataSync(): string {
|
||||
return fs.readFileSync(DiskCache.FILE_NAME, 'utf8');
|
||||
}
|
||||
}
|
||||
|
@ -34,7 +34,7 @@ class Donations {
|
||||
this.notifyDonationStatusCallback = fn;
|
||||
}
|
||||
|
||||
createRequest(amount: number, orderId: string): Promise<any> {
|
||||
$createRequest(amount: number, orderId: string): Promise<any> {
|
||||
logger.notice('New invoice request. Handle: ' + orderId + ' Amount: ' + amount + ' BTC');
|
||||
|
||||
const postData = {
|
||||
@ -55,7 +55,7 @@ class Donations {
|
||||
const formattedBody = {
|
||||
id: body.data.id,
|
||||
amount: parseFloat(body.data.btcPrice),
|
||||
address: body.data.bitcoinAddress,
|
||||
addresses: body.data.addresses,
|
||||
};
|
||||
resolve(formattedBody);
|
||||
});
|
||||
@ -110,7 +110,7 @@ class Donations {
|
||||
connection.release();
|
||||
return rows;
|
||||
} catch (e) {
|
||||
logger.err('$getDonationsFromDatabase() error' + e);
|
||||
logger.err('$getDonationsFromDatabase() error: ' + e.message || e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@ -123,7 +123,7 @@ class Donations {
|
||||
connection.release();
|
||||
return rows;
|
||||
} catch (e) {
|
||||
logger.err('$getLegacyDonations() error' + e);
|
||||
logger.err('$getLegacyDonations() error' + e.message || e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@ -159,7 +159,7 @@ class Donations {
|
||||
const [result]: any = await connection.query(query, params);
|
||||
connection.release();
|
||||
} catch (e) {
|
||||
logger.err('$addDonationToDatabase() error' + e);
|
||||
logger.err('$addDonationToDatabase() error' + e.message || e);
|
||||
}
|
||||
}
|
||||
|
||||
@ -177,7 +177,7 @@ class Donations {
|
||||
const [result]: any = await connection.query(query, params);
|
||||
connection.release();
|
||||
} catch (e) {
|
||||
logger.err('$updateDonation() error' + e);
|
||||
logger.err('$updateDonation() error' + e.message || e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -234,53 +234,52 @@ class Statistics {
|
||||
connection.release();
|
||||
return result.insertId;
|
||||
} catch (e) {
|
||||
logger.err('$create() error' + e);
|
||||
logger.err('$create() error' + e.message || e);
|
||||
}
|
||||
}
|
||||
|
||||
private getQueryForDays(days: number, groupBy: number) {
|
||||
|
||||
private getQueryForDays(div: number) {
|
||||
return `SELECT id, added, unconfirmed_transactions,
|
||||
AVG(tx_per_second) AS tx_per_second,
|
||||
AVG(vbytes_per_second) AS vbytes_per_second,
|
||||
AVG(vsize_1) AS vsize_1,
|
||||
AVG(vsize_2) AS vsize_2,
|
||||
AVG(vsize_3) AS vsize_3,
|
||||
AVG(vsize_4) AS vsize_4,
|
||||
AVG(vsize_5) AS vsize_5,
|
||||
AVG(vsize_6) AS vsize_6,
|
||||
AVG(vsize_8) AS vsize_8,
|
||||
AVG(vsize_10) AS vsize_10,
|
||||
AVG(vsize_12) AS vsize_12,
|
||||
AVG(vsize_15) AS vsize_15,
|
||||
AVG(vsize_20) AS vsize_20,
|
||||
AVG(vsize_30) AS vsize_30,
|
||||
AVG(vsize_40) AS vsize_40,
|
||||
AVG(vsize_50) AS vsize_50,
|
||||
AVG(vsize_60) AS vsize_60,
|
||||
AVG(vsize_70) AS vsize_70,
|
||||
AVG(vsize_80) AS vsize_80,
|
||||
AVG(vsize_90) AS vsize_90,
|
||||
AVG(vsize_100) AS vsize_100,
|
||||
AVG(vsize_125) AS vsize_125,
|
||||
AVG(vsize_150) AS vsize_150,
|
||||
AVG(vsize_175) AS vsize_175,
|
||||
AVG(vsize_200) AS vsize_200,
|
||||
AVG(vsize_250) AS vsize_250,
|
||||
AVG(vsize_300) AS vsize_300,
|
||||
AVG(vsize_350) AS vsize_350,
|
||||
AVG(vsize_400) AS vsize_400,
|
||||
AVG(vsize_500) AS vsize_500,
|
||||
AVG(vsize_600) AS vsize_600,
|
||||
AVG(vsize_700) AS vsize_700,
|
||||
AVG(vsize_800) AS vsize_800,
|
||||
AVG(vsize_900) AS vsize_900,
|
||||
AVG(vsize_1000) AS vsize_1000,
|
||||
AVG(vsize_1200) AS vsize_1200,
|
||||
AVG(vsize_1400) AS vsize_1400,
|
||||
AVG(vsize_1600) AS vsize_1600,
|
||||
AVG(vsize_1800) AS vsize_1800,
|
||||
AVG(vsize_2000) AS vsize_2000 FROM statistics GROUP BY UNIX_TIMESTAMP(added) DIV ${groupBy} ORDER BY id DESC LIMIT ${days}`;
|
||||
tx_per_second,
|
||||
vbytes_per_second,
|
||||
vsize_1,
|
||||
vsize_2,
|
||||
vsize_3,
|
||||
vsize_4,
|
||||
vsize_5,
|
||||
vsize_6,
|
||||
vsize_8,
|
||||
vsize_10,
|
||||
vsize_12,
|
||||
vsize_15,
|
||||
vsize_20,
|
||||
vsize_30,
|
||||
vsize_40,
|
||||
vsize_50,
|
||||
vsize_60,
|
||||
vsize_70,
|
||||
vsize_80,
|
||||
vsize_90,
|
||||
vsize_100,
|
||||
vsize_125,
|
||||
vsize_150,
|
||||
vsize_175,
|
||||
vsize_200,
|
||||
vsize_250,
|
||||
vsize_300,
|
||||
vsize_350,
|
||||
vsize_400,
|
||||
vsize_500,
|
||||
vsize_600,
|
||||
vsize_700,
|
||||
vsize_800,
|
||||
vsize_900,
|
||||
vsize_1000,
|
||||
vsize_1200,
|
||||
vsize_1400,
|
||||
vsize_1600,
|
||||
vsize_1800,
|
||||
vsize_2000 FROM statistics GROUP BY UNIX_TIMESTAMP(added) DIV ${div} ORDER BY id DESC LIMIT 480`;
|
||||
}
|
||||
|
||||
public async $get(id: number): Promise<OptimizedStatistic | undefined> {
|
||||
@ -293,7 +292,7 @@ class Statistics {
|
||||
return this.mapStatisticToOptimizedStatistic([rows[0]])[0];
|
||||
}
|
||||
} catch (e) {
|
||||
logger.err('$list2H() error' + e);
|
||||
logger.err('$list2H() error' + e.message || e);
|
||||
}
|
||||
}
|
||||
|
||||
@ -305,7 +304,7 @@ class Statistics {
|
||||
connection.release();
|
||||
return this.mapStatisticToOptimizedStatistic(rows);
|
||||
} catch (e) {
|
||||
logger.err('$list2H() error' + e);
|
||||
logger.err('$list2H() error' + e.message || e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@ -313,11 +312,12 @@ class Statistics {
|
||||
public async $list24H(): Promise<OptimizedStatistic[]> {
|
||||
try {
|
||||
const connection = await DB.pool.getConnection();
|
||||
const query = this.getQueryForDays(120, 720);
|
||||
const query = this.getQueryForDays(180);
|
||||
const [rows] = await connection.query<any>(query);
|
||||
connection.release();
|
||||
return this.mapStatisticToOptimizedStatistic(rows);
|
||||
} catch (e) {
|
||||
logger.err('$list24h() error' + e.message || e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@ -325,7 +325,7 @@ class Statistics {
|
||||
public async $list1W(): Promise<OptimizedStatistic[]> {
|
||||
try {
|
||||
const connection = await DB.pool.getConnection();
|
||||
const query = this.getQueryForDays(120, 5040);
|
||||
const query = this.getQueryForDays(1260);
|
||||
const [rows] = await connection.query<any>(query);
|
||||
connection.release();
|
||||
return this.mapStatisticToOptimizedStatistic(rows);
|
||||
@ -338,7 +338,7 @@ class Statistics {
|
||||
public async $list1M(): Promise<OptimizedStatistic[]> {
|
||||
try {
|
||||
const connection = await DB.pool.getConnection();
|
||||
const query = this.getQueryForDays(120, 20160);
|
||||
const query = this.getQueryForDays(5040);
|
||||
const [rows] = await connection.query<any>(query);
|
||||
connection.release();
|
||||
return this.mapStatisticToOptimizedStatistic(rows);
|
||||
@ -351,7 +351,7 @@ class Statistics {
|
||||
public async $list3M(): Promise<OptimizedStatistic[]> {
|
||||
try {
|
||||
const connection = await DB.pool.getConnection();
|
||||
const query = this.getQueryForDays(120, 60480);
|
||||
const query = this.getQueryForDays(15120);
|
||||
const [rows] = await connection.query<any>(query);
|
||||
connection.release();
|
||||
return this.mapStatisticToOptimizedStatistic(rows);
|
||||
@ -364,7 +364,7 @@ class Statistics {
|
||||
public async $list6M(): Promise<OptimizedStatistic[]> {
|
||||
try {
|
||||
const connection = await DB.pool.getConnection();
|
||||
const query = this.getQueryForDays(120, 120960);
|
||||
const query = this.getQueryForDays(30240);
|
||||
const [rows] = await connection.query<any>(query);
|
||||
connection.release();
|
||||
return this.mapStatisticToOptimizedStatistic(rows);
|
||||
@ -377,7 +377,7 @@ class Statistics {
|
||||
public async $list1Y(): Promise<OptimizedStatistic[]> {
|
||||
try {
|
||||
const connection = await DB.pool.getConnection();
|
||||
const query = this.getQueryForDays(120, 241920);
|
||||
const query = this.getQueryForDays(60480);
|
||||
const [rows] = await connection.query<any>(query);
|
||||
connection.release();
|
||||
return this.mapStatisticToOptimizedStatistic(rows);
|
||||
|
@ -107,7 +107,7 @@ class WebsocketHandler {
|
||||
client.send(JSON.stringify(response));
|
||||
}
|
||||
} catch (e) {
|
||||
logger.err('Error parsing websocket message: ' + e);
|
||||
logger.err('Error parsing websocket message: ' + e.message || e);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -20,7 +20,7 @@ export async function checkDbConnection() {
|
||||
logger.info('Database connection established.');
|
||||
connection.release();
|
||||
} catch (e) {
|
||||
logger.err('Could not connect to database.');
|
||||
logger.err('Could not connect to database: ' + e.message || e);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { Express, Request, Response, NextFunction } from 'express';
|
||||
import * as express from 'express';
|
||||
import * as compression from 'compression';
|
||||
import * as http from 'http';
|
||||
import * as https from 'https';
|
||||
import * as WebSocket from 'ws';
|
||||
@ -66,7 +65,6 @@ class Server {
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
next();
|
||||
})
|
||||
.use(compression())
|
||||
.use(express.urlencoded({ extended: true }))
|
||||
.use(express.json());
|
||||
|
||||
|
@ -7,13 +7,15 @@ import mempoolBlocks from './api/mempool-blocks';
|
||||
import mempool from './api/mempool';
|
||||
import bisq from './api/bisq/bisq';
|
||||
import bisqMarket from './api/bisq/markets-api';
|
||||
import { RequiredSpec } from './interfaces';
|
||||
import { OptimizedStatistic, RequiredSpec } from './interfaces';
|
||||
import { MarketsApiError } from './api/bisq/interfaces';
|
||||
import donations from './api/donations';
|
||||
import logger from './logger';
|
||||
|
||||
class Routes {
|
||||
private cache = {};
|
||||
private cache: { [date: string]: OptimizedStatistic[] } = {
|
||||
'24h': [], '1w': [], '1m': [], '3m': [], '6m': [], '1y': [],
|
||||
};
|
||||
|
||||
constructor() {
|
||||
if (config.DATABASE.ENABLED && config.STATISTICS.ENABLED) {
|
||||
@ -134,7 +136,7 @@ class Routes {
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await donations.createRequest(p.amount, p.orderId);
|
||||
const result = await donations.$createRequest(p.amount, p.orderId);
|
||||
res.json(result);
|
||||
} catch (e) {
|
||||
res.status(500).send(e.message);
|
||||
|
2525
frontend/package-lock.json
generated
2525
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -59,7 +59,7 @@
|
||||
"zone.js": "~0.10.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "~0.1000.3",
|
||||
"@angular-devkit/build-angular": "^0.1002.0",
|
||||
"@angular/cli": "~10.0.3",
|
||||
"@angular/compiler-cli": "~10.0.4",
|
||||
"@angular/language-service": "~10.0.4",
|
||||
|
@ -43,9 +43,11 @@ import { NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { FeesBoxComponent } from './components/fees-box/fees-box.component';
|
||||
import { DashboardComponent } from './dashboard/dashboard.component';
|
||||
import { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontawesome';
|
||||
import { faChartArea, faCogs, faCubes, faDatabase, faInfoCircle, faList, faSearch, faTachometerAlt, faThList, faTv } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faAngleDoubleDown, faAngleDoubleUp, faAngleDown, faAngleUp, faBolt, faChartArea, faCogs, faCubes, faDatabase, faInfoCircle,
|
||||
faLink, faList, faSearch, faTachometerAlt, faThList, faTint, faTv } from '@fortawesome/free-solid-svg-icons';
|
||||
import { ApiDocsComponent } from './components/api-docs/api-docs.component';
|
||||
import { TermsOfServiceComponent } from './components/terms-of-service/terms-of-service.component';
|
||||
import { StorageService } from './services/storage.service';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@ -97,6 +99,7 @@ import { TermsOfServiceComponent } from './components/terms-of-service/terms-of-
|
||||
WebsocketService,
|
||||
AudioService,
|
||||
SeoService,
|
||||
StorageService,
|
||||
],
|
||||
bootstrap: [AppComponent]
|
||||
})
|
||||
@ -113,5 +116,10 @@ export class AppModule {
|
||||
library.addIcons(faTachometerAlt);
|
||||
library.addIcons(faDatabase);
|
||||
library.addIcons(faSearch);
|
||||
library.addIcons(faLink);
|
||||
library.addIcons(faBolt);
|
||||
library.addIcons(faTint);
|
||||
library.addIcons(faAngleDown);
|
||||
library.addIcons(faAngleUp);
|
||||
}
|
||||
}
|
||||
|
@ -73,14 +73,16 @@
|
||||
</div>
|
||||
<input formControlName="amount" class="form-control" type="number" min="0.001" step="1E-03">
|
||||
</div>
|
||||
<div class="input-group mb-4" *ngIf="donationForm.get('amount').value >= 0.01; else lowAmount">
|
||||
<div class="input-group" *ngIf="donationForm.get('amount').value >= 0.01; else lowAmount">
|
||||
<div class="input-group-prepend" style="width: 42px;">
|
||||
<span class="input-group-text">@</span>
|
||||
</div>
|
||||
<input formControlName="handle" class="form-control" type="text" placeholder="Twitter handle (Optional)">
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<button class="btn btn-primary mx-auto" type="submit">Request invoice</button>
|
||||
<div class="required" *ngIf="donationForm.get('amount').hasError('required')">Amount required</div>
|
||||
<div class="required" *ngIf="donationForm.get('amount').hasError('min')">Minimum amount is 0.001 BTC</div>
|
||||
<div class="input-group mt-4">
|
||||
<button class="btn btn-primary mx-auto" type="submit" [disabled]="donationForm.invalid">Request invoice</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@ -92,14 +94,78 @@
|
||||
</ng-template>
|
||||
|
||||
<div *ngIf="donationStatus === 3" class="text-center">
|
||||
<div class="qr-wrapper mt-2 mb-2">
|
||||
<a [href]="bypassSecurityTrustUrl('bitcoin:' + donationObj.address + '?amount=' + donationObj.amount)" target="_blank">
|
||||
<app-qrcode [data]="'bitcoin:' + donationObj.address + '?amount=' + donationObj.amount"></app-qrcode>
|
||||
</a>
|
||||
</div>
|
||||
<br>
|
||||
<p style="font-size: 10px;">{{ donationObj.address }}</p>
|
||||
<p style="font-size: 12px;">{{ donationObj.amount }} BTC</p>
|
||||
|
||||
<form [formGroup]="paymentForm">
|
||||
<div class="btn-group btn-group-toggle mb-2" ngbRadioGroup formControlName="method">
|
||||
<label ngbButtonLabel class="btn-primary">
|
||||
<input ngbButton type="radio" value="chain"> <fa-icon [icon]="['fas', 'link']" [fixedWidth]="true" title="Onchain"></fa-icon>
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary" *ngIf="donationObj.addresses.BTC_LightningLike">
|
||||
<input ngbButton type="radio" value="lightning"> <fa-icon [icon]="['fas', 'bolt']" [fixedWidth]="true" title="Lightning"></fa-icon>
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary" *ngIf="donationObj.addresses.LBTC">
|
||||
<input ngbButton type="radio" value="lbtc"> <fa-icon [icon]="['fas', 'tint']" [fixedWidth]="true" title="Liquid Bitcoin"></fa-icon>
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<ng-template [ngIf]="paymentForm.get('method').value === 'chain'">
|
||||
<div class="qr-wrapper mt-2 mb-2">
|
||||
<a [href]="bypassSecurityTrustUrl('bitcoin:' + donationObj.addresses.BTC + '?amount=' + donationObj.amount)" target="_blank">
|
||||
<app-qrcode imageUrl="./resources/bitcoin-logo.png" [size]="200" [data]="'bitcoin:' + donationObj.addresses.BTC + '?amount=' + donationObj.amount"></app-qrcode>
|
||||
</a>
|
||||
</div>
|
||||
<br>
|
||||
<div class="input-group input-group-sm mb-3 mt-3 info-group mx-auto">
|
||||
<input type="text" class="form-control" readonly [value]="donationObj.addresses.BTC">
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-outline-secondary" type="button" ><app-clipboard [text]="donationObj.addresses.BTC"></app-clipboard></button>
|
||||
</div>
|
||||
</div>
|
||||
<p style="font-size: 12px;">{{ donationObj.amount }} BTC</p>
|
||||
</ng-template>
|
||||
|
||||
<ng-template [ngIf]="paymentForm.get('method').value === 'lightning'">
|
||||
<div class="qr-wrapper mt-2 mb-2">
|
||||
<a [href]="bypassSecurityTrustUrl('lightning:' + donationObj.addresses.BTC_LightningLike)" target="_blank">
|
||||
<app-qrcode imageUrl="./resources/bitcoin-logo.png" [size]="200" [data]="donationObj.addresses.BTC_LightningLike.toUpperCase()"></app-qrcode>
|
||||
</a>
|
||||
</div>
|
||||
<br>
|
||||
|
||||
<div class="input-group input-group-sm mb-3 mt-3 info-group mx-auto">
|
||||
<input type="text" class="form-control" readonly [value]="donationObj.addresses.BTC_LightningLike">
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-outline-secondary" type="button"><app-clipboard [text]="donationObj.addresses.BTC_LightningLike"></app-clipboard></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input-group input-group-sm mb-3 mt-3 info-group mx-auto">
|
||||
<input type="text" class="form-control" readonly value="0334ac407769a00334afac4231a6e4c0faa31328b67b42c0c59e722e083ed5e6cf@103.99.170.180:9735">
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-outline-secondary" type="button"><app-clipboard [text]="'0334ac407769a00334afac4231a6e4c0faa31328b67b42c0c59e722e083ed5e6cf@103.99.170.180:9735'"></app-clipboard></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p style="font-size: 10px;"></p>
|
||||
<p style="font-size: 12px;">{{ donationObj.amount }} BTC</p>
|
||||
</ng-template>
|
||||
|
||||
<ng-template [ngIf]="paymentForm.get('method').value === 'lbtc'">
|
||||
<div class="qr-wrapper mt-2 mb-2">
|
||||
<a [href]="bypassSecurityTrustUrl('liquidnetwork:' + donationObj.addresses.LBTC + '?amount=' + donationObj.amount + '&assetid=6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d')" target="_blank">
|
||||
<app-qrcode imageUrl="./resources/liquid-bitcoin.png" [size]="200" [data]="'liquidnetwork:' + donationObj.addresses.LBTC + '?amount=' + donationObj.amount + '&assetid=6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d'"></app-qrcode>
|
||||
</a>
|
||||
</div>
|
||||
<br>
|
||||
<div class="input-group input-group-sm mb-3 mt-3 info-group mx-auto">
|
||||
<input type="text" class="form-control" readonly [value]="donationObj.addresses.LBTC">
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-outline-secondary" type="button" ><app-clipboard [text]="donationObj.addresses.LBTC"></app-clipboard></button>
|
||||
</div>
|
||||
</div>
|
||||
<p style="font-size: 12px;">{{ donationObj.amount }} BTC</p>
|
||||
</ng-template>
|
||||
|
||||
<p>Waiting for transaction... </p>
|
||||
<div class="spinner-border text-light"></div>
|
||||
|
@ -2,6 +2,7 @@
|
||||
background-color: #FFF;
|
||||
padding: 10px;
|
||||
display: inline-block;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.profile_photo {
|
||||
@ -22,3 +23,13 @@
|
||||
.text-small {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.info-group {
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
|
||||
.required {
|
||||
color: #FF0000;
|
||||
font-weight: bold;
|
||||
}
|
@ -3,7 +3,7 @@ import { WebsocketService } from '../../services/websocket.service';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { StateService } from 'src/app/services/state.service';
|
||||
import { Observable } from 'rxjs';
|
||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { ApiService } from 'src/app/services/api.service';
|
||||
import { env } from '../../app.constants';
|
||||
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
|
||||
@ -17,6 +17,7 @@ import { map } from 'rxjs/operators';
|
||||
export class AboutComponent implements OnInit {
|
||||
gitCommit$: Observable<string>;
|
||||
donationForm: FormGroup;
|
||||
paymentForm: FormGroup;
|
||||
donationStatus = 1;
|
||||
sponsors$: Observable<any>;
|
||||
donationObj: any;
|
||||
@ -38,10 +39,14 @@ export class AboutComponent implements OnInit {
|
||||
this.websocketService.want(['blocks']);
|
||||
|
||||
this.donationForm = this.formBuilder.group({
|
||||
amount: [0.01],
|
||||
amount: [0.01, [Validators.min(0.001), Validators.required]],
|
||||
handle: [''],
|
||||
});
|
||||
|
||||
this.paymentForm = this.formBuilder.group({
|
||||
'method': 'chain'
|
||||
});
|
||||
|
||||
this.apiService.getDonation$()
|
||||
.subscribe((sponsors) => {
|
||||
this.sponsors = sponsors;
|
||||
|
@ -43,7 +43,7 @@
|
||||
<tbody>
|
||||
<tr *ngIf="block.medianFee !== undefined">
|
||||
<td class="td-width">Median fee</td>
|
||||
<td>~{{ block.medianFee | number:'1.0-0' }} sat/vB (<app-fiat [value]="block.medianFee * 250" digitsInfo="1.2-2"></app-fiat>)</td>
|
||||
<td>~{{ block.medianFee | number:'1.0-0' }} sat/vB (<app-fiat [value]="block.medianFee * 140" digitsInfo="1.2-2" ngbTooltip="Based on average native segwit transaction of 140 vBytes" placement="bottom"></app-fiat>)</td>
|
||||
</tr>
|
||||
<ng-template [ngIf]="fees !== undefined" [ngIfElse]="loadingFees">
|
||||
<tr>
|
||||
|
@ -3,19 +3,19 @@
|
||||
<td class="d-none d-md-block">
|
||||
<h5 class="card-title">Low priority</h5>
|
||||
<p class="card-text">
|
||||
{{ feeEstimations.hourFee }} sat/vB (<app-fiat [value]="feeEstimations.hourFee * 250"></app-fiat>)
|
||||
{{ feeEstimations.hourFee }} sat/vB (<app-fiat [value]="feeEstimations.hourFee * 140" ngbTooltip="Based on average native segwit transaction of 140 vBytes" placement="bottom"></app-fiat>)
|
||||
</p>
|
||||
</td>
|
||||
<td>
|
||||
<h5 class="card-title">Medium priority</h5>
|
||||
<p class="card-text">
|
||||
{{ feeEstimations.halfHourFee }} sat/vB (<app-fiat [value]="feeEstimations.halfHourFee * 250"></app-fiat>)
|
||||
{{ feeEstimations.halfHourFee }} sat/vB (<app-fiat [value]="feeEstimations.halfHourFee * 140" ngbTooltip="Based on average native segwit transaction of 140 vBytes" placement="bottom"></app-fiat>)
|
||||
</p>
|
||||
</td>
|
||||
<td>
|
||||
<h5 class="card-title">High priority</h5>
|
||||
<p class="card-text">
|
||||
{{ feeEstimations.fastestFee }} sat/vB (<app-fiat [value]="feeEstimations.fastestFee * 250"></app-fiat>)
|
||||
{{ feeEstimations.fastestFee }} sat/vB (<app-fiat [value]="feeEstimations.fastestFee * 140" ngbTooltip="Based on average native segwit transaction of 140 vBytes" placement="bottom"></app-fiat>)
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -14,7 +14,7 @@
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Median fee</td>
|
||||
<td>~{{ mempoolBlock.medianFee | number:'1.0-0' }} sat/vB (<app-fiat [value]="mempoolBlock.medianFee * 250" digitsInfo="1.2-2"></app-fiat>)</td>
|
||||
<td>~{{ mempoolBlock.medianFee | number:'1.0-0' }} sat/vB (<app-fiat [value]="mempoolBlock.medianFee * 140" digitsInfo="1.2-2" ngbTooltip="Based on average native segwit transaction of 140 vBytes" placement="bottom"></app-fiat>)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Fee span</td>
|
||||
|
@ -30,9 +30,13 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
|
||||
|
||||
ngOnInit(): void {
|
||||
const showLegend = !this.isMobile && this.showLegend;
|
||||
let labelHops = !this.showLegend ? 12 : 6;
|
||||
let labelHops = !this.showLegend ? 48 : 24;
|
||||
if (this.small) {
|
||||
labelHops = labelHops * 2;
|
||||
labelHops = labelHops / 2;
|
||||
}
|
||||
|
||||
if (this.isMobile) {
|
||||
labelHops = 96;
|
||||
}
|
||||
|
||||
const labelInterpolationFnc = (value: any, index: any) => {
|
||||
@ -50,7 +54,7 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
|
||||
case '1y':
|
||||
value = formatDate(value, 'dd/MM', this.locale);
|
||||
}
|
||||
return index % labelHops === 0 ? value : null;
|
||||
return index % labelHops === 0 ? value : null;
|
||||
};
|
||||
|
||||
this.mempoolVsizeFeesOptions = {
|
||||
@ -65,7 +69,7 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
|
||||
},
|
||||
axisY: {
|
||||
labelInterpolationFnc: (value: number): any => this.vbytesPipe.transform(value, 2),
|
||||
offset: showLegend ? 160 : 80,
|
||||
offset: showLegend ? 160 : 60,
|
||||
},
|
||||
plugins: [
|
||||
Chartist.plugins.ctTargetLine({
|
||||
|
@ -1 +1,4 @@
|
||||
<canvas #canvas></canvas>
|
||||
<div class="holder" [ngStyle]="{'width': size, 'height': size}">
|
||||
<img *ngIf="imageUrl" [src]="imageUrl">
|
||||
<canvas #canvas></canvas>
|
||||
</div>
|
||||
|
@ -0,0 +1,11 @@
|
||||
img {
|
||||
position: absolute;
|
||||
top: 67px;
|
||||
left: 67px;
|
||||
width: 65px;
|
||||
height: 65px;
|
||||
}
|
||||
|
||||
.holder {
|
||||
position: relative;
|
||||
}
|
@ -8,6 +8,8 @@ import * as QRCode from 'qrcode/build/qrcode.js';
|
||||
})
|
||||
export class QrcodeComponent implements AfterViewInit {
|
||||
@Input() data: string;
|
||||
@Input() size = 125;
|
||||
@Input() imageUrl: string;
|
||||
@ViewChild('canvas') canvas: ElementRef;
|
||||
|
||||
qrcodeObject: any;
|
||||
@ -22,8 +24,8 @@ export class QrcodeComponent implements AfterViewInit {
|
||||
dark: '#000',
|
||||
light: '#fff'
|
||||
},
|
||||
width: 125,
|
||||
height: 125,
|
||||
width: this.size,
|
||||
height: this.size,
|
||||
};
|
||||
|
||||
if (!this.data) {
|
||||
|
@ -53,7 +53,11 @@ export class StatisticsComponent implements OnInit {
|
||||
this.seoService.setTitle('Graphs');
|
||||
this.stateService.networkChanged$.subscribe((network) => this.network = network);
|
||||
const isMobile = window.innerWidth <= 767.98;
|
||||
const labelHops = isMobile ? 12 : 6;
|
||||
let labelHops = isMobile ? 48 : 24;
|
||||
|
||||
if (isMobile) {
|
||||
labelHops = 96;
|
||||
}
|
||||
|
||||
const labelInterpolationFnc = (value: any, index: any) => {
|
||||
switch (this.radioGroupForm.controls.dateSpan.value) {
|
||||
|
@ -1,4 +1,8 @@
|
||||
<span *ngIf="segwitGains.realizedGains && !segwitGains.potentialBech32Gains" class="badge badge-success mr-1" ngbTooltip="This transaction saved {{ segwitGains.realizedGains * 100 | number: '1.0-0' }}% on fees by using native SegWit-Bech32" placement="bottom">SegWit</span>
|
||||
<span *ngIf="segwitGains.realizedGains && segwitGains.potentialBech32Gains" class="badge badge-warning mr-1" ngbTooltip="This transaction saved {{ segwitGains.realizedGains * 100 | number: '1.0-0' }}% on fees by using SegWit and could save {{ segwitGains.potentialBech32Gains * 100 | number : '1.0-0' }}% more by fully upgrading to native SegWit-Bech32" placement="bottom">SegWit</span>
|
||||
<span *ngIf="segwitGains.potentialP2shGains" class="badge badge-danger mr-1" ngbTooltip="This transaction could save {{ segwitGains.potentialBech32Gains * 100 | number : '1.0-0' }}% on fees by upgrading to native SegWit-Bech32 or {{ segwitGains.potentialP2shGains * 100 | number: '1.0-0' }}% by upgrading to SegWit-P2SH" placement="bottom"><del>SegWit</del></span>
|
||||
<span *ngIf="segwitGains.realizedGains && !segwitGains.potentialBech32Gains; else segwitTwo" class="badge badge-success mr-1" ngbTooltip="This transaction saved {{ segwitGains.realizedGains * 100 | number: '1.0-0' }}% on fees by using native SegWit-Bech32" placement="bottom">SegWit</span>
|
||||
<ng-template #segwitTwo>
|
||||
<span *ngIf="segwitGains.realizedGains && segwitGains.potentialBech32Gains else potentialP2shGains" class="badge badge-warning mr-1" ngbTooltip="This transaction saved {{ segwitGains.realizedGains * 100 | number: '1.0-0' }}% on fees by using SegWit and could save {{ segwitGains.potentialBech32Gains * 100 | number : '1.0-0' }}% more by fully upgrading to native SegWit-Bech32" placement="bottom">SegWit</span>
|
||||
<ng-template #potentialP2shGains>
|
||||
<span *ngIf="segwitGains.potentialP2shGains" class="badge badge-danger mr-1" ngbTooltip="This transaction could save {{ segwitGains.potentialBech32Gains * 100 | number : '1.0-0' }}% on fees by upgrading to native SegWit-Bech32 or {{ segwitGains.potentialP2shGains * 100 | number: '1.0-0' }}% by upgrading to SegWit-P2SH" placement="bottom"><del>SegWit</del></span>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
<span *ngIf="isRbfTransaction" class="badge badge-success" ngbTooltip="This transaction support Replace-By-Fee (RBF) allowing fee bumping" placement="bottom">RBF</span>
|
||||
|
@ -2,67 +2,60 @@
|
||||
<div class="container-xl mt-2">
|
||||
|
||||
<div class="row row-cols-1 row-cols-md-2" *ngIf="{ value: (mempoolInfoData$ | async) } as mempoolInfoData">
|
||||
<div class="col mb-4">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<app-fees-box class="d-block"></app-fees-box>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col mb-4" *ngIf="(network$ | async) !== 'liquid'; else emptyBlock">
|
||||
<div class="card text-center">
|
||||
<div class="card-body more-padding">
|
||||
<h5 class="card-title">Difficulty adjustment</h5>
|
||||
<div class="progress" *ngIf="(difficultyEpoch$ | async) as epochData; else loading">
|
||||
<div class="progress-bar" role="progressbar" style="width: 15%; background-color: #105fb0" [ngStyle]="{'width': epochData.base}"><ng-template [ngIf]="epochData.change > 0">+</ng-template>{{ epochData.change | number: '1.0-2' }}%</div>
|
||||
<div class="progress-bar bg-success" role="progressbar" style="width: 0%" [ngStyle]="{'width': epochData.green}"></div>
|
||||
<div class="progress-bar bg-danger" role="progressbar" style="width: 1%; background-color: #f14d80;" [ngStyle]="{'width': epochData.red}"></div>
|
||||
<ng-template [ngIf]="collapseLevel === 'three'" [ngIfElse]="expanded">
|
||||
<div class="col mb-4">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<app-fees-box class="d-block"></app-fees-box>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col mb-4">
|
||||
<div class="card text-center graph-card">
|
||||
<div class="card-body pl-0">
|
||||
<div style="padding-left: 1.25rem;">
|
||||
<table style="width: 100%;">
|
||||
<tr>
|
||||
<td>
|
||||
<h5 class="card-title">Mempool size</h5>
|
||||
<p class="card-text" *ngIf="(mempoolBlocksData$ | async) as mempoolBlocksData; else loading">
|
||||
{{ mempoolBlocksData.size | bytes }} ({{ mempoolBlocksData.blocks }} block<span [hidden]="mempoolBlocksData.blocks <= 1">s</span>)
|
||||
</p>
|
||||
</td>
|
||||
<td>
|
||||
<h5 class="card-title">Unconfirmed</h5>
|
||||
<p class="card-text" *ngIf="mempoolInfoData.value; else loading">
|
||||
{{ mempoolInfoData.value.memPoolInfo.size | number }} TXs
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<hr>
|
||||
</div>
|
||||
<div style="height: 250px;" *ngIf="(mempoolStats$ | async) as mempoolStats">
|
||||
<app-mempool-graph [data]="mempoolStats.mempool" [showLegend]="false" [offsetX]="10" [small]="true"></app-mempool-graph>
|
||||
<div class="col mb-4">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<ng-container *ngTemplateOutlet="mempoolTable; context: { $implicit: mempoolInfoData }"></ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col mb-4">
|
||||
<div class="card text-center graph-card">
|
||||
<div class="card-body ">
|
||||
<h5 class="card-title">Tx vBytes per second</h5>
|
||||
<ng-template [ngIf]="mempoolInfoData.value" [ngIfElse]="loading">
|
||||
<span *ngIf="mempoolInfoData.value.vBytesPerSecond === 0; else inSync">
|
||||
<span class="badge badge-pill badge-warning">Backend is synchronizing</span>
|
||||
</span>
|
||||
<ng-template #inSync>
|
||||
<div class="progress sub-text" style="max-width: 250px;">
|
||||
<div class="progress-bar {{ mempoolInfoData.value.progressClass }}" style="padding: 4px;" role="progressbar" [ngStyle]="{'width': mempoolInfoData.value.progressWidth}">{{ mempoolInfoData.value.vBytesPerSecond | ceil | number }} vB/s</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
<div class="col mb-4">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<ng-container *ngTemplateOutlet="txPerSecond; context: { $implicit: mempoolInfoData }"></ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col mb-4" *ngIf="(network$ | async) !== 'liquid'; else emptyBlock">
|
||||
<ng-container *ngTemplateOutlet="difficultyEpoch"></ng-container>
|
||||
</div>
|
||||
</ng-template>
|
||||
<ng-template #expanded>
|
||||
<div class="col mb-4">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<app-fees-box class="d-block"></app-fees-box>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col mb-4" *ngIf="(network$ | async) !== 'liquid'; else emptyBlock">
|
||||
<ng-container *ngTemplateOutlet="difficultyEpoch"></ng-container>
|
||||
</div>
|
||||
<div class="col mb-4">
|
||||
<div class="card text-center graph-card">
|
||||
<div class="card-body pl-0">
|
||||
<div style="padding-left: 1.25rem;">
|
||||
<ng-container *ngTemplateOutlet="mempoolTable; context: { $implicit: mempoolInfoData }"></ng-container>
|
||||
<hr>
|
||||
</div>
|
||||
<div style="height: 250px;" *ngIf="(mempoolStats$ | async) as mempoolStats">
|
||||
<app-mempool-graph [data]="mempoolStats.mempool" [showLegend]="false" [offsetX]="20" [small]="true"></app-mempool-graph>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col mb-4">
|
||||
<div class="card text-center graph-card">
|
||||
<div class="card-body">
|
||||
<ng-container *ngTemplateOutlet="txPerSecond; context: { $implicit: mempoolInfoData }"></ng-container>
|
||||
<br>
|
||||
<hr>
|
||||
<div style="height: 250px;" *ngIf="(mempoolStats$ | async) as mempoolStats">
|
||||
@ -72,67 +65,75 @@
|
||||
[options]="transactionsWeightPerSecondOptions">
|
||||
</app-chartist>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col mb-4">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Latest blocks</h5>
|
||||
<table class="table table-borderless text-left">
|
||||
<thead>
|
||||
<th style="width: 15%;">Height</th>
|
||||
<th style="width: 35%;">Mined</th>
|
||||
<th style="width: 20%;" class="d-none d-lg-table-cell">Txs</th>
|
||||
<th style="width: 30%;">Filled</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let block of blocks$ | async; let i = index; trackBy: trackByBlock">
|
||||
<td><a [routerLink]="['/block' | relativeUrl, block.id]" [state]="{ data: { block: block } }">{{ block.height }}</a></td>
|
||||
<td><app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since> ago</td>
|
||||
<td class="d-none d-lg-table-cell">{{ block.tx_count | number }}</td>
|
||||
<td>
|
||||
<div class="progress position-relative">
|
||||
<div class="progress-bar progress-mempool {{ network$ | async }}" role="progressbar" [ngStyle]="{'width': (block.weight / 4000000)*100 + '%' }"></div>
|
||||
<div class="progress-text">{{ block.size | bytes: 2 }}</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="text-center"><a href="" [routerLink]="['/blocks' | relativeUrl]">View all »</a></div>
|
||||
<ng-template [ngIf]="collapseLevel === 'one'">
|
||||
<div class="col mb-4">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Latest blocks</h5>
|
||||
<table class="table table-borderless text-left">
|
||||
<thead>
|
||||
<th style="width: 15%;">Height</th>
|
||||
<th style="width: 35%;">Mined</th>
|
||||
<th style="width: 20%;" class="d-none d-lg-table-cell">Txs</th>
|
||||
<th style="width: 30%;">Filled</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let block of blocks$ | async; let i = index; trackBy: trackByBlock">
|
||||
<td><a [routerLink]="['/block' | relativeUrl, block.id]" [state]="{ data: { block: block } }">{{ block.height }}</a></td>
|
||||
<td><app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since> ago</td>
|
||||
<td class="d-none d-lg-table-cell">{{ block.tx_count | number }}</td>
|
||||
<td>
|
||||
<div class="progress position-relative">
|
||||
<div class="progress-bar progress-mempool {{ network$ | async }}" role="progressbar" [ngStyle]="{'width': (block.weight / 4000000)*100 + '%' }"></div>
|
||||
<div class="progress-text">{{ block.size | bytes: 2 }}</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="text-center"><a href="" [routerLink]="['/blocks' | relativeUrl]">View all »</a></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col mb-4">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Latest transactions</h5>
|
||||
<table class="table table-borderless text-left">
|
||||
<thead>
|
||||
<th style="width: 20%;">TXID</th>
|
||||
<th style="width: 35%;" class="text-right d-none d-lg-table-cell">Amount</th>
|
||||
<th *ngIf="(network$ | async) === ''" style="width: 20%;" class="text-right d-none d-lg-table-cell">USD</th>
|
||||
<th style="width: 25%;" class="text-right">Fee</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let transaction of transactions$ | async; let i = index;">
|
||||
<td><a [routerLink]="['/tx' | relativeUrl, transaction.txid]">{{ transaction.txid | shortenString : 10 }}</a></td>
|
||||
<td class="text-right d-none d-lg-table-cell"><app-amount [satoshis]="transaction.value" digitsInfo="1.8-8" [noFiat]="true"></app-amount></td>
|
||||
<td *ngIf="(network$ | async) === ''" class="text-right d-none d-lg-table-cell"><app-fiat [value]="transaction.value" digitsInfo="1.0-0"></app-fiat></td>
|
||||
<td class="text-right">{{ transaction.fee / (transaction.weight / 4) | number : '1.1-1' }} sat/vB</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="text-center"> </div>
|
||||
<div class="col mb-4">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Latest transactions</h5>
|
||||
<table class="table table-borderless text-left">
|
||||
<thead>
|
||||
<th style="width: 20%;">TXID</th>
|
||||
<th style="width: 35%;" class="text-right d-none d-lg-table-cell">Amount</th>
|
||||
<th *ngIf="(network$ | async) === ''" style="width: 20%;" class="text-right d-none d-lg-table-cell">USD</th>
|
||||
<th style="width: 25%;" class="text-right">Fee</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let transaction of transactions$ | async; let i = index;">
|
||||
<td><a [routerLink]="['/tx' | relativeUrl, transaction.txid]">{{ transaction.txid | shortenString : 10 }}</a></td>
|
||||
<td class="text-right d-none d-lg-table-cell"><app-amount [satoshis]="transaction.value" digitsInfo="1.8-8" [noFiat]="true"></app-amount></td>
|
||||
<td *ngIf="(network$ | async) === ''" class="text-right d-none d-lg-table-cell"><app-fiat [value]="transaction.value" digitsInfo="1.0-0"></app-fiat></td>
|
||||
<td class="text-right">{{ transaction.fee / (transaction.weight / 4) | number : '1.1-1' }} sat/vB</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="text-center"> </div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
<button type="button" class="btn btn-secondary btn-sm d-block mx-auto" (click)="toggleCollapsed()">
|
||||
<div [ngSwitch]="collapseLevel">
|
||||
<fa-icon *ngSwitchCase="'three'" [icon]="['fas', 'angle-down']" [fixedWidth]="true" title="Collapse"></fa-icon>
|
||||
<fa-icon *ngSwitchDefault [icon]="['fas', 'angle-up']" [fixedWidth]="true" title="Expand"></fa-icon>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div class="text-small text-center">
|
||||
<div class="text-small text-center mt-3">
|
||||
<a [routerLink]="['/terms-of-service']">Terms of Service</a>
|
||||
</div>
|
||||
|
||||
@ -147,3 +148,49 @@
|
||||
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #mempoolTable let-mempoolInfoData>
|
||||
<table style="width: 100%;">
|
||||
<tr>
|
||||
<td>
|
||||
<h5 class="card-title">Mempool size</h5>
|
||||
<p class="card-text" *ngIf="(mempoolBlocksData$ | async) as mempoolBlocksData; else loading">
|
||||
{{ mempoolBlocksData.size | bytes }} ({{ mempoolBlocksData.blocks }} block<span [hidden]="mempoolBlocksData.blocks <= 1">s</span>)
|
||||
</p>
|
||||
</td>
|
||||
<td>
|
||||
<h5 class="card-title">Unconfirmed</h5>
|
||||
<p class="card-text" *ngIf="mempoolInfoData.value; else loading">
|
||||
{{ mempoolInfoData.value.memPoolInfo.size | number }} TXs
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #txPerSecond let-mempoolInfoData>
|
||||
<h5 class="card-title">Incoming transactions</h5>
|
||||
<ng-template [ngIf]="mempoolInfoData.value" [ngIfElse]="loading">
|
||||
<span *ngIf="mempoolInfoData.value.vBytesPerSecond === 0; else inSync">
|
||||
<span class="badge badge-pill badge-warning">Backend is synchronizing</span>
|
||||
</span>
|
||||
<ng-template #inSync>
|
||||
<div class="progress sub-text" style="max-width: 250px;">
|
||||
<div class="progress-bar {{ mempoolInfoData.value.progressClass }}" style="padding: 4px;" role="progressbar" [ngStyle]="{'width': mempoolInfoData.value.progressWidth}">{{ mempoolInfoData.value.vBytesPerSecond | ceil | number }} vB/s</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #difficultyEpoch>
|
||||
<div class="card text-center">
|
||||
<div class="card-body more-padding">
|
||||
<h5 class="card-title">Difficulty adjustment</h5>
|
||||
<div class="progress" *ngIf="(difficultyEpoch$ | async) as epochData; else loading">
|
||||
<div class="progress-bar" role="progressbar" style="width: 15%; background-color: #105fb0" [ngStyle]="{'width': epochData.base}"><ng-template [ngIf]="epochData.change > 0">+</ng-template>{{ epochData.change | number: '1.0-2' }}%</div>
|
||||
<div class="progress-bar bg-success" role="progressbar" style="width: 0%" [ngStyle]="{'width': epochData.green}"></div>
|
||||
<div class="progress-bar bg-danger" role="progressbar" style="width: 1%; background-color: #f14d80;" [ngStyle]="{'width': epochData.red}"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
@ -10,6 +10,7 @@ import * as Chartist from 'chartist';
|
||||
import { formatDate } from '@angular/common';
|
||||
import { WebsocketService } from '../services/websocket.service';
|
||||
import { SeoService } from '../services/seo.service';
|
||||
import { StorageService } from '../services/storage.service';
|
||||
|
||||
interface MempoolBlocksData {
|
||||
blocks: number;
|
||||
@ -42,6 +43,7 @@ interface MempoolStatsData {
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class DashboardComponent implements OnInit {
|
||||
collapseLevel: string;
|
||||
network$: Observable<string>;
|
||||
mempoolBlocksData$: Observable<MempoolBlocksData>;
|
||||
mempoolInfoData$: Observable<MempoolInfoData>;
|
||||
@ -60,12 +62,14 @@ export class DashboardComponent implements OnInit {
|
||||
private apiService: ApiService,
|
||||
private websocketService: WebsocketService,
|
||||
private seoService: SeoService,
|
||||
private storageService: StorageService,
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.seoService.resetTitle();
|
||||
this.websocketService.want(['blocks', 'stats', 'mempool-blocks', 'live-2h-chart']);
|
||||
this.network$ = merge(of(''), this.stateService.networkChanged$);
|
||||
this.collapseLevel = this.storageService.getValue('dashboard-collapsed') || 'one';
|
||||
|
||||
this.mempoolInfoData$ = combineLatest([
|
||||
this.stateService.mempoolInfo$,
|
||||
@ -194,7 +198,7 @@ export class DashboardComponent implements OnInit {
|
||||
},
|
||||
axisX: {
|
||||
labelInterpolationFnc: (value: any, index: any) => index % 24 === 0 ? formatDate(value, 'HH:mm', this.locale) : null,
|
||||
offset: 10
|
||||
offset: 20
|
||||
},
|
||||
plugins: [
|
||||
Chartist.plugins.ctTargetLine({
|
||||
@ -217,4 +221,15 @@ export class DashboardComponent implements OnInit {
|
||||
trackByBlock(index: number, block: Block) {
|
||||
return block.height;
|
||||
}
|
||||
|
||||
toggleCollapsed() {
|
||||
if (this.collapseLevel === 'one') {
|
||||
this.collapseLevel = 'two';
|
||||
} else if (this.collapseLevel === 'two') {
|
||||
this.collapseLevel = 'three';
|
||||
} else {
|
||||
this.collapseLevel = 'one';
|
||||
}
|
||||
this.storageService.setValue('dashboard-collapsed', this.collapseLevel);
|
||||
}
|
||||
}
|
||||
|
22
frontend/src/app/services/storage.service.ts
Normal file
22
frontend/src/app/services/storage.service.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class StorageService {
|
||||
getValue(key: string): string {
|
||||
try {
|
||||
return localStorage.getItem(key);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
setValue(key: string, value: any): void {
|
||||
try {
|
||||
localStorage.setItem(key, value);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 25 KiB |
Binary file not shown.
Before Width: | Height: | Size: 26 KiB |
BIN
frontend/src/resources/liquid-bitcoin.png
Normal file
BIN
frontend/src/resources/liquid-bitcoin.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.0 KiB |
@ -10,13 +10,13 @@ do
|
||||
cp "../production/mempool-config.${site}.json" "mempool-config.json"
|
||||
touch cache.json
|
||||
|
||||
npm install --only=prod
|
||||
npm install
|
||||
npm run build
|
||||
|
||||
if [ "${site}" = "mainnet" ]
|
||||
then
|
||||
cd "${HOME}/${site}/frontend/"
|
||||
npm install --only=prod
|
||||
npm install
|
||||
npm run build
|
||||
rsync -av ./dist/mempool/* "${HOME}/public_html/${site}/"
|
||||
fi
|
||||
|
@ -34,13 +34,13 @@ do
|
||||
if [ "${site}" = "mainnet" ]
|
||||
then
|
||||
cd "$HOME/${site}/frontend"
|
||||
npm install --only=prod
|
||||
npm install
|
||||
npm run build
|
||||
rsync -av ./dist/mempool/* "${HOME}/public_html/${site}/"
|
||||
fi
|
||||
|
||||
cd "$HOME/${site}/backend"
|
||||
npm install --only=prod
|
||||
npm install
|
||||
npm run build
|
||||
done
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user