Merge branch 'master' into hunicus/enterprise-cta-docs
This commit is contained in:
@@ -41,6 +41,7 @@ export class BisqAddressComponent implements OnInit, OnDestroy {
|
||||
document.body.scrollTo(0, 0);
|
||||
this.addressString = params.get('id') || '';
|
||||
this.seoService.setTitle($localize`:@@bisq-address.component.browser-title:Address: ${this.addressString}:INTERPOLATION:`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.bisq.address:See current balance, pending transactions, and history of confirmed transactions for BSQ address ${this.addressString}:INTERPOLATION:.`);
|
||||
|
||||
return this.bisqApiService.getAddress$(this.addressString)
|
||||
.pipe(
|
||||
|
||||
@@ -88,6 +88,7 @@ export class BisqBlockComponent implements OnInit, OnDestroy {
|
||||
this.isLoading = false;
|
||||
this.blockHeight = block.height;
|
||||
this.seoService.setTitle($localize`:@@bisq-block.component.browser-title:Block ${block.height}:BLOCK_HEIGHT:: ${block.hash}:BLOCK_HASH:`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.bisq.block:See all BSQ transactions in Bitcoin block ${block.height}:BLOCK_HEIGHT: (block hash ${block.hash}:BLOCK_HASH:).`);
|
||||
this.block = block;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ export class BisqBlocksComponent implements OnInit {
|
||||
ngOnInit(): void {
|
||||
this.websocketService.want(['blocks']);
|
||||
this.seoService.setTitle($localize`:@@8a7b4bd44c0ac71b2e72de0398b303257f7d2f54:Blocks`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.bisq.blocks:See a list of recent Bitcoin blocks with BSQ transactions, total BSQ sent per block, and more.`);
|
||||
this.itemsPerPage = Math.max(Math.round(this.contentSpace / this.fiveItemsPxSize) * 5, 10);
|
||||
this.loadingItems = Array(this.itemsPerPage);
|
||||
if (document.body.clientWidth < 670) {
|
||||
|
||||
@@ -29,7 +29,8 @@ export class BisqDashboardComponent implements OnInit {
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.seoService.setTitle(`Markets`);
|
||||
this.seoService.setTitle($localize`:@@meta.title.bisq.markets:Markets`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.bisq.markets:Explore the full Bitcoin ecosystem with The Mempool Open Project™. See Bisq market prices, trading activity, and more.`);
|
||||
this.websocketService.want(['blocks']);
|
||||
|
||||
this.volumes$ = this.bisqApiService.getAllVolumesDay$()
|
||||
|
||||
@@ -34,6 +34,7 @@ export class BisqMainDashboardComponent implements OnInit {
|
||||
|
||||
ngOnInit(): void {
|
||||
this.seoService.resetTitle();
|
||||
this.seoService.resetDescription();
|
||||
this.websocketService.want(['blocks']);
|
||||
|
||||
this.usdPrice$ = this.stateService.conversions$.asObservable().pipe(
|
||||
|
||||
@@ -48,7 +48,8 @@ export class BisqMarketComponent implements OnInit, OnDestroy {
|
||||
map(([markets, routeParams]) => {
|
||||
const pair = routeParams.get('pair');
|
||||
const pairUpperCase = pair.replace('_', '/').toUpperCase();
|
||||
this.seoService.setTitle(`Bisq market: ${pairUpperCase}`);
|
||||
this.seoService.setTitle($localize`:@@meta.title.bisq.market:Bisq market: ${pairUpperCase}`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.bisq.market:See price history, current buy/sell offers, and latest trades for the ${pairUpperCase} market on Bisq.`);
|
||||
|
||||
return {
|
||||
pair: pairUpperCase,
|
||||
|
||||
@@ -26,6 +26,7 @@ export class BisqStatsComponent implements OnInit {
|
||||
this.websocketService.want(['blocks']);
|
||||
|
||||
this.seoService.setTitle($localize`:@@2a30a4cdb123a03facc5ab8c5b3e6d8b8dbbc3d4:BSQ statistics`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.bisq.stats:See high-level stats on the BSQ economy: supply metrics, number of addresses, BSQ price, market cap, and more.`);
|
||||
this.stateService.bsqPrice$
|
||||
.subscribe((bsqPrice) => {
|
||||
this.price = bsqPrice;
|
||||
|
||||
@@ -48,6 +48,7 @@ export class BisqTransactionComponent implements OnInit, OnDestroy {
|
||||
document.body.scrollTo(0, 0);
|
||||
this.txId = params.get('id') || '';
|
||||
this.seoService.setTitle($localize`:@@bisq.transaction.browser-title:Transaction: ${this.txId}:INTERPOLATION:`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.bisq.transaction:See inputs, outputs, transaction type, burnt amount, and more for transaction with txid ${this.txId}:INTERPOLATION:.`);
|
||||
if (history.state.data) {
|
||||
return of(history.state.data);
|
||||
}
|
||||
|
||||
@@ -79,6 +79,7 @@ export class BisqTransactionsComponent implements OnInit, OnDestroy {
|
||||
ngOnInit(): void {
|
||||
this.websocketService.want(['blocks']);
|
||||
this.seoService.setTitle($localize`:@@add4cd82e3e38a3110fe67b3c7df56e9602644ee:Transactions`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.bisq.transactions:See recent BSQ transactions: amount, txid, associated Bitcoin block, transaction type, and more.`);
|
||||
|
||||
this.radioGroupForm = this.formBuilder.group({
|
||||
txTypes: [this.txTypesDefaultChecked],
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
<span style="margin-left: auto; margin-right: -20px; margin-bottom: -20px">®</span>
|
||||
<img class="logo" src="/resources/mempool-logo-bigger.png" />
|
||||
<div class="version">
|
||||
v{{ packetJsonVersion }} [<a href="https://github.com/mempool/mempool/commit/{{ frontendGitCommitHash }}">{{ frontendGitCommitHash }}</a>]
|
||||
<span>v{{ packetJsonVersion }} [<a href="https://github.com/mempool/mempool/commit/{{ frontendGitCommitHash }}">{{ frontendGitCommitHash }}</a>]</span>
|
||||
<span *ngIf="stateService.env.GIT_COMMIT_HASH_MEMPOOL_SPACE">[{{ stateService.env.GIT_COMMIT_HASH_MEMPOOL_SPACE }}]</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -181,21 +182,6 @@
|
||||
</svg>
|
||||
<span>Exodus</span>
|
||||
</a>
|
||||
<a href="https://www.luminex.io" target="_blank" title="Luminex">
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="66.95" height="80" viewBox="0 0 300.43 385" style="padding-top: 10px;">
|
||||
<defs>
|
||||
<style>
|
||||
.lum-cls-1 {
|
||||
fill: #f2ea25;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path class="lum-cls-1" d="m309.02,90.04c0,49.65-38.73,90.04-95.34,90.04s-95.34-40.39-95.34-90.04S153.77,0,213.69,0c56.28,0,95.34,40.39,95.34,90.04Zm-63.56,0c0-20.52-14.23-37.07-31.78-37.07s-31.78,16.55-31.78,37.07,14.23,37.07,31.78,37.07,31.78-16.55,31.78-37.07Z"/>
|
||||
<path class="lum-cls-1" d="m311.87,372.67h-66.34l-31.84-47.76-31.84,47.76h-66.34l58.38-90.22-53.07-79.61h66.34l26.54,42.46,26.53-42.46h66.34l-53.07,79.61,58.38,90.22Z"/>
|
||||
<rect class="lum-cls-1" width="60.69" height="372.67"/>
|
||||
</svg>
|
||||
<span>Luminex</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -257,7 +243,7 @@
|
||||
<img class="image" src="/resources/profile/ronindojo.png" />
|
||||
<span>RoninDojo</span>
|
||||
</a>
|
||||
<a href="https://github.com/runcitadel/core" target="_blank" title="Citadel">
|
||||
<a href="https://github.com/runcitadel" target="_blank" title="Citadel">
|
||||
<img class="image" src="/resources/profile/runcitadel.svg" />
|
||||
<span>Citadel</span>
|
||||
</a>
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
|
||||
.intro {
|
||||
margin: 25px auto 30px;
|
||||
margin-top: 25px;
|
||||
width: 250px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -43,6 +43,7 @@ export class AboutComponent implements OnInit {
|
||||
ngOnInit() {
|
||||
this.backendInfo$ = this.stateService.backendInfo$;
|
||||
this.seoService.setTitle($localize`:@@004b222ff9ef9dd4771b777950ca1d0e4cd4348a:About`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.about:Learn more about The Mempool Open Source Project™\: enterprise sponsors, individual sponsors, integrations, who contributes, FOSS licensing, and more.`);
|
||||
this.websocketService.want(['blocks']);
|
||||
|
||||
this.profiles$ = this.apiService.getAboutPageProfiles$().pipe(
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<div class="fee-graph" *ngIf="tx && estimate">
|
||||
<div class="column">
|
||||
<ng-container *ngFor="let bar of bars">
|
||||
<div class="bar {{ bar.class }}" [class.active]="bar.active" [style]="bar.style" (click)="onClick($event, bar);">
|
||||
<div class="fill"></div>
|
||||
<div class="line">
|
||||
<p class="fee-rate">
|
||||
<span class="label">{{ bar.label }}</span>
|
||||
<span class="rate">
|
||||
<app-fee-rate [fee]="bar.rate"></app-fee-rate>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="spacer"></div>
|
||||
<span class="fee">{{ bar.class === 'tx' ? '' : '+' }} {{ bar.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span></span>
|
||||
<div class="spacer"></div>
|
||||
<div class="spacer"></div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,157 @@
|
||||
.fee-graph {
|
||||
height: 100%;
|
||||
min-width: 120px;
|
||||
width: 120px;
|
||||
max-height: 90vh;
|
||||
margin-left: 4em;
|
||||
margin-right: 1.5em;
|
||||
padding-bottom: 63px;
|
||||
|
||||
.column {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
background: #181b2d;
|
||||
|
||||
.bar {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.fill {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
opacity: 0.75;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.fee {
|
||||
font-size: 0.9em;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
flex-grow: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.line {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
left: -4.5em;
|
||||
border-top: dashed white 1.5px;
|
||||
|
||||
.fee-rate {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0.2em;
|
||||
font-size: 0.8em;
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
justify-content: space-between;
|
||||
margin: 0;
|
||||
|
||||
.label {
|
||||
margin-right: .2em;
|
||||
}
|
||||
|
||||
.rate .symbol {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.tx {
|
||||
.fill {
|
||||
background: #3bcc49;
|
||||
}
|
||||
.line {
|
||||
.fee-rate {
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
.fee {
|
||||
position: absolute;
|
||||
opacity: 1;
|
||||
z-index: 11;
|
||||
}
|
||||
}
|
||||
|
||||
&.target {
|
||||
.fill {
|
||||
background: #653b9c;
|
||||
}
|
||||
.fee {
|
||||
position: absolute;
|
||||
opacity: 1;
|
||||
z-index: 11;
|
||||
}
|
||||
.line .fee-rate {
|
||||
bottom: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
&.max {
|
||||
cursor: pointer;
|
||||
.line .fee-rate {
|
||||
.label {
|
||||
opacity: 0;
|
||||
}
|
||||
bottom: 2px;
|
||||
}
|
||||
&.active, &:hover {
|
||||
.fill {
|
||||
background: #105fb0;
|
||||
}
|
||||
.line {
|
||||
.fee-rate .label {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.fill {
|
||||
z-index: 10;
|
||||
}
|
||||
.line {
|
||||
z-index: 11;
|
||||
}
|
||||
.fee {
|
||||
opacity: 1;
|
||||
z-index: 12;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover > .bar:not(:hover) {
|
||||
&.target, &.max {
|
||||
.fee {
|
||||
opacity: 0;
|
||||
}
|
||||
.line .fee-rate .label {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
&.max {
|
||||
.fill {
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { Component, OnInit, Input, Output, OnChanges, EventEmitter, HostListener, Inject, LOCALE_ID } from '@angular/core';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { Outspend, Transaction, Vin, Vout } from '../../interfaces/electrs.interface';
|
||||
import { Router } from '@angular/router';
|
||||
import { ReplaySubject, merge, Subscription, of } from 'rxjs';
|
||||
import { tap, switchMap } from 'rxjs/operators';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { AccelerationEstimate, RateOption } from './accelerate-preview.component';
|
||||
|
||||
interface GraphBar {
|
||||
rate: number;
|
||||
style: any;
|
||||
class: 'tx' | 'target' | 'max';
|
||||
label: string;
|
||||
active?: boolean;
|
||||
rateIndex?: number;
|
||||
fee?: number;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-accelerate-fee-graph',
|
||||
templateUrl: './accelerate-fee-graph.component.html',
|
||||
styleUrls: ['./accelerate-fee-graph.component.scss'],
|
||||
})
|
||||
export class AccelerateFeeGraphComponent implements OnInit, OnChanges {
|
||||
@Input() tx: Transaction;
|
||||
@Input() estimate: AccelerationEstimate;
|
||||
@Input() maxRateOptions: RateOption[] = [];
|
||||
@Input() maxRateIndex: number = 0;
|
||||
@Output() setUserBid = new EventEmitter<{ fee: number, index: number }>();
|
||||
|
||||
bars: GraphBar[] = [];
|
||||
tooltipPosition = { x: 0, y: 0 };
|
||||
|
||||
ngOnInit(): void {
|
||||
this.initGraph();
|
||||
}
|
||||
|
||||
ngOnChanges(): void {
|
||||
this.initGraph();
|
||||
}
|
||||
|
||||
initGraph(): void {
|
||||
if (!this.tx || !this.estimate) {
|
||||
return;
|
||||
}
|
||||
const maxRate = Math.max(...this.maxRateOptions.map(option => option.rate));
|
||||
const baseRate = this.estimate.txSummary.effectiveFee / this.estimate.txSummary.effectiveVsize;
|
||||
const baseHeight = baseRate / maxRate;
|
||||
const bars: GraphBar[] = this.maxRateOptions.slice().reverse().map(option => {
|
||||
return {
|
||||
rate: option.rate,
|
||||
style: this.getStyle(option.rate, maxRate, baseHeight),
|
||||
class: 'max',
|
||||
label: 'maximum',
|
||||
active: option.index === this.maxRateIndex,
|
||||
rateIndex: option.index,
|
||||
fee: option.fee,
|
||||
}
|
||||
});
|
||||
bars.push({
|
||||
rate: this.estimate.targetFeeRate,
|
||||
style: this.getStyle(this.estimate.targetFeeRate, maxRate, baseHeight),
|
||||
class: 'target',
|
||||
label: 'next block',
|
||||
fee: this.estimate.nextBlockFee - this.estimate.txSummary.effectiveFee
|
||||
});
|
||||
bars.push({
|
||||
rate: baseRate,
|
||||
style: this.getStyle(baseRate, maxRate, 0),
|
||||
class: 'tx',
|
||||
label: '',
|
||||
fee: this.estimate.txSummary.effectiveFee,
|
||||
});
|
||||
this.bars = bars;
|
||||
}
|
||||
|
||||
getStyle(rate, maxRate, base) {
|
||||
const top = (rate / maxRate);
|
||||
return {
|
||||
height: `${(top - base) * 100}%`,
|
||||
bottom: base ? `${base * 100}%` : '0',
|
||||
}
|
||||
}
|
||||
|
||||
onClick(event, bar): void {
|
||||
if (bar.rateIndex != null) {
|
||||
this.setUserBid.emit({ fee: bar.fee, index: bar.rateIndex });
|
||||
}
|
||||
}
|
||||
|
||||
@HostListener('pointermove', ['$event'])
|
||||
onPointerMove(event) {
|
||||
this.tooltipPosition = { x: event.offsetX, y: event.offsetY };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
<div class="row" *ngIf="showSuccess">
|
||||
<div class="col" id="successAlert">
|
||||
<div class="alert alert-success">
|
||||
Transaction has now been submitted to mining pools for acceleration. You can track the progress <a class="alert-link" routerLink="/services/accelerator/history">here</a>.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" *ngIf="error">
|
||||
<div class="col" id="mempoolError">
|
||||
<app-mempool-error [error]="error"></app-mempool-error>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="accelerate-cols">
|
||||
<ng-container *ngIf="!isMobile">
|
||||
<app-accelerate-fee-graph
|
||||
[tx]="tx"
|
||||
[estimate]="estimate"
|
||||
[maxRateOptions]="maxRateOptions"
|
||||
[maxRateIndex]="selectFeeRateIndex"
|
||||
(setUserBid)="setUserBid($event)"
|
||||
></app-accelerate-fee-graph>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="estimate">
|
||||
<div [class]="{estimateDisabled: error}">
|
||||
<h5>Your transaction</h5>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<small *ngIf="hasAncestors" class="form-text text-muted mb-2">
|
||||
Plus {{ estimate.txSummary.ancestorCount - 1 }} unconfirmed ancestor{{ estimate.txSummary.ancestorCount > 2 ? 's' : ''}}.
|
||||
</small>
|
||||
<table class="table table-borderless table-border table-dark table-accelerator">
|
||||
<tbody>
|
||||
<tr class="group-first">
|
||||
<td class="item">
|
||||
Virtual size
|
||||
</td>
|
||||
<td class="units" [innerHTML]="'‎' + (estimate.txSummary.effectiveVsize | vbytes: 2)"></td>
|
||||
</tr>
|
||||
<tr class="info">
|
||||
<td class="info">
|
||||
<i><small>Size in vbytes of this transaction<span *ngIf="hasAncestors"> and its unconfirmed ancestors</span></small></i>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="item">
|
||||
In-band fees
|
||||
</td>
|
||||
<td class="units">
|
||||
{{ estimate.txSummary.effectiveFee | number : '1.0-0' }} <span class="symbol" i18n="shared.sats|sats">sats</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="info group-last">
|
||||
<td class="info">
|
||||
<i><small>Fees already paid by this transaction<span *ngIf="hasAncestors"> and its unconfirmed ancestors</span></small></i>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
<h5>How much more are you willing to pay?</h5>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<small class="form-text text-muted mb-2">
|
||||
Choose the maximum extra transaction fee you're willing to pay to get into the next block.<br>
|
||||
If the estimated next block rate rises beyond this limit, we will automatically cancel your acceleration request.
|
||||
</small>
|
||||
<div class="form-group">
|
||||
<div class="fee-card">
|
||||
<div class="d-flex mb-0">
|
||||
<ng-container *ngFor="let option of maxRateOptions">
|
||||
<button type="button" class="btn btn-primary flex-grow-1 btn-border btn-sm feerate" [class]="{active: selectFeeRateIndex === option.index}" (click)="setUserBid(option)">
|
||||
<span class="fee">{{ option.fee | number }} <span class="symbol" i18n="shared.sats|sats">sats</span></span>
|
||||
<span class="rate">~ <app-fee-rate [fee]="option.rate" rounding="1.0-0"></app-fee-rate></span>
|
||||
</button>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5>Acceleration summary</h5>
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
<div class="table-toggle btn-group btn-group-toggle">
|
||||
<div class="btn btn-primary btn-sm" [class.active]="showTable === 'estimated'" (click)="showTable = 'estimated'">
|
||||
<span>Estimated cost</span>
|
||||
</div>
|
||||
<div class="btn btn-primary btn-sm" [class.active]="showTable === 'maximum'" (click)="showTable = 'maximum'">
|
||||
<span>Maximum cost</span>
|
||||
</div>
|
||||
</div>
|
||||
<table class="table table-borderless table-border table-dark table-accelerator">
|
||||
<tbody>
|
||||
<!-- ESTIMATED FEE -->
|
||||
<ng-container *ngIf="showTable === 'estimated'">
|
||||
<tr class="group-first">
|
||||
<td class="item">
|
||||
Next block market rate
|
||||
</td>
|
||||
<td class="amt" style="font-size: 20px">
|
||||
{{ estimate.targetFeeRate | number : '1.0-0' }}
|
||||
</td>
|
||||
<td class="units"><span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></td>
|
||||
</tr>
|
||||
<tr class="info">
|
||||
<td class="info">
|
||||
<i><small>Estimated extra fee required</small></i>
|
||||
</td>
|
||||
<td class="amt">
|
||||
{{ math.max(0, estimate.nextBlockFee - estimate.txSummary.effectiveFee) | number }}
|
||||
</td>
|
||||
<td class="units">
|
||||
<span class="symbol" i18n="shared.sats|sats">sats</span>
|
||||
<span class="fiat"><app-fiat [value]="math.max(0, estimate.nextBlockFee - estimate.txSummary.effectiveFee)"></app-fiat></span>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<!-- USER MAX BID -->
|
||||
<ng-container *ngIf="showTable === 'maximum'">
|
||||
<tr class="group-first">
|
||||
<td class="item">
|
||||
Your maximum
|
||||
</td>
|
||||
<td class="amt" style="width: 45%; font-size: 20px">
|
||||
~{{ ((estimate.txSummary.effectiveFee + userBid) / estimate.txSummary.effectiveVsize) | number : '1.0-0' }}
|
||||
</td>
|
||||
<td class="units"><span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></td>
|
||||
</tr>
|
||||
<tr class="info">
|
||||
<td class="info">
|
||||
<i><small>The maximum extra transaction fee you could pay</small></i>
|
||||
</td>
|
||||
<td class="amt">
|
||||
<span>
|
||||
{{ userBid | number }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="units">
|
||||
<span class="symbol" i18n="shared.sats|sats">sats</span>
|
||||
<span class="fiat"><app-fiat [value]="userBid"></app-fiat></span>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-container>
|
||||
|
||||
<!-- MEMPOOL BASE FEE -->
|
||||
<tr>
|
||||
<td class="item">
|
||||
Mempool Accelerator™ fees
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="info">
|
||||
<td class="info">
|
||||
<i><small>mempool.space fee</small></i>
|
||||
</td>
|
||||
<td class="amt">
|
||||
+{{ estimate.mempoolBaseFee | number }}
|
||||
</td>
|
||||
<td class="units">
|
||||
<span class="symbol" i18n="shared.sats|sats">sats</span>
|
||||
<span class="fiat"><app-fiat [value]="estimate.mempoolBaseFee"></app-fiat></span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="info group-last" style="border-bottom: 1px solid lightgrey">
|
||||
<td class="info">
|
||||
<i><small>Transaction vsize fee</small></i>
|
||||
</td>
|
||||
<td class="amt">
|
||||
+{{ estimate.vsizeFee | number }}
|
||||
</td>
|
||||
<td class="units">
|
||||
<span class="symbol" i18n="shared.sats|sats">sats</span>
|
||||
<span class="fiat"><app-fiat [value]="estimate.vsizeFee"></app-fiat></span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- NEXT BLOCK ESTIMATE -->
|
||||
<ng-container *ngIf="showTable === 'estimated'">
|
||||
<tr class="group-first">
|
||||
<td class="item">
|
||||
<b style="background-color: #5E35B1" class="p-1 pl-0">Estimated acceleration cost</b>
|
||||
</td>
|
||||
<td class="amt">
|
||||
<span style="background-color: #5E35B1" class="p-1 pl-0">
|
||||
{{ estimate.cost + estimate.mempoolBaseFee + estimate.vsizeFee | number }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="units">
|
||||
<span class="symbol" i18n="shared.sats|sats">sats</span>
|
||||
<span class="fiat"><app-fiat [value]="estimate.cost + estimate.mempoolBaseFee + estimate.vsizeFee"></app-fiat></span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="info group-last">
|
||||
<td class="info">
|
||||
<i><small>If your tx is accelerated to </small><small>{{ estimate.targetFeeRate | number : '1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></small></i>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-container>
|
||||
|
||||
<!-- MAX COST -->
|
||||
<ng-container *ngIf="showTable === 'maximum'">
|
||||
<tr class="group-first">
|
||||
<td class="item">
|
||||
<b style="background-color: #105fb0;" class="p-1 pl-0">Maximum acceleration cost</b>
|
||||
</td>
|
||||
<td class="amt">
|
||||
<span style="background-color: #105fb0" class="p-1 pl-0">
|
||||
{{ maxCost | number }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="units">
|
||||
<span class="symbol" i18n="shared.sats|sats">sats</span>
|
||||
<span class="fiat">
|
||||
<app-fiat [value]="maxCost" [colorClass]="estimate.userBalance < maxCost ? 'red-color' : 'green-color'"></app-fiat>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="info group-last">
|
||||
<td class="info">
|
||||
<i><small>If your tx is accelerated to </small><small>~{{ ((estimate.txSummary.effectiveFee + userBid) / estimate.txSummary.effectiveVsize) | number : '1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></small></i>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-container>
|
||||
|
||||
<!-- USER BALANCE -->
|
||||
<ng-container *ngIf="estimate.userBalance < maxCost">
|
||||
<tr class="group-first group-last" style="border-top: 1px dashed grey">
|
||||
<td class="item">
|
||||
Available balance
|
||||
</td>
|
||||
<td class="amt">
|
||||
{{ estimate.userBalance | number }}
|
||||
</td>
|
||||
<td class="units">
|
||||
<span class="symbol" i18n="shared.sats|sats">sats</span>
|
||||
<span class="fiat">
|
||||
<app-fiat [value]="estimate.userBalance" [colorClass]="estimate.userBalance < maxCost ? 'red-color' : 'green-color'"></app-fiat>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-container>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3" *ngIf="isLoggedIn()">
|
||||
<div class="col">
|
||||
<div class="d-flex justify-content-end">
|
||||
<button class="btn btn-sm btn-primary btn-success" style="width: 150px" (click)="accelerate()">Accelerate</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
@@ -0,0 +1,88 @@
|
||||
.fee-card {
|
||||
padding: 15px;
|
||||
background-color: #1d1f31;
|
||||
|
||||
.feerate {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.fee {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
.rate {
|
||||
font-size: 0.9em;
|
||||
.symbol {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-border {
|
||||
border: solid 1px black;
|
||||
background-color: #0c4a87;
|
||||
}
|
||||
|
||||
.feerate.active {
|
||||
background-color: #105fb0 !important;
|
||||
opacity: 1;
|
||||
border: 1px solid white !important;
|
||||
}
|
||||
|
||||
.estimateDisabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.table-toggle {
|
||||
width: 100%;
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
.table-accelerator {
|
||||
tr {
|
||||
text-wrap: wrap;
|
||||
|
||||
td {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
&.group-first {
|
||||
td {
|
||||
padding-top: 0.75rem;
|
||||
}
|
||||
}
|
||||
&.group-last {
|
||||
td {
|
||||
padding-bottom: 0.75rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
td {
|
||||
&:first-child {
|
||||
width: 100vw;
|
||||
}
|
||||
&.info {
|
||||
color: #6c757d;
|
||||
}
|
||||
&.amt {
|
||||
text-align: right;
|
||||
padding-right: 0.2em;
|
||||
}
|
||||
&.units {
|
||||
padding-left: 0.2em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.accelerate-cols {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: stretch;
|
||||
margin-top: 1em;
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
import { Component, OnInit, Input, OnDestroy, OnChanges, SimpleChanges, HostListener } from '@angular/core';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { Subscription, catchError, of, tap } from 'rxjs';
|
||||
import { StorageService } from '../../services/storage.service';
|
||||
import { Transaction } from '../../interfaces/electrs.interface';
|
||||
import { nextRoundNumber } from '../../shared/common.utils';
|
||||
|
||||
export type AccelerationEstimate = {
|
||||
txSummary: TxSummary;
|
||||
nextBlockFee: number;
|
||||
targetFeeRate: number;
|
||||
userBalance: number;
|
||||
enoughBalance: boolean;
|
||||
cost: number;
|
||||
mempoolBaseFee: number;
|
||||
vsizeFee: number;
|
||||
}
|
||||
export type TxSummary = {
|
||||
txid: string; // txid of the current transaction
|
||||
effectiveVsize: number; // Total vsize of the dependency tree
|
||||
effectiveFee: number; // Total fee of the dependency tree in sats
|
||||
ancestorCount: number; // Number of ancestors
|
||||
}
|
||||
|
||||
export interface RateOption {
|
||||
fee: number;
|
||||
rate: number;
|
||||
index: number;
|
||||
}
|
||||
|
||||
export const MIN_BID_RATIO = 1;
|
||||
export const DEFAULT_BID_RATIO = 2;
|
||||
export const MAX_BID_RATIO = 4;
|
||||
|
||||
@Component({
|
||||
selector: 'app-accelerate-preview',
|
||||
templateUrl: 'accelerate-preview.component.html',
|
||||
styleUrls: ['accelerate-preview.component.scss']
|
||||
})
|
||||
export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges {
|
||||
@Input() tx: Transaction | undefined;
|
||||
@Input() scrollEvent: boolean;
|
||||
|
||||
math = Math;
|
||||
error = '';
|
||||
showSuccess = false;
|
||||
estimateSubscription: Subscription;
|
||||
accelerationSubscription: Subscription;
|
||||
estimate: any;
|
||||
hasAncestors: boolean = false;
|
||||
minExtraCost = 0;
|
||||
minBidAllowed = 0;
|
||||
maxBidAllowed = 0;
|
||||
defaultBid = 0;
|
||||
maxCost = 0;
|
||||
userBid = 0;
|
||||
selectFeeRateIndex = 1;
|
||||
showTable: 'estimated' | 'maximum' = 'maximum';
|
||||
isMobile: boolean = window.innerWidth <= 767.98;
|
||||
|
||||
maxRateOptions: RateOption[] = [];
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private storageService: StorageService
|
||||
) { }
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this.estimateSubscription) {
|
||||
this.estimateSubscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes.scrollEvent) {
|
||||
this.scrollToPreview('acceleratePreviewAnchor', 'center');
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.estimateSubscription = this.apiService.estimate$(this.tx.txid).pipe(
|
||||
tap((response) => {
|
||||
if (response.status === 204) {
|
||||
this.estimate = undefined;
|
||||
this.error = `cannot_accelerate_tx`;
|
||||
this.scrollToPreviewWithTimeout('mempoolError', 'center');
|
||||
this.estimateSubscription.unsubscribe();
|
||||
} else {
|
||||
this.estimate = response.body;
|
||||
if (!this.estimate) {
|
||||
this.error = `cannot_accelerate_tx`;
|
||||
this.scrollToPreviewWithTimeout('mempoolError', 'center');
|
||||
this.estimateSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
if (this.estimate.userBalance <= 0) {
|
||||
if (this.isLoggedIn()) {
|
||||
this.error = `not_enough_balance`;
|
||||
this.scrollToPreviewWithTimeout('mempoolError', 'center');
|
||||
}
|
||||
}
|
||||
|
||||
this.hasAncestors = this.estimate.txSummary.ancestorCount > 1;
|
||||
|
||||
// Make min extra fee at least 50% of the current tx fee
|
||||
this.minExtraCost = nextRoundNumber(Math.max(this.estimate.cost * 2, this.estimate.txSummary.effectiveFee));
|
||||
|
||||
this.maxRateOptions = [1, 2, 4].map((multiplier, index) => {
|
||||
return {
|
||||
fee: this.minExtraCost * multiplier,
|
||||
rate: (this.estimate.txSummary.effectiveFee + (this.minExtraCost * multiplier)) / this.estimate.txSummary.effectiveVsize,
|
||||
index,
|
||||
};
|
||||
});
|
||||
|
||||
this.minBidAllowed = this.minExtraCost * MIN_BID_RATIO;
|
||||
this.defaultBid = this.minExtraCost * DEFAULT_BID_RATIO;
|
||||
this.maxBidAllowed = this.minExtraCost * MAX_BID_RATIO;
|
||||
|
||||
this.userBid = this.defaultBid;
|
||||
if (this.userBid < this.minBidAllowed) {
|
||||
this.userBid = this.minBidAllowed;
|
||||
} else if (this.userBid > this.maxBidAllowed) {
|
||||
this.userBid = this.maxBidAllowed;
|
||||
}
|
||||
this.maxCost = this.userBid + this.estimate.mempoolBaseFee + this.estimate.vsizeFee;
|
||||
|
||||
if (!this.error) {
|
||||
this.scrollToPreview('acceleratePreviewAnchor', 'center');
|
||||
}
|
||||
}
|
||||
}),
|
||||
catchError((response) => {
|
||||
this.estimate = undefined;
|
||||
this.error = response.error;
|
||||
this.scrollToPreviewWithTimeout('mempoolError', 'center');
|
||||
this.estimateSubscription.unsubscribe();
|
||||
return of(null);
|
||||
})
|
||||
).subscribe();
|
||||
}
|
||||
|
||||
/**
|
||||
* User changed his bid
|
||||
*/
|
||||
setUserBid({ fee, index }: { fee: number, index: number}) {
|
||||
if (this.estimate) {
|
||||
this.selectFeeRateIndex = index;
|
||||
this.userBid = Math.max(0, fee);
|
||||
this.maxCost = this.userBid + this.estimate.mempoolBaseFee + this.estimate.vsizeFee;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll to element id with or without setTimeout
|
||||
*/
|
||||
scrollToPreviewWithTimeout(id: string, position: ScrollLogicalPosition) {
|
||||
setTimeout(() => {
|
||||
this.scrollToPreview(id, position);
|
||||
}, 100);
|
||||
}
|
||||
scrollToPreview(id: string, position: ScrollLogicalPosition) {
|
||||
const acceleratePreviewAnchor = document.getElementById(id);
|
||||
if (acceleratePreviewAnchor) {
|
||||
acceleratePreviewAnchor.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
inline: position,
|
||||
block: position,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send acceleration request
|
||||
*/
|
||||
accelerate() {
|
||||
if (this.accelerationSubscription) {
|
||||
this.accelerationSubscription.unsubscribe();
|
||||
}
|
||||
this.accelerationSubscription = this.apiService.accelerate$(
|
||||
this.tx.txid,
|
||||
this.userBid
|
||||
).subscribe({
|
||||
next: () => {
|
||||
this.showSuccess = true;
|
||||
this.scrollToPreviewWithTimeout('successAlert', 'center');
|
||||
this.estimateSubscription.unsubscribe();
|
||||
},
|
||||
error: (response) => {
|
||||
this.error = response.error;
|
||||
this.scrollToPreviewWithTimeout('mempoolError', 'center');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
isLoggedIn() {
|
||||
const auth = this.storageService.getAuth();
|
||||
return auth !== null;
|
||||
}
|
||||
|
||||
@HostListener('window:resize', ['$event'])
|
||||
onResize(): void {
|
||||
this.isMobile = window.innerWidth <= 767.98;
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import { AudioService } from '../../services/audio.service';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { of, merge, Subscription, Observable } from 'rxjs';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { seoDescriptionNetwork } from '../../shared/common.utils';
|
||||
import { AddressInformation } from '../../interfaces/node-api.interface';
|
||||
|
||||
@Component({
|
||||
@@ -68,6 +69,7 @@ export class AddressPreviewComponent implements OnInit, OnDestroy {
|
||||
this.addressString = this.addressString.toLowerCase();
|
||||
}
|
||||
this.seoService.setTitle($localize`:@@address.component.browser-title:Address: ${this.addressString}:INTERPOLATION:`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.bitcoin.address:See mempool transactions, confirmed transactions, balance, and more for ${this.stateService.network==='liquid'||this.stateService.network==='liquidtestnet'?'Liquid':'Bitcoin'}${seoDescriptionNetwork(this.stateService.network)} address ${this.addressString}:INTERPOLATION:.`);
|
||||
|
||||
return (this.addressString.match(/04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}/)
|
||||
? this.electrsApiService.getPubKeyAddress$(this.addressString)
|
||||
|
||||
@@ -9,6 +9,7 @@ import { AudioService } from '../../services/audio.service';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { of, merge, Subscription, Observable } from 'rxjs';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { seoDescriptionNetwork } from '../../shared/common.utils';
|
||||
import { AddressInformation } from '../../interfaces/node-api.interface';
|
||||
|
||||
@Component({
|
||||
@@ -76,6 +77,7 @@ export class AddressComponent implements OnInit, OnDestroy {
|
||||
this.addressString = this.addressString.toLowerCase();
|
||||
}
|
||||
this.seoService.setTitle($localize`:@@address.component.browser-title:Address: ${this.addressString}:INTERPOLATION:`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.bitcoin.address:See mempool transactions, confirmed transactions, balance, and more for ${this.stateService.network==='liquid'||this.stateService.network==='liquidtestnet'?'Liquid':'Bitcoin'}${seoDescriptionNetwork(this.stateService.network)} address ${this.addressString}:INTERPOLATION:.`);
|
||||
|
||||
return merge(
|
||||
of(true),
|
||||
|
||||
@@ -40,6 +40,7 @@ export class AssetsNavComponent implements OnInit {
|
||||
|
||||
ngOnInit(): void {
|
||||
this.seoService.setTitle($localize`:@@ee8f8008bae6ce3a49840c4e1d39b4af23d4c263:Assets`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.liquid.assets:Explore all the assets issued on the Liquid network like L-BTC, L-CAD, USDT, and more.`);
|
||||
this.typeaheadSearchFn = this.typeaheadSearch;
|
||||
|
||||
this.searchForm = this.formBuilder.group({
|
||||
|
||||
@@ -64,6 +64,7 @@ export class BlockFeeRatesGraphComponent implements OnInit {
|
||||
|
||||
ngOnInit(): void {
|
||||
this.seoService.setTitle($localize`:@@ed8e33059967f554ff06b4f5b6049c465b92d9b3:Block Fee Rates`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.bitcoin.graphs.block-fee-rates:See Bitcoin feerates visualized over time, including minimum and maximum feerates per block along with feerates at various percentiles.`);
|
||||
this.miningWindowPreference = this.miningService.getDefaultTimespan('24h');
|
||||
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
|
||||
this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);
|
||||
|
||||
@@ -65,6 +65,7 @@ export class BlockFeesGraphComponent implements OnInit {
|
||||
|
||||
ngOnInit(): void {
|
||||
this.seoService.setTitle($localize`:@@6c453b11fd7bd159ae30bc381f367bc736d86909:Block Fees`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.bitcoin.graphs.block-fees:See the average mining fees earned per Bitcoin block visualized in BTC and USD over time.`);
|
||||
this.miningWindowPreference = this.miningService.getDefaultTimespan('1m');
|
||||
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
|
||||
this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);
|
||||
@@ -192,7 +193,7 @@ export class BlockFeesGraphComponent implements OnInit {
|
||||
{
|
||||
name: 'Fees ' + this.currency,
|
||||
inactiveColor: 'rgb(110, 112, 121)',
|
||||
textStyle: {
|
||||
textStyle: {
|
||||
color: 'white',
|
||||
},
|
||||
icon: 'roundRect',
|
||||
|
||||
@@ -61,6 +61,7 @@ export class BlockHealthGraphComponent implements OnInit {
|
||||
|
||||
ngOnInit(): void {
|
||||
this.seoService.setTitle($localize`:@@d7d5fcf50179ad70c938491c517efb82de2c8146:Block Health`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.bitcoin.graphs.block-health:See Bitcoin block health visualized over time. Block health is a measure of how many expected transactions were included in an actual mined block. Expected transactions are determined using Mempool's re-implementation of Bitcoin Core's transaction selection algorithm.`);
|
||||
this.miningWindowPreference = '24h';//this.miningService.getDefaultTimespan('24h');
|
||||
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
|
||||
this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);
|
||||
|
||||
@@ -70,9 +70,11 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
||||
this.canvas.nativeElement.addEventListener('webglcontextlost', this.handleContextLost, false);
|
||||
this.canvas.nativeElement.addEventListener('webglcontextrestored', this.handleContextRestored, false);
|
||||
this.gl = this.canvas.nativeElement.getContext('webgl');
|
||||
this.initCanvas();
|
||||
|
||||
this.resizeCanvas();
|
||||
if (this.gl) {
|
||||
this.initCanvas();
|
||||
this.resizeCanvas();
|
||||
}
|
||||
}
|
||||
|
||||
ngOnChanges(changes): void {
|
||||
@@ -195,10 +197,16 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
||||
cancelAnimationFrame(this.animationFrameRequest);
|
||||
this.animationFrameRequest = null;
|
||||
this.running = false;
|
||||
this.gl = null;
|
||||
}
|
||||
|
||||
handleContextRestored(event): void {
|
||||
this.initCanvas();
|
||||
if (this.canvas?.nativeElement) {
|
||||
this.gl = this.canvas.nativeElement.getContext('webgl');
|
||||
if (this.gl) {
|
||||
this.initCanvas();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@HostListener('window:resize', ['$event'])
|
||||
@@ -224,6 +232,9 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
||||
}
|
||||
|
||||
compileShader(src, type): WebGLShader {
|
||||
if (!this.gl) {
|
||||
return;
|
||||
}
|
||||
const shader = this.gl.createShader(type);
|
||||
|
||||
this.gl.shaderSource(shader, src);
|
||||
@@ -237,6 +248,9 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
||||
}
|
||||
|
||||
buildShaderProgram(shaderInfo): WebGLProgram {
|
||||
if (!this.gl) {
|
||||
return;
|
||||
}
|
||||
const program = this.gl.createProgram();
|
||||
|
||||
shaderInfo.forEach((desc) => {
|
||||
@@ -273,7 +287,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
||||
now = performance.now();
|
||||
}
|
||||
// skip re-render if there's no change to the scene
|
||||
if (this.scene) {
|
||||
if (this.scene && this.gl) {
|
||||
/* SET UP SHADER UNIFORMS */
|
||||
// screen dimensions
|
||||
this.gl.uniform2f(this.gl.getUniformLocation(this.shaderProgram, 'screenSize'), this.displayWidth, this.displayHeight);
|
||||
|
||||
@@ -63,6 +63,7 @@ export class BlockRewardsGraphComponent implements OnInit {
|
||||
|
||||
ngOnInit(): void {
|
||||
this.seoService.setTitle($localize`:@@8ba8fe810458280a83df7fdf4c614dfc1a826445:Block Rewards`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.bitcoin.graphs.block-rewards:See Bitcoin block rewards in BTC and USD visualized over time. Block rewards are the total funds miners earn from the block subsidy and fees.`);
|
||||
this.miningWindowPreference = this.miningService.getDefaultTimespan('3m');
|
||||
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
|
||||
this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);
|
||||
@@ -191,7 +192,7 @@ export class BlockRewardsGraphComponent implements OnInit {
|
||||
{
|
||||
name: 'Rewards ' + this.currency,
|
||||
inactiveColor: 'rgb(110, 112, 121)',
|
||||
textStyle: {
|
||||
textStyle: {
|
||||
color: 'white',
|
||||
},
|
||||
icon: 'roundRect',
|
||||
|
||||
@@ -60,6 +60,7 @@ export class BlockSizesWeightsGraphComponent implements OnInit {
|
||||
let firstRun = true;
|
||||
|
||||
this.seoService.setTitle($localize`:@@56fa1cd221491b6478998679cba2dc8d55ba330d:Block Sizes and Weights`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.bitcoin.graphs.block-sizes:See Bitcoin block sizes (MB) and block weights (weight units) visualized over time.`);
|
||||
this.miningWindowPreference = this.miningService.getDefaultTimespan('24h');
|
||||
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
|
||||
this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);
|
||||
|
||||
@@ -8,6 +8,7 @@ import { SeoService } from '../../services/seo.service';
|
||||
import { OpenGraphService } from '../../services/opengraph.service';
|
||||
import { BlockExtended, TransactionStripped } from '../../interfaces/node-api.interface';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { seoDescriptionNetwork } from '../../shared/common.utils';
|
||||
import { BlockOverviewGraphComponent } from '../../components/block-overview-graph/block-overview-graph.component';
|
||||
|
||||
@Component({
|
||||
@@ -97,6 +98,11 @@ export class BlockPreviewComponent implements OnInit, OnDestroy {
|
||||
this.blockHeight = block.height;
|
||||
|
||||
this.seoService.setTitle($localize`:@@block.component.browser-title:Block ${block.height}:BLOCK_HEIGHT:: ${block.id}:BLOCK_ID:`);
|
||||
if( this.stateService.network === 'liquid' || this.stateService.network === 'liquidtestnet' ) {
|
||||
this.seoService.setDescription($localize`:@@meta.description.liquid.block:See size, weight, fee range, included transactions, and more for Liquid${seoDescriptionNetwork(this.stateService.network)} block ${block.height}:BLOCK_HEIGHT: (${block.id}:BLOCK_ID:).`);
|
||||
} else {
|
||||
this.seoService.setDescription($localize`:@@meta.description.bitcoin.block:See size, weight, fee range, included transactions, audit (expected v actual), and more for Bitcoin${seoDescriptionNetwork(this.stateService.network)} block ${block.height}:BLOCK_HEIGHT: (${block.id}:BLOCK_ID:).`);
|
||||
}
|
||||
this.isLoadingBlock = false;
|
||||
this.setBlockSubsidy();
|
||||
if (block?.extras?.reward !== undefined) {
|
||||
|
||||
@@ -13,6 +13,7 @@ import { BlockAudit, BlockExtended, TransactionStripped } from '../../interfaces
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { BlockOverviewGraphComponent } from '../../components/block-overview-graph/block-overview-graph.component';
|
||||
import { detectWebGL } from '../../shared/graphs.utils';
|
||||
import { seoDescriptionNetwork } from '../../shared/common.utils';
|
||||
import { PriceService, Price } from '../../services/price.service';
|
||||
import { CacheService } from '../../services/cache.service';
|
||||
|
||||
@@ -261,6 +262,11 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
this.setNextAndPreviousBlockLink();
|
||||
|
||||
this.seoService.setTitle($localize`:@@block.component.browser-title:Block ${block.height}:BLOCK_HEIGHT:: ${block.id}:BLOCK_ID:`);
|
||||
if( this.stateService.network === 'liquid' || this.stateService.network === 'liquidtestnet' ) {
|
||||
this.seoService.setDescription($localize`:@@meta.description.liquid.block:See size, weight, fee range, included transactions, and more for Liquid${seoDescriptionNetwork(this.stateService.network)} block ${block.height}:BLOCK_HEIGHT: (${block.id}:BLOCK_ID:).`);
|
||||
} else {
|
||||
this.seoService.setDescription($localize`:@@meta.description.bitcoin.block:See size, weight, fee range, included transactions, audit (expected v actual), and more for Bitcoin${seoDescriptionNetwork(this.stateService.network)} block ${block.height}:BLOCK_HEIGHT: (${block.id}:BLOCK_ID:).`);
|
||||
}
|
||||
this.isLoadingBlock = false;
|
||||
this.setBlockSubsidy();
|
||||
if (block?.extras?.reward !== undefined) {
|
||||
@@ -325,7 +331,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
]);
|
||||
})
|
||||
)
|
||||
.subscribe(([transactions, blockAudit]) => {
|
||||
.subscribe(([transactions, blockAudit]) => {
|
||||
if (transactions) {
|
||||
this.strippedTransactions = transactions;
|
||||
} else {
|
||||
@@ -680,7 +686,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
this.setAuditAvailable(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
isAuditAvailableFromBlockHeight(blockHeight: number): boolean {
|
||||
if (!this.auditSupported) {
|
||||
return false;
|
||||
@@ -729,4 +735,4 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
this.block.canonical = block.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, Input, Output, EventEmitter, HostListener, ChangeDetectorRef } from '@angular/core';
|
||||
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, Input, Output, EventEmitter, HostListener, ChangeDetectorRef, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { firstValueFrom, Subscription } from 'rxjs';
|
||||
import { StateService } from '../../services/state.service';
|
||||
|
||||
@@ -8,12 +8,13 @@ import { StateService } from '../../services/state.service';
|
||||
styleUrls: ['./blockchain.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class BlockchainComponent implements OnInit, OnDestroy {
|
||||
export class BlockchainComponent implements OnInit, OnDestroy, OnChanges {
|
||||
@Input() pages: any[] = [];
|
||||
@Input() pageIndex: number;
|
||||
@Input() blocksPerPage: number = 8;
|
||||
@Input() minScrollWidth: number = 0;
|
||||
@Input() scrollableMempool: boolean = false;
|
||||
@Input() containerWidth: number;
|
||||
|
||||
@Output() mempoolOffsetChange: EventEmitter<number> = new EventEmitter();
|
||||
|
||||
@@ -85,19 +86,25 @@ export class BlockchainComponent implements OnInit, OnDestroy {
|
||||
this.mempoolOffsetChange.emit(this.mempoolOffset);
|
||||
}
|
||||
|
||||
@HostListener('window:resize', ['$event'])
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes.containerWidth) {
|
||||
this.onResize();
|
||||
}
|
||||
}
|
||||
|
||||
onResize(): void {
|
||||
if (window.innerWidth >= 768) {
|
||||
const width = this.containerWidth || window.innerWidth;
|
||||
if (width >= 768) {
|
||||
if (this.stateService.isLiquid()) {
|
||||
this.dividerOffset = 420;
|
||||
} else {
|
||||
this.dividerOffset = window.innerWidth * 0.5;
|
||||
this.dividerOffset = width * 0.5;
|
||||
}
|
||||
} else {
|
||||
if (this.stateService.isLiquid()) {
|
||||
this.dividerOffset = window.innerWidth * 0.5;
|
||||
this.dividerOffset = width * 0.5;
|
||||
} else {
|
||||
this.dividerOffset = window.innerWidth * 0.95;
|
||||
this.dividerOffset = width * 0.95;
|
||||
}
|
||||
}
|
||||
this.cd.markForCheck();
|
||||
|
||||
@@ -5,6 +5,8 @@ import { BlockExtended } from '../../interfaces/node-api.interface';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { WebsocketService } from '../../services/websocket.service';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { seoDescriptionNetwork } from '../../shared/common.utils';
|
||||
|
||||
@Component({
|
||||
selector: 'app-blocks-list',
|
||||
@@ -35,6 +37,7 @@ export class BlocksList implements OnInit {
|
||||
private websocketService: WebsocketService,
|
||||
public stateService: StateService,
|
||||
private cd: ChangeDetectorRef,
|
||||
private seoService: SeoService,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -50,6 +53,14 @@ export class BlocksList implements OnInit {
|
||||
this.skeletonLines = this.widget === true ? [...Array(6).keys()] : [...Array(15).keys()];
|
||||
this.paginationMaxSize = window.matchMedia('(max-width: 670px)').matches ? 3 : 5;
|
||||
|
||||
this.seoService.setTitle($localize`:@@meta.title.blocks-list:Blocks`);
|
||||
if( this.stateService.network==='liquid'||this.stateService.network==='liquidtestnet' ) {
|
||||
this.seoService.setDescription($localize`:@@meta.description.liquid.blocks:See the most recent Liquid${seoDescriptionNetwork(this.stateService.network)} blocks along with basic stats such as block height, block size, and more.`);
|
||||
} else {
|
||||
this.seoService.setDescription($localize`:@@meta.description.bitcoin.blocks:See the most recent Bitcoin${seoDescriptionNetwork(this.stateService.network)} blocks along with basic stats such as block height, block reward, block size, and more.`);
|
||||
}
|
||||
|
||||
|
||||
this.blocks$ = combineLatest([
|
||||
this.fromHeightSubject.pipe(
|
||||
switchMap((fromBlockHeight) => {
|
||||
@@ -129,4 +140,4 @@ export class BlocksList implements OnInit {
|
||||
isEllipsisActive(e): boolean {
|
||||
return (e.offsetWidth < e.scrollWidth);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div [formGroup]="fiatForm" class="text-small text-center">
|
||||
<select formControlName="fiat" class="custom-select custom-select-sm form-control-secondary form-control mx-auto" style="width: 150px;" (change)="changeFiat()">
|
||||
<option *ngFor="let currency of currencies" [value]="currency[1].code">{{ currency[1].name + " (" + currency[1].code + ")" }}</option>
|
||||
<select formControlName="fiat" class="custom-select custom-select-sm form-control-secondary form-control mx-auto" style="width: 85px;" (change)="changeFiat()">
|
||||
<option *ngFor="let currency of currencies" [value]="currency[1].code">{{ currency[1].code }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -12,6 +12,7 @@ import { MiningService } from '../../services/mining.service';
|
||||
import { download } from '../../shared/graphs.utils';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { seoDescriptionNetwork } from '../../shared/common.utils';
|
||||
|
||||
@Component({
|
||||
selector: 'app-hashrate-chart',
|
||||
@@ -71,6 +72,7 @@ export class HashrateChartComponent implements OnInit {
|
||||
this.miningWindowPreference = '1y';
|
||||
} else {
|
||||
this.seoService.setTitle($localize`:@@3510fc6daa1d975f331e3a717bdf1a34efa06dff:Hashrate & Difficulty`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.bitcoin.graphs.hashrate:See hashrate and difficulty for the Bitcoin${seoDescriptionNetwork(this.network)} network visualized over time.`);
|
||||
this.miningWindowPreference = this.miningService.getDefaultTimespan('3m');
|
||||
}
|
||||
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
|
||||
@@ -256,7 +258,7 @@ export class HashrateChartComponent implements OnInit {
|
||||
let difficultyPowerOfTen = hashratePowerOfTen;
|
||||
let difficulty = tick.data[1];
|
||||
if (difficulty === null) {
|
||||
difficultyString = `${tick.marker} ${tick.seriesName}: No data<br>`;
|
||||
difficultyString = `${tick.marker} ${tick.seriesName}: No data<br>`;
|
||||
} else {
|
||||
if (this.isMobile()) {
|
||||
difficultyPowerOfTen = selectPowerOfTen(tick.data[1]);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div [formGroup]="languageForm" class="text-small text-center">
|
||||
<select formControlName="language" class="custom-select custom-select-sm form-control-secondary form-control mx-auto" style="width: 100px;" (change)="changeLanguage()">
|
||||
<select formControlName="language" class="custom-select custom-select-sm form-control-secondary form-control mx-auto" style="width: 95px;" (change)="changeLanguage()">
|
||||
<option *ngFor="let lang of languages" [value]="lang.code">{{ lang.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,20 @@
|
||||
<ng-container *ngIf="{ val: network$ | async } as network">
|
||||
<header *ngIf="headerVisible">
|
||||
<header *ngIf="headerVisible" class="sticky-header">
|
||||
|
||||
<nav class="navbar navbar-expand-md navbar-dark bg-dark">
|
||||
<!-- Hamburger -->
|
||||
<ng-container *ngIf="servicesEnabled">
|
||||
<div *ngIf="user" class="profile_image_container" [class]="{'anon': !user.imageMd5}" (click)="hamburgerClick($event)">
|
||||
<img *ngIf="user.imageMd5" [src]="'/api/v1/services/account/image/' + user.username + '?md5=' + user.imageMd5" class="profile_image">
|
||||
<app-svg-images style="color: lightgrey; fill: lightgray" *ngIf="!user.imageMd5" name="anon"></app-svg-images>
|
||||
</div>
|
||||
<div *ngIf="false && user === null" class="profile_image_container" (click)="hamburgerClick($event)">
|
||||
<app-svg-images name="hamburger" height="40"></app-svg-images>
|
||||
</div>
|
||||
<!-- Empty placeholder -->
|
||||
<div *ngIf="user === undefined" class="profile_image_container"></div>
|
||||
</ng-container>
|
||||
|
||||
<a class="navbar-brand" [ngClass]="{'dual-logos': subdomain}" [routerLink]="['/' | relativeUrl]" (click)="brandClick($event)">
|
||||
<ng-template [ngIf]="subdomain">
|
||||
<div class="subdomain_container">
|
||||
@@ -62,11 +76,19 @@
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<app-testnet-alert *ngIf="network.val === 'testnet' || network.val === 'signet'"></app-testnet-alert>
|
||||
<div class="d-flex" style="overflow: clip">
|
||||
<app-menu *ngIf="servicesEnabled" [navOpen]="menuOpen" (loggedOut)="onLoggedOut()" (menuToggled)="menuToggled($event)"></app-menu>
|
||||
|
||||
<main>
|
||||
<router-outlet></router-outlet>
|
||||
</main>
|
||||
<div class="flex-grow-1 d-flex flex-column">
|
||||
<app-testnet-alert *ngIf="network.val === 'testnet' || network.val === 'signet'"></app-testnet-alert>
|
||||
|
||||
<main style="min-width: 375px" [style]="menuOpen ? 'max-width: calc(100vw - 225px)' : 'max-width: 100vw'">
|
||||
<router-outlet></router-outlet>
|
||||
</main>
|
||||
|
||||
<div class="flex-grow-1"></div>
|
||||
<app-global-footer *ngIf="footerVisible"></app-global-footer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<app-global-footer *ngIf="footerVisible"></app-global-footer>
|
||||
</ng-container>
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
.sticky-header {
|
||||
position: sticky;
|
||||
position: -webkit-sticky;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
li.nav-item.active {
|
||||
background-color: #653b9c;
|
||||
}
|
||||
@@ -86,7 +94,6 @@ li.nav-item {
|
||||
|
||||
.navbar-brand {
|
||||
position: relative;
|
||||
height: 65px;
|
||||
}
|
||||
|
||||
.navbar-brand.dual-logos {
|
||||
@@ -102,7 +109,7 @@ nav {
|
||||
|
||||
.connection-badge {
|
||||
position: absolute;
|
||||
top: 22px;
|
||||
top: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -209,4 +216,26 @@ nav {
|
||||
margin-left: 5px;
|
||||
margin-right: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.profile_image_container {
|
||||
width: 35px;
|
||||
margin-right: 15px;
|
||||
text-align: center;
|
||||
align-self: center;
|
||||
cursor: pointer;
|
||||
&.anon {
|
||||
border: 1.5px solid lightgrey;
|
||||
color: lightgrey;
|
||||
border-radius: 5px;
|
||||
}
|
||||
}
|
||||
.profile_image {
|
||||
height: 35px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
main {
|
||||
transition: 0.2s;
|
||||
transition-property: max-width;
|
||||
}
|
||||
@@ -1,9 +1,13 @@
|
||||
import { Component, OnInit, Input } from '@angular/core';
|
||||
import { Component, OnInit, Input, ViewChild } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { Env, StateService } from '../../services/state.service';
|
||||
import { Observable, merge, of } from 'rxjs';
|
||||
import { LanguageService } from '../../services/language.service';
|
||||
import { EnterpriseService } from '../../services/enterprise.service';
|
||||
import { NavigationService } from '../../services/navigation.service';
|
||||
import { MenuComponent } from '../menu/menu.component';
|
||||
import { StorageService } from '../../services/storage.service';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-master-page',
|
||||
@@ -25,12 +29,21 @@ export class MasterPageComponent implements OnInit {
|
||||
networkPaths: { [network: string]: string };
|
||||
networkPaths$: Observable<Record<string, string>>;
|
||||
footerVisible = true;
|
||||
user: any = undefined;
|
||||
servicesEnabled = false;
|
||||
menuOpen = false;
|
||||
|
||||
@ViewChild(MenuComponent)
|
||||
public menuComponent!: MenuComponent;
|
||||
|
||||
constructor(
|
||||
public stateService: StateService,
|
||||
private languageService: LanguageService,
|
||||
private enterpriseService: EnterpriseService,
|
||||
private navigationService: NavigationService,
|
||||
private storageService: StorageService,
|
||||
private apiService: ApiService,
|
||||
private router: Router,
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
@@ -51,17 +64,47 @@ export class MasterPageComponent implements OnInit {
|
||||
this.footerVisible = this.footerVisibleOverride;
|
||||
}
|
||||
});
|
||||
|
||||
this.servicesEnabled = this.officialMempoolSpace && this.stateService.env.ACCELERATOR === true && this.stateService.network === '';
|
||||
this.refreshAuth();
|
||||
|
||||
const isServicesPage = this.router.url.includes('/services/');
|
||||
this.menuOpen = isServicesPage && !this.isSmallScreen();
|
||||
}
|
||||
|
||||
collapse(): void {
|
||||
this.navCollapsed = !this.navCollapsed;
|
||||
}
|
||||
|
||||
isSmallScreen() {
|
||||
return window.innerWidth <= 767.98;
|
||||
}
|
||||
|
||||
onResize(): void {
|
||||
this.isMobile = window.innerWidth <= 767.98;
|
||||
this.isMobile = this.isSmallScreen();
|
||||
}
|
||||
|
||||
brandClick(e): void {
|
||||
this.stateService.resetScroll$.next(true);
|
||||
}
|
||||
|
||||
onLoggedOut(): void {
|
||||
this.refreshAuth();
|
||||
}
|
||||
|
||||
refreshAuth(): void {
|
||||
this.user = this.storageService.getAuth()?.user ?? null;
|
||||
}
|
||||
|
||||
hamburgerClick(event): void {
|
||||
if (this.menuComponent) {
|
||||
this.menuComponent.hamburgerClick();
|
||||
this.menuOpen = this.menuComponent.navOpen;
|
||||
event.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
menuToggled(isOpen: boolean): void {
|
||||
this.menuOpen = isOpen;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { switchMap, map, tap, filter } from 'rxjs/operators';
|
||||
import { MempoolBlock, TransactionStripped } from '../../interfaces/websocket.interface';
|
||||
import { Observable, BehaviorSubject } from 'rxjs';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { seoDescriptionNetwork } from '../../shared/common.utils';
|
||||
import { WebsocketService } from '../../services/websocket.service';
|
||||
|
||||
@Component({
|
||||
@@ -54,6 +55,7 @@ export class MempoolBlockComponent implements OnInit, OnDestroy {
|
||||
const ordinal = this.getOrdinal(mempoolBlocks[this.mempoolBlockIndex]);
|
||||
this.ordinal$.next(ordinal);
|
||||
this.seoService.setTitle(ordinal);
|
||||
this.seoService.setDescription($localize`:@@meta.description.mempool-block:See stats for ${this.stateService.network==='liquid'||this.stateService.network==='liquidtestnet'?'Liquid':'Bitcoin'}${seoDescriptionNetwork(this.stateService.network)} transactions in the mempool: fee range, aggregate size, and more. Mempool blocks are updated in real-time as the network receives new transactions.`);
|
||||
mempoolBlocks[this.mempoolBlockIndex].isStack = mempoolBlocks[this.mempoolBlockIndex].blockVSize > this.stateService.blockVSize;
|
||||
return mempoolBlocks[this.mempoolBlockIndex];
|
||||
})
|
||||
|
||||
@@ -31,6 +31,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
@Input() spotlight: number = 0;
|
||||
@Input() getHref?: (index) => string = (index) => `/mempool-block/${index}`;
|
||||
@Input() allBlocks: boolean = false;
|
||||
@Input() forceRtl: boolean = false;
|
||||
|
||||
mempoolWidth: number = 0;
|
||||
@Output() widthChange: EventEmitter<number> = new EventEmitter();
|
||||
@@ -102,7 +103,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
}
|
||||
|
||||
this.timeLtrSubscription = this.stateService.timeLtr.subscribe((ltr) => {
|
||||
this.timeLtr = !!ltr;
|
||||
this.timeLtr = !this.forceRtl && !!ltr;
|
||||
this.cd.markForCheck();
|
||||
});
|
||||
|
||||
@@ -114,11 +115,6 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
});
|
||||
this.reduceEmptyBlocksToFitScreen(this.mempoolEmptyBlocks);
|
||||
|
||||
this.mempoolBlocks.map(() => {
|
||||
this.updateMempoolBlockStyles();
|
||||
this.calculateTransactionPosition();
|
||||
});
|
||||
this.reduceMempoolBlocksToFitScreen(this.mempoolBlocks);
|
||||
this.isTabHiddenSubscription = this.stateService.isTabHidden$.subscribe((tabHidden) => this.tabHidden = tabHidden);
|
||||
this.loadingBlocks$ = combineLatest([
|
||||
this.stateService.isLoadingWebSocket$,
|
||||
@@ -206,14 +202,17 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
if (!block) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isNewBlock = block.height > this.chainTip;
|
||||
|
||||
if (this.chainTip === -1) {
|
||||
this.animateEntry = block.height === this.stateService.latestBlockHeight;
|
||||
} else {
|
||||
this.animateEntry = block.height > this.chainTip;
|
||||
this.animateEntry = isNewBlock;
|
||||
}
|
||||
|
||||
this.chainTip = this.stateService.latestBlockHeight;
|
||||
if ((block?.extras?.similarity == null || block?.extras?.similarity > 0.5) && !this.tabHidden) {
|
||||
if (isNewBlock && (block?.extras?.similarity == null || block?.extras?.similarity > 0.5) && !this.tabHidden) {
|
||||
this.blockIndex++;
|
||||
}
|
||||
});
|
||||
@@ -283,7 +282,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
}
|
||||
|
||||
reduceEmptyBlocksToFitScreen(blocks: MempoolBlock[]): MempoolBlock[] {
|
||||
const innerWidth = this.stateService.env.BASE_MODULE !== 'liquid' && window.innerWidth <= 767.98 ? window.innerWidth : window.innerWidth / 2;
|
||||
const innerWidth = this.containerWidth || (this.stateService.env.BASE_MODULE !== 'liquid' && window.innerWidth <= 767.98 ? window.innerWidth : window.innerWidth / 2);
|
||||
let blocksAmount = this.stateService.env.MEMPOOL_BLOCKS_AMOUNT;
|
||||
if (!this.allBlocks) {
|
||||
blocksAmount = Math.min(this.stateService.env.MEMPOOL_BLOCKS_AMOUNT, Math.floor(innerWidth / (this.blockWidth + this.blockPadding)));
|
||||
@@ -306,7 +305,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
}
|
||||
|
||||
reduceMempoolBlocksToFitScreen(blocks: MempoolBlock[]): MempoolBlock[] {
|
||||
const innerWidth = this.stateService.env.BASE_MODULE !== 'liquid' && window.innerWidth <= 767.98 ? window.innerWidth : window.innerWidth / 2;
|
||||
const innerWidth = this.containerWidth || (this.stateService.env.BASE_MODULE !== 'liquid' && window.innerWidth <= 767.98 ? window.innerWidth : window.innerWidth / 2);
|
||||
let blocksAmount = this.stateService.env.MEMPOOL_BLOCKS_AMOUNT;
|
||||
if (this.count) {
|
||||
blocksAmount = 8;
|
||||
@@ -316,7 +315,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
while (blocks.length > blocksAmount) {
|
||||
const block = blocks.pop();
|
||||
if (!this.count) {
|
||||
const lastBlock = blocks[0];
|
||||
const lastBlock = blocks[blocks.length - 1];
|
||||
lastBlock.blockSize += block.blockSize;
|
||||
lastBlock.blockVSize += block.blockVSize;
|
||||
lastBlock.nTx += block.nTx;
|
||||
@@ -327,7 +326,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
}
|
||||
}
|
||||
if (blocks.length) {
|
||||
blocks[0].isStack = blocks[0].blockVSize > this.stateService.blockVSize;
|
||||
blocks[blocks.length - 1].isStack = blocks[blocks.length - 1].blockVSize > this.stateService.blockVSize;
|
||||
}
|
||||
return blocks;
|
||||
}
|
||||
|
||||
31
frontend/src/app/components/menu/menu.component.html
Normal file
31
frontend/src/app/components/menu/menu.component.html
Normal file
@@ -0,0 +1,31 @@
|
||||
<div class="sidenav menu-click" [class]="navOpen ? 'open': ''">
|
||||
<div class="d-flex menu-click">
|
||||
|
||||
<nav class="scrollable menu-click">
|
||||
<span *ngIf="userAuth" class="menu-click">
|
||||
<strong class="menu-click">@ {{ userAuth.user.username }}</strong>
|
||||
</span>
|
||||
<a *ngIf="!userAuth" class="d-flex justify-content-center align-items-center nav-link m-0 menu-click" routerLink="/login" role="tab" (click)="onLinkClick('/login')">
|
||||
<fa-icon class="menu-click" [icon]="['fas', 'user-circle']" [fixedWidth]="true" style="font-size: 25px;margin-right: 15px;"></fa-icon>
|
||||
<span class="menu-click" style="font-size: 20px;">Sign in</span>
|
||||
</a>
|
||||
|
||||
<ng-container *ngIf="userMenuGroups$ | async as menuGroups">
|
||||
<div class="menu-click" *ngFor="let group of menuGroups" style="height: max-content;">
|
||||
<h6 class="d-flex justify-content-between align-items-center mt-4 mb-2 text-uppercase menu-click">
|
||||
<span class="menu-click">{{ group.title }}</span>
|
||||
</h6>
|
||||
<ul class="nav flex-column menu-click" *ngFor="let item of group.items" (click)="onLinkClick(item.link)">
|
||||
<li class="nav-item d-flex justify-content-start align-items-center menu-click">
|
||||
<fa-icon class="menu-click" [icon]="['fas', item.faIcon]" [fixedWidth]="true"></fa-icon>
|
||||
<button *ngIf="item.link === 'logout'" class="btn nav-link menu-click" role="tab" (click)="logout()">{{ item.title }}</button>
|
||||
<a *ngIf="item.title !== 'Logout'" class="nav-link menu-click" [routerLink]="[item.link]" role="tab">{{ item.title }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</ng-container>
|
||||
</nav>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
48
frontend/src/app/components/menu/menu.component.scss
Normal file
48
frontend/src/app/components/menu/menu.component.scss
Normal file
@@ -0,0 +1,48 @@
|
||||
.sidenav {
|
||||
z-index: 1;
|
||||
background-color: transparent;
|
||||
width: 225px;
|
||||
height: calc(100vh - 65px);
|
||||
position: sticky;
|
||||
top: 65px;
|
||||
transition: 0.25s;
|
||||
margin-left: -250px;
|
||||
box-shadow: 5px 0px 30px 0px #000;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.scrollable {
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.sidenav.open {
|
||||
margin-left: 0px;
|
||||
left: 0px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.sidenav a, button{
|
||||
text-decoration: none;
|
||||
color: lightgray;
|
||||
margin-left: 20px;
|
||||
}
|
||||
.sidenav a:hover {
|
||||
color: white;
|
||||
}
|
||||
.sidenav nav {
|
||||
width: 100%;
|
||||
height: calc(100vh - 65px);
|
||||
background-color: #1d1f31;
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
padding-top: 20px;
|
||||
padding-bottom: 20px;
|
||||
@media (max-width: 991px) {
|
||||
padding-bottom: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-height: 450px) {
|
||||
.sidenav a {font-size: 18px;}
|
||||
}
|
||||
101
frontend/src/app/components/menu/menu.component.ts
Normal file
101
frontend/src/app/components/menu/menu.component.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { Component, OnInit, Input, Output, EventEmitter, HostListener, OnDestroy } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { MenuGroup } from '../../interfaces/services.interface';
|
||||
import { StorageService } from '../../services/storage.service';
|
||||
import { Router, NavigationStart } from '@angular/router';
|
||||
import { StateService } from '../../services/state.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-menu',
|
||||
templateUrl: './menu.component.html',
|
||||
styleUrls: ['./menu.component.scss']
|
||||
})
|
||||
|
||||
export class MenuComponent implements OnInit, OnDestroy {
|
||||
@Input() navOpen: boolean = false;
|
||||
@Output() loggedOut = new EventEmitter<boolean>();
|
||||
@Output() menuToggled = new EventEmitter<boolean>();
|
||||
|
||||
userMenuGroups$: Observable<MenuGroup[]> | undefined;
|
||||
userAuth: any | undefined;
|
||||
isServicesPage = false;
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private storageService: StorageService,
|
||||
private router: Router,
|
||||
private stateService: StateService
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.userAuth = this.storageService.getAuth();
|
||||
|
||||
if (this.stateService.env.GIT_COMMIT_HASH_MEMPOOL_SPACE) {
|
||||
this.userMenuGroups$ = this.apiService.getUserMenuGroups$();
|
||||
}
|
||||
|
||||
this.isServicesPage = this.router.url.includes('/services/');
|
||||
this.router.events.subscribe((event) => {
|
||||
if (event instanceof NavigationStart) {
|
||||
if (!this.isServicesPage) {
|
||||
this.toggleMenu(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
toggleMenu(toggled: boolean) {
|
||||
this.navOpen = toggled;
|
||||
this.menuToggled.emit(toggled);
|
||||
}
|
||||
|
||||
isSmallScreen() {
|
||||
return window.innerWidth <= 767.98;
|
||||
}
|
||||
|
||||
logout(): void {
|
||||
this.apiService.logout$().subscribe(() => {
|
||||
this.loggedOut.emit(true);
|
||||
if (this.stateService.env.GIT_COMMIT_HASH_MEMPOOL_SPACE) {
|
||||
this.userMenuGroups$ = this.apiService.getUserMenuGroups$();
|
||||
this.router.navigateByUrl('/');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onLinkClick(link) {
|
||||
if (!this.isServicesPage || this.isSmallScreen()) {
|
||||
this.toggleMenu(false);
|
||||
}
|
||||
this.router.navigateByUrl(link);
|
||||
}
|
||||
|
||||
hamburgerClick() {
|
||||
this.toggleMenu(!this.navOpen);
|
||||
this.stateService.menuOpen$.next(this.navOpen);
|
||||
}
|
||||
|
||||
@HostListener('window:click', ['$event'])
|
||||
onClick(event) {
|
||||
const isServicesPageOnMobile = this.isServicesPage && this.isSmallScreen();
|
||||
const cssClasses = event.target.className;
|
||||
|
||||
if (!cssClasses.indexOf) { // Click on chart or non html thingy, close the menu
|
||||
if (!this.isServicesPage || isServicesPageOnMobile) {
|
||||
this.toggleMenu(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const isHamburger = cssClasses.indexOf('profile_image') !== -1;
|
||||
const isMenu = cssClasses.indexOf('menu-click') !== -1;
|
||||
if (!isHamburger && !isMenu && (!this.isServicesPage || isServicesPageOnMobile)) {
|
||||
this.toggleMenu(false);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.stateService.menuOpen$.next(false);
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ export class MiningDashboardComponent implements OnInit, AfterViewInit {
|
||||
private router: Router
|
||||
) {
|
||||
this.seoService.setTitle($localize`:@@a681a4e2011bb28157689dbaa387de0dd0aa0c11:Mining Dashboard`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.mining.dashboard:Get real-time Bitcoin mining stats like hashrate, difficulty adjustment, block rewards, pool dominance, and more.`);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
@@ -29,7 +30,7 @@ export class MiningDashboardComponent implements OnInit, AfterViewInit {
|
||||
this.router.events.subscribe((e: NavigationStart) => {
|
||||
if (e.type === EventType.NavigationStart) {
|
||||
if (e.url.indexOf('graphs') === -1) { // The mining dashboard and the graph component are part of the same module so we can't use ngAfterViewInit in graphs.component.ts to blur the input
|
||||
this.stateService.focusSearchInputDesktop();
|
||||
this.stateService.focusSearchInputDesktop();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -56,6 +56,7 @@ export class PoolRankingComponent implements OnInit {
|
||||
this.miningWindowPreference = '1w';
|
||||
} else {
|
||||
this.seoService.setTitle($localize`:@@mining.mining-pools:Mining Pools`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.bitcoin.graphs.pool-ranking:See the top Bitcoin mining pools ranked by number of blocks mined, over your desired timeframe.`);
|
||||
this.miningWindowPreference = this.miningService.getDefaultTimespan('24h');
|
||||
}
|
||||
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
|
||||
@@ -116,7 +117,7 @@ export class PoolRankingComponent implements OnInit {
|
||||
} else if (this.widget) {
|
||||
poolShareThreshold = 1;
|
||||
}
|
||||
|
||||
|
||||
const data: object[] = [];
|
||||
let totalShareOther = 0;
|
||||
let totalBlockOther = 0;
|
||||
|
||||
@@ -83,6 +83,7 @@ export class PoolPreviewComponent implements OnInit {
|
||||
}
|
||||
|
||||
this.seoService.setTitle(poolStats.pool.name);
|
||||
this.seoService.setDescription($localize`:@@meta.description.mining.pool:See mining pool stats for ${poolStats.pool.name}\: most recent mined blocks, hashrate over time, total block reward to date, known coinbase addresses, and more.`);
|
||||
let regexes = '"';
|
||||
for (const regex of poolStats.pool.regexes) {
|
||||
regexes += regex + '", "';
|
||||
|
||||
@@ -83,6 +83,7 @@ export class PoolComponent implements OnInit {
|
||||
}),
|
||||
map((poolStats) => {
|
||||
this.seoService.setTitle(poolStats.pool.name);
|
||||
this.seoService.setDescription($localize`:@@meta.description.mining.pool:See mining pool stats for ${poolStats.pool.name}\: most recent mined blocks, hashrate over time, total block reward to date, known coinbase addresses, and more.`);
|
||||
let regexes = '"';
|
||||
for (const regex of poolStats.pool.regexes) {
|
||||
regexes += regex + '", "';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Env, StateService } from '../../services/state.service';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-privacy-policy',
|
||||
@@ -11,5 +12,11 @@ export class PrivacyPolicyComponent {
|
||||
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
private seoService: SeoService,
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.seoService.setTitle('Privacy Policy');
|
||||
this.seoService.setDescription('Trusted third parties are security holes, as are trusted first parties...you should only trust your own self-hosted instance of The Mempool Open Source Project™.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { seoDescriptionNetwork } from '../../shared/common.utils';
|
||||
|
||||
@Component({
|
||||
selector: 'app-push-transaction',
|
||||
@@ -16,12 +19,17 @@ export class PushTransactionComponent implements OnInit {
|
||||
constructor(
|
||||
private formBuilder: UntypedFormBuilder,
|
||||
private apiService: ApiService,
|
||||
public stateService: StateService,
|
||||
private seoService: SeoService,
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.pushTxForm = this.formBuilder.group({
|
||||
txHash: ['', Validators.required],
|
||||
});
|
||||
|
||||
this.seoService.setTitle($localize`:@@meta.title.push-tx:Broadcast Transaction`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.push-tx:Broadcast a transaction to the ${this.stateService.network==='liquid'||this.stateService.network==='liquidtestnet'?'Liquid':'Bitcoin'}${seoDescriptionNetwork(this.stateService.network)} network using the transaction's hash.`);
|
||||
}
|
||||
|
||||
postTx() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div [formGroup]="rateUnitForm" class="text-small text-center">
|
||||
<select formControlName="rateUnits" class="custom-select custom-select-sm form-control-secondary form-control mx-auto" style="width: 100px;" (change)="changeUnits()">
|
||||
<select formControlName="rateUnits" class="custom-select custom-select-sm form-control-secondary form-control mx-auto" style="width: 95px;" (change)="changeUnits()">
|
||||
<option *ngFor="let unit of units" [value]="unit.name">{{ unit.label }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,8 @@ import { WebsocketService } from '../../services/websocket.service';
|
||||
import { RbfTree } from '../../interfaces/node-api.interface';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { seoDescriptionNetwork } from '../../shared/common.utils';
|
||||
|
||||
@Component({
|
||||
selector: 'app-rbf-list',
|
||||
@@ -26,6 +28,7 @@ export class RbfList implements OnInit, OnDestroy {
|
||||
private apiService: ApiService,
|
||||
public stateService: StateService,
|
||||
private websocketService: WebsocketService,
|
||||
private seoService: SeoService,
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
@@ -51,9 +54,12 @@ export class RbfList implements OnInit, OnDestroy {
|
||||
this.isLoading = false;
|
||||
})
|
||||
);
|
||||
|
||||
this.seoService.setTitle($localize`:@@meta.title.rbf-list:RBF Replacements`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.rbf-list:See the most recent RBF replacements on the Bitcoin${seoDescriptionNetwork(this.stateService.network)} network, updated in real-time.`);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.websocketService.stopTrackRbf();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,15 +10,25 @@
|
||||
|
||||
<div *ngIf="countdown > 0" class="warning-label">{{ eventName }} in {{ countdown | number }} block{{ countdown === 1 ? '' : 's' }}!</div>
|
||||
|
||||
<div class="blockchain-wrapper" [class.time-ltr]="timeLtr" [class.time-rtl]="!timeLtr">
|
||||
<div class="blockchain-wrapper" [class.time-ltr]="timeLtr" [class.time-rtl]="!timeLtr" #blockchainWrapper>
|
||||
<div id="blockchain-container" [dir]="timeLtr ? 'rtl' : 'ltr'" #blockchainContainer
|
||||
[class.menu-open]="menuOpen"
|
||||
[class.menu-closing]="menuSliding && !menuOpen"
|
||||
(mousedown)="onMouseDown($event)"
|
||||
(pointerdown)="onPointerDown($event)"
|
||||
(touchmove)="onTouchMove($event)"
|
||||
(dragstart)="onDragStart($event)"
|
||||
(scroll)="onScroll($event)"
|
||||
>
|
||||
<app-blockchain [pageIndex]="pageIndex" [pages]="pages" [blocksPerPage]="blocksPerPage" [minScrollWidth]="minScrollWidth" [scrollableMempool]="true" (mempoolOffsetChange)="onMempoolOffsetChange($event)"></app-blockchain>
|
||||
<app-blockchain
|
||||
[containerWidth]="chainWidth"
|
||||
[pageIndex]="pageIndex"
|
||||
[pages]="pages"
|
||||
[blocksPerPage]="blocksPerPage"
|
||||
[minScrollWidth]="minScrollWidth"
|
||||
[scrollableMempool]="true"
|
||||
(mempoolOffsetChange)="onMempoolOffsetChange($event)"
|
||||
></app-blockchain>
|
||||
</div>
|
||||
<div class="reset-scroll" [class.hidden]="pageIndex === 0" (click)="resetScroll()">
|
||||
<fa-icon [icon]="['fas', 'circle-left']" [fixedWidth]="true"></fa-icon>
|
||||
|
||||
@@ -6,6 +6,20 @@
|
||||
overflow-y: hidden;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
width: calc(100% + 120px);
|
||||
|
||||
transform: translateX(0px);
|
||||
transition: transform 0;
|
||||
|
||||
&.menu-open {
|
||||
transform: translateX(-112.5px);
|
||||
transition: transform 0.25s;
|
||||
}
|
||||
|
||||
&.menu-closing {
|
||||
transform: translateX(0px);
|
||||
transition: transform 0.25s;
|
||||
}
|
||||
}
|
||||
|
||||
#blockchain-container::-webkit-scrollbar {
|
||||
|
||||
@@ -28,8 +28,10 @@ export class StartComponent implements OnInit, OnDestroy, DoCheck {
|
||||
lastMark: MarkBlockState;
|
||||
markBlockSubscription: Subscription;
|
||||
blockCounterSubscription: Subscription;
|
||||
@ViewChild('blockchainWrapper', { static: true }) blockchainWrapper: ElementRef;
|
||||
@ViewChild('blockchainContainer') blockchainContainer: ElementRef;
|
||||
resetScrollSubscription: Subscription;
|
||||
resetScrollSubscription: Subscription;
|
||||
menuSubscription: Subscription;
|
||||
|
||||
isMobile: boolean = false;
|
||||
isiOS: boolean = false;
|
||||
@@ -49,6 +51,12 @@ export class StartComponent implements OnInit, OnDestroy, DoCheck {
|
||||
velocity: number = 0;
|
||||
mempoolOffset: number = 0;
|
||||
|
||||
private resizeObserver: ResizeObserver;
|
||||
chainWidth: number = window.innerWidth;
|
||||
menuOpen: boolean = false;
|
||||
menuSliding: boolean = false;
|
||||
menuTimeout: number;
|
||||
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
) {
|
||||
@@ -151,6 +159,13 @@ export class StartComponent implements OnInit, OnDestroy, DoCheck {
|
||||
this.stateService.resetScroll$.next(false);
|
||||
}
|
||||
});
|
||||
|
||||
this.menuSubscription = this.stateService.menuOpen$.subscribe((open) => {
|
||||
if (this.menuOpen !== open) {
|
||||
this.menuOpen = open;
|
||||
this.applyMenuScroll(this.menuOpen);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onMempoolOffsetChange(offset): void {
|
||||
@@ -171,9 +186,18 @@ export class StartComponent implements OnInit, OnDestroy, DoCheck {
|
||||
}
|
||||
}
|
||||
|
||||
applyMenuScroll(opening: boolean): void {
|
||||
this.menuSliding = true;
|
||||
window.clearTimeout(this.menuTimeout);
|
||||
this.menuTimeout = window.setTimeout(() => {
|
||||
this.menuSliding = false;
|
||||
}, 300);
|
||||
}
|
||||
|
||||
@HostListener('window:resize', ['$event'])
|
||||
onResize(): void {
|
||||
this.isMobile = window.innerWidth <= 767.98;
|
||||
this.chainWidth = window.innerWidth;
|
||||
this.isMobile = this.chainWidth <= 767.98;
|
||||
let firstVisibleBlock;
|
||||
let offset;
|
||||
if (this.blockchainContainer?.nativeElement != null) {
|
||||
@@ -188,7 +212,7 @@ export class StartComponent implements OnInit, OnDestroy, DoCheck {
|
||||
});
|
||||
}
|
||||
|
||||
this.blocksPerPage = Math.ceil(window.innerWidth / this.blockWidth);
|
||||
this.blocksPerPage = Math.ceil(this.chainWidth / this.blockWidth);
|
||||
this.pageWidth = this.blocksPerPage * this.blockWidth;
|
||||
this.minScrollWidth = this.firstPageWidth + (this.pageWidth * 2);
|
||||
|
||||
@@ -295,7 +319,7 @@ export class StartComponent implements OnInit, OnDestroy, DoCheck {
|
||||
onScroll(e) {
|
||||
const middlePage = this.pageIndex === 0 ? this.pages[0] : this.pages[1];
|
||||
// compensate for css transform
|
||||
const translation = (this.isMobile ? window.innerWidth * 0.95 : window.innerWidth * 0.5);
|
||||
const translation = (this.isMobile ? this.chainWidth * 0.95 : this.chainWidth * 0.5);
|
||||
const backThreshold = middlePage.offset + (this.pageWidth * 0.5) + translation;
|
||||
const forwardThreshold = middlePage.offset - (this.pageWidth * 0.5) + translation;
|
||||
const scrollLeft = this.getConvertedScrollOffset();
|
||||
@@ -414,10 +438,10 @@ export class StartComponent implements OnInit, OnDestroy, DoCheck {
|
||||
|
||||
blockInViewport(height: number): boolean {
|
||||
const firstHeight = this.pages[0].height;
|
||||
const translation = (this.isMobile ? window.innerWidth * 0.95 : window.innerWidth * 0.5);
|
||||
const translation = (this.isMobile ? this.chainWidth * 0.95 : this.chainWidth * 0.5);
|
||||
const firstX = this.pages[0].offset - this.getConvertedScrollOffset() + translation;
|
||||
const xPos = firstX + ((firstHeight - height) * 155);
|
||||
return xPos > -55 && xPos < (window.innerWidth - 100);
|
||||
return xPos > -55 && xPos < (this.chainWidth - 100);
|
||||
}
|
||||
|
||||
getConvertedScrollOffset(): number {
|
||||
@@ -458,5 +482,6 @@ export class StartComponent implements OnInit, OnDestroy, DoCheck {
|
||||
this.markBlockSubscription.unsubscribe();
|
||||
this.blockCounterSubscription.unsubscribe();
|
||||
this.resetScrollSubscription.unsubscribe();
|
||||
this.menuSubscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@ export class StatisticsComponent implements OnInit {
|
||||
this.inverted = this.storageService.getValue('inverted-graph') === 'true';
|
||||
this.setFeeLevelDropdownData();
|
||||
this.seoService.setTitle($localize`:@@5d4f792f048fcaa6df5948575d7cb325c9393383:Graphs`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.bitcoin.graphs.mempool:See mempool size (in MvB) and transactions per second (in vB/s) visualized over time.`);
|
||||
this.stateService.networkChanged$.subscribe((network) => this.network = network);
|
||||
this.graphWindowPreference = this.storageService.getValue('graphWindowPreference') ? this.storageService.getValue('graphWindowPreference').trim() : '2h';
|
||||
|
||||
|
||||
@@ -74,6 +74,16 @@
|
||||
<path fill="#FFFFFF" d="M128 768h256v64H128v-64z m320-384H128v64h320v-64z m128 192V448L384 640l192 192V704h320V576H576z m-288-64H128v64h160v-64zM128 704h160v-64H128v64z m576 64h64v128c-1 18-7 33-19 45s-27 18-45 19H64c-35 0-64-29-64-64V192c0-35 29-64 64-64h192C256 57 313 0 384 0s128 57 128 128h192c35 0 64 29 64 64v320h-64V320H64v576h640V768zM128 256h512c0-35-29-64-64-64h-64c-35 0-64-29-64-64s-29-64-64-64-64 29-64 64-29 64-64 64h-64c-35 0-64 29-64 64z" />
|
||||
</svg>
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchCase="'hamburger'">
|
||||
<svg [attr.width]="width" [attr.height]="height" viewBox="0 0 10 10" xmlns="http://www.w3.org/2000/svg" stroke="currentColor">
|
||||
<path stroke-width="0.5" stroke-linecap="round" d="M0.5 2.5 H7 M0.5 5 H5.5 M0.5 7.5 H7"></path>
|
||||
</svg>
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchCase="'anon'">
|
||||
<svg [attr.width]="width" [attr.height]="height" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 9.5v-2a3 3 0 116 0v2c0 1.11-.603 2.08-1.5 2.599v1.224a1 1 0 00.629.928l2.05.82A3.693 3.693 0 0118.5 18.5h-13c0-1.51.92-2.868 2.321-3.428l2.05-.82a1 1 0 00.629-.929v-1.224A2.999 2.999 0 019 9.5z"></path>
|
||||
</svg>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #bitcoinLogo let-color let-width="width" let-height="height" let-viewBox="viewBox">
|
||||
|
||||
@@ -37,6 +37,7 @@ export class TelevisionComponent implements OnInit, OnDestroy {
|
||||
|
||||
ngOnInit() {
|
||||
this.seoService.setTitle($localize`:@@46ce8155c9ab953edeec97e8950b5a21e67d7c4e:TV view`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.tv:See Bitcoin blocks and mempool congestion in real-time in a simplified format perfect for a TV.`);
|
||||
this.websocketService.want(['blocks', 'live-2h-chart', 'mempool-blocks']);
|
||||
|
||||
this.timeLtrSubscription = this.stateService.timeLtr.subscribe((ltr) => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Env, StateService } from '../../services/state.service';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-terms-of-service',
|
||||
@@ -10,5 +11,11 @@ export class TermsOfServiceComponent {
|
||||
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
private seoService: SeoService,
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.seoService.setTitle('Terms of Service');
|
||||
this.seoService.setDescription('Out of respect for the Bitcoin community, the mempool.space website is Bitcoin Only and does not display any advertising.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Env, StateService } from '../../services/state.service';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-trademark-policy',
|
||||
@@ -11,5 +12,11 @@ export class TrademarkPolicyComponent {
|
||||
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
private seoService: SeoService,
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.seoService.setTitle('Trademark Policy');
|
||||
this.seoService.setDescription('An overview of the trademarks registered by Mempool Space K.K. and The Mempool Open Source Project™ and what we consider to be lawful usage of those trademarks.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import { CacheService } from '../../services/cache.service';
|
||||
import { OpenGraphService } from '../../services/opengraph.service';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { seoDescriptionNetwork } from '../../shared/common.utils';
|
||||
import { CpfpInfo } from '../../interfaces/node-api.interface';
|
||||
import { LiquidUnblinding } from './liquid-ublinding';
|
||||
|
||||
@@ -87,6 +88,7 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy {
|
||||
this.seoService.setTitle(
|
||||
$localize`:@@bisq.transaction.browser-title:Transaction: ${this.txId}:INTERPOLATION:`
|
||||
);
|
||||
this.seoService.setDescription($localize`:@@meta.description.bitcoin.transaction:Get real-time status, addresses, fees, script info, and more for ${this.stateService.network==='liquid'||this.stateService.network==='liquidtestnet'?'Liquid':'Bitcoin'}${seoDescriptionNetwork(this.stateService.network)} transaction with txid {txid}.`);
|
||||
this.resetTransaction();
|
||||
return merge(
|
||||
of(true),
|
||||
|
||||
@@ -6,6 +6,13 @@
|
||||
<app-truncate [text]="rbfTransaction.txid" [lastChars]="12" [link]="['/tx/' | relativeUrl, rbfTransaction.txid]"></app-truncate>
|
||||
</div>
|
||||
|
||||
<div *ngIf="acceleratorAvailable && accelerateCtaType === 'alert' && !tx?.status?.confirmed && !tx?.acceleration" class="alert alert-mempool alert-dismissible" role="alert">
|
||||
<span><a class="link accelerator" (click)="onAccelerateClicked()">Accelerate</a> this transaction using Mempool Accelerator ™</span>
|
||||
<button type="button" class="close" aria-label="Close" (click)="dismissAccelAlert()">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="!rbfTransaction || rbfTransaction?.size || tx">
|
||||
<h1 i18n="shared.transaction">Transaction</h1>
|
||||
|
||||
@@ -66,12 +73,22 @@
|
||||
<div class="col-sm">
|
||||
<ng-container *ngTemplateOutlet="feeTable"></ng-container>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</ng-template>
|
||||
|
||||
<!-- Accelerator -->
|
||||
<ng-container *ngIf="!tx?.status?.confirmed && showAccelerationSummary">
|
||||
<div class="title mt-3" id="acceleratePreviewAnchor">
|
||||
<h2>Accelerate</h2>
|
||||
</div>
|
||||
<div class="box">
|
||||
<app-accelerate-preview [tx]="tx" [scrollEvent]="scrollIntoAccelPreview"></app-accelerate-preview>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
|
||||
<ng-template #unconfirmedTemplate>
|
||||
|
||||
<div class="box">
|
||||
@@ -92,16 +109,16 @@
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
<tr *ngIf="!replaced && !isCached">
|
||||
<td class="td-width" i18n="transaction.eta|Transaction ETA">ETA</td>
|
||||
<td class="td-width align-items-center align-middle" i18n="transaction.eta|Transaction ETA">ETA</td>
|
||||
<td>
|
||||
<ng-template [ngIf]="this.mempoolPosition?.block == null" [ngIfElse]="estimationTmpl">
|
||||
<span class="skeleton-loader"></span>
|
||||
</ng-template>
|
||||
<ng-template #estimationTmpl>
|
||||
<ng-template [ngIf]="this.mempoolPosition.block >= 7" [ngIfElse]="belowBlockLimit">
|
||||
<span class="eta d-flex">
|
||||
<span [class]="(acceleratorAvailable && accelerateCtaType === 'button') ? 'etaDeepMempool d-flex justify-content-end align-items-center' : ''">
|
||||
<span i18n="transaction.eta.in-several-hours|Transaction ETA in several hours or more">In several hours (or more)</span>
|
||||
<span class="ml-2"></span><a *ngIf="stateService.env.OFFICIAL_MEMPOOL_SPACE && stateService.env.ACCELERATOR" [href]="'/services/accelerator/accelerate?txid=' + tx.txid" class="btn badge badge-primary accelerate ml-auto" i18n="transaction.accelerate|Accelerate button label">Accelerate</a>
|
||||
<a *ngIf="acceleratorAvailable && accelerateCtaType === 'button' && !tx?.acceleration" [href]="'/services/accelerator/accelerate?txid=' + tx.txid" class="btn btn-sm accelerateDeepMempool" i18n="transaction.accelerate|Accelerate button label" (click)="onAccelerateClicked()">Accelerate</a>
|
||||
</span>
|
||||
</ng-template>
|
||||
<ng-template #belowBlockLimit>
|
||||
@@ -109,9 +126,9 @@
|
||||
<app-time kind="until" [time]="(60 * 1000 * this.mempoolPosition.block) + now" [fastRender]="false" [fixedRender]="true"></app-time>
|
||||
</ng-template>
|
||||
<ng-template #timeEstimateDefault>
|
||||
<span class="d-flex">
|
||||
<span class="eta justify-content-end" [class]="(acceleratorAvailable && accelerateCtaType === 'button') ? 'd-flex align-items-center' : ''">
|
||||
<app-time kind="until" *ngIf="(da$ | async) as da;" [time]="da.timeAvg * (this.mempoolPosition.block + 1) + now + da.timeOffset" [fastRender]="false" [fixedRender]="true"></app-time>
|
||||
<span class="ml-2"></span><a *ngIf="stateService.env.OFFICIAL_MEMPOOL_SPACE && stateService.env.ACCELERATOR" [href]="'/services/accelerator/accelerate?txid=' + tx.txid" class="btn badge badge-primary accelerate ml-auto" i18n="transaction.accelerate|Accelerate button label">Accelerate</a>
|
||||
<a *ngIf="acceleratorAvailable && accelerateCtaType === 'button' && !tx?.acceleration" [href]="'/services/accelerator/accelerate?txid=' + tx.txid" class="btn btn-sm accelerate" i18n="transaction.accelerate|Accelerate button label" (click)="onAccelerateClicked()">Accelerate</a>
|
||||
</span>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
|
||||
@@ -130,7 +130,7 @@
|
||||
}
|
||||
|
||||
.table {
|
||||
tr td {
|
||||
tr td {
|
||||
padding: 0.75rem 0.5rem;
|
||||
@media (min-width: 576px) {
|
||||
padding: 0.75rem 0.75rem;
|
||||
@@ -138,7 +138,7 @@
|
||||
&:last-child {
|
||||
text-align: right;
|
||||
@media (min-width: 850px) {
|
||||
text-align: left;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
.btn {
|
||||
@@ -218,21 +218,52 @@
|
||||
}
|
||||
}
|
||||
|
||||
.link.accelerator {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.eta {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
flex-wrap: wrap;
|
||||
align-content: center;
|
||||
@media (min-width: 850px) {
|
||||
justify-content: space-between;
|
||||
justify-content: left !important;
|
||||
}
|
||||
}
|
||||
|
||||
.accelerate {
|
||||
display: flex !important;
|
||||
align-self: auto;
|
||||
margin-top: 3px;
|
||||
@media (min-width: 850px) {
|
||||
justify-self: start;
|
||||
margin-left: auto;
|
||||
background-color: #653b9c;
|
||||
@media (max-width: 849px) {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.etaDeepMempool {
|
||||
display: flex !important;
|
||||
justify-content: end;
|
||||
flex-wrap: wrap;
|
||||
align-content: center;
|
||||
@media (max-width: 995px) {
|
||||
justify-content: left !important;
|
||||
}
|
||||
@media (max-width: 849px) {
|
||||
justify-content: right !important;
|
||||
}
|
||||
}
|
||||
|
||||
.accelerateDeepMempool {
|
||||
align-self: auto;
|
||||
margin-top: 3px;
|
||||
margin-left: auto;
|
||||
background-color: #653b9c;
|
||||
@media (max-width: 995px) {
|
||||
margin-left: 0px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 849px) {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ import { WebsocketService } from '../../services/websocket.service';
|
||||
import { AudioService } from '../../services/audio.service';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { StorageService } from '../../services/storage.service';
|
||||
import { seoDescriptionNetwork } from '../../shared/common.utils';
|
||||
import { BlockExtended, CpfpInfo, RbfTree, MempoolPosition, DifficultyAdjustment } from '../../interfaces/node-api.interface';
|
||||
import { LiquidUnblinding } from './liquid-ublinding';
|
||||
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
||||
@@ -88,6 +90,10 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
rbfEnabled: boolean;
|
||||
taprootEnabled: boolean;
|
||||
hasEffectiveFeeRate: boolean;
|
||||
accelerateCtaType: 'alert' | 'button' = 'alert';
|
||||
acceleratorAvailable: boolean = this.stateService.env.OFFICIAL_MEMPOOL_SPACE && this.stateService.env.ACCELERATOR && this.stateService.network === '';
|
||||
showAccelerationSummary = false;
|
||||
scrollIntoAccelPreview = false;
|
||||
|
||||
@ViewChild('graphContainer')
|
||||
graphContainer: ElementRef;
|
||||
@@ -104,14 +110,22 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
private apiService: ApiService,
|
||||
private seoService: SeoService,
|
||||
private priceService: PriceService,
|
||||
private storageService: StorageService
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.acceleratorAvailable = this.stateService.env.OFFICIAL_MEMPOOL_SPACE && this.stateService.env.ACCELERATOR && this.stateService.network === '';
|
||||
|
||||
this.websocketService.want(['blocks', 'mempool-blocks']);
|
||||
this.stateService.networkChanged$.subscribe(
|
||||
(network) => (this.network = network)
|
||||
(network) => {
|
||||
this.network = network;
|
||||
this.acceleratorAvailable = this.stateService.env.OFFICIAL_MEMPOOL_SPACE && this.stateService.env.ACCELERATOR && this.stateService.network === '';
|
||||
}
|
||||
);
|
||||
|
||||
this.accelerateCtaType = (this.storageService.getValue('accel-cta-type') as 'alert' | 'button') ?? 'alert';
|
||||
|
||||
this.setFlowEnabled();
|
||||
this.flowPrefSubscription = this.stateService.hideFlow.subscribe((hide) => {
|
||||
this.hideFlow = !!hide;
|
||||
@@ -161,34 +175,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
})
|
||||
)
|
||||
.subscribe((cpfpInfo) => {
|
||||
if (!cpfpInfo || !this.tx) {
|
||||
this.cpfpInfo = null;
|
||||
this.hasEffectiveFeeRate = false;
|
||||
return;
|
||||
}
|
||||
// merge ancestors/descendants
|
||||
const relatives = [...(cpfpInfo.ancestors || []), ...(cpfpInfo.descendants || [])];
|
||||
if (cpfpInfo.bestDescendant && !cpfpInfo.descendants?.length) {
|
||||
relatives.push(cpfpInfo.bestDescendant);
|
||||
}
|
||||
const hasRelatives = !!relatives.length;
|
||||
if (!cpfpInfo.effectiveFeePerVsize && hasRelatives) {
|
||||
let totalWeight =
|
||||
this.tx.weight +
|
||||
relatives.reduce((prev, val) => prev + val.weight, 0);
|
||||
let totalFees =
|
||||
this.tx.fee +
|
||||
relatives.reduce((prev, val) => prev + val.fee, 0);
|
||||
this.tx.effectiveFeePerVsize = totalFees / (totalWeight / 4);
|
||||
} else {
|
||||
this.tx.effectiveFeePerVsize = cpfpInfo.effectiveFeePerVsize;
|
||||
}
|
||||
if (cpfpInfo.acceleration) {
|
||||
this.tx.acceleration = cpfpInfo.acceleration;
|
||||
}
|
||||
|
||||
this.cpfpInfo = cpfpInfo;
|
||||
this.hasEffectiveFeeRate = hasRelatives || (this.tx.effectiveFeePerVsize && (Math.abs(this.tx.effectiveFeePerVsize - this.tx.feePerVsize) > 0.01));
|
||||
this.setCpfpInfo(cpfpInfo);
|
||||
});
|
||||
|
||||
this.fetchRbfSubscription = this.fetchRbfHistory$
|
||||
@@ -259,6 +246,10 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
mempoolPosition: this.mempoolPosition
|
||||
});
|
||||
this.txInBlockIndex = this.mempoolPosition.block;
|
||||
|
||||
if (txPosition.cpfp !== undefined) {
|
||||
this.setCpfpInfo(txPosition.cpfp);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.mempoolPosition = null;
|
||||
@@ -297,6 +288,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.seoService.setTitle(
|
||||
$localize`:@@bisq.transaction.browser-title:Transaction: ${this.txId}:INTERPOLATION:`
|
||||
);
|
||||
this.seoService.setDescription($localize`:@@meta.description.bitcoin.transaction:Get real-time status, addresses, fees, script info, and more for ${this.stateService.network==='liquid'||this.stateService.network==='liquidtestnet'?'Liquid':'Bitcoin'}${seoDescriptionNetwork(this.stateService.network)} transaction with txid {txid}.`);
|
||||
this.resetTransaction();
|
||||
return merge(
|
||||
of(true),
|
||||
@@ -399,7 +391,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.blockConversion = price;
|
||||
})
|
||||
).subscribe();
|
||||
|
||||
|
||||
setTimeout(() => { this.applyFragment(); }, 0);
|
||||
},
|
||||
(error) => {
|
||||
@@ -486,6 +478,20 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.setGraphSize();
|
||||
}
|
||||
|
||||
dismissAccelAlert(): void {
|
||||
this.storageService.setValue('accel-cta-type', 'button');
|
||||
this.accelerateCtaType = 'button';
|
||||
}
|
||||
|
||||
onAccelerateClicked() {
|
||||
if (!this.txId) {
|
||||
return;
|
||||
}
|
||||
this.showAccelerationSummary = true && this.acceleratorAvailable;
|
||||
this.scrollIntoAccelPreview = !this.scrollIntoAccelPreview;
|
||||
return false;
|
||||
}
|
||||
|
||||
handleLoadElectrsTransactionError(error: any): Observable<any> {
|
||||
if (error.status === 404 && /^[a-fA-F0-9]{64}$/.test(this.txId)) {
|
||||
this.websocketService.startMultiTrackTransaction(this.txId);
|
||||
@@ -507,6 +513,37 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
setCpfpInfo(cpfpInfo: CpfpInfo): void {
|
||||
if (!cpfpInfo || !this.tx) {
|
||||
this.cpfpInfo = null;
|
||||
this.hasEffectiveFeeRate = false;
|
||||
return;
|
||||
}
|
||||
// merge ancestors/descendants
|
||||
const relatives = [...(cpfpInfo.ancestors || []), ...(cpfpInfo.descendants || [])];
|
||||
if (cpfpInfo.bestDescendant && !cpfpInfo.descendants?.length) {
|
||||
relatives.push(cpfpInfo.bestDescendant);
|
||||
}
|
||||
const hasRelatives = !!relatives.length;
|
||||
if (!cpfpInfo.effectiveFeePerVsize && hasRelatives) {
|
||||
const totalWeight =
|
||||
this.tx.weight +
|
||||
relatives.reduce((prev, val) => prev + val.weight, 0);
|
||||
const totalFees =
|
||||
this.tx.fee +
|
||||
relatives.reduce((prev, val) => prev + val.fee, 0);
|
||||
this.tx.effectiveFeePerVsize = totalFees / (totalWeight / 4);
|
||||
} else {
|
||||
this.tx.effectiveFeePerVsize = cpfpInfo.effectiveFeePerVsize;
|
||||
}
|
||||
if (cpfpInfo.acceleration) {
|
||||
this.tx.acceleration = cpfpInfo.acceleration;
|
||||
}
|
||||
|
||||
this.cpfpInfo = cpfpInfo;
|
||||
this.hasEffectiveFeeRate = hasRelatives || (this.tx.effectiveFeePerVsize && (Math.abs(this.tx.effectiveFeePerVsize - this.tx.feePerVsize) > 0.01));
|
||||
}
|
||||
|
||||
setFeatures(): void {
|
||||
if (this.tx) {
|
||||
this.segwitEnabled = !this.tx.status.confirmed || isFeatureActive(this.stateService.network, this.tx.status.block_height, 'segwit');
|
||||
|
||||
@@ -69,6 +69,7 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
ngOnInit(): void {
|
||||
this.isLoadingWebSocket$ = this.stateService.isLoadingWebSocket$;
|
||||
this.seoService.resetTitle();
|
||||
this.seoService.resetDescription();
|
||||
this.websocketService.want(['blocks', 'stats', 'mempool-blocks', 'live-2h-chart']);
|
||||
this.websocketService.startTrackRbfSummary();
|
||||
this.network$ = merge(of(''), this.stateService.networkChanged$);
|
||||
|
||||
@@ -155,7 +155,7 @@ ul.no-bull.block-audit code{
|
||||
#doc-nav-desktop.fixed {
|
||||
float: unset;
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
top: 80px;
|
||||
overflow-y: auto;
|
||||
height: calc(100vh - 50px);
|
||||
scrollbar-color: #2d3348 #11131f;
|
||||
|
||||
@@ -44,7 +44,7 @@ export class ApiDocsComponent implements OnInit, AfterViewInit {
|
||||
if (this.faqTemplates) {
|
||||
this.faqTemplates.forEach((x) => this.dict[x.type] = x.template);
|
||||
}
|
||||
this.desktopDocsNavPosition = ( window.pageYOffset > 182 ) ? "fixed" : "relative";
|
||||
this.desktopDocsNavPosition = ( window.pageYOffset > 115 ) ? "fixed" : "relative";
|
||||
this.mobileViewport = window.innerWidth <= 992;
|
||||
}
|
||||
|
||||
@@ -114,7 +114,7 @@ export class ApiDocsComponent implements OnInit, AfterViewInit {
|
||||
}
|
||||
|
||||
onDocScroll() {
|
||||
this.desktopDocsNavPosition = ( window.pageYOffset > 182 ) ? "fixed" : "relative";
|
||||
this.desktopDocsNavPosition = ( window.pageYOffset > 115 ) ? "fixed" : "relative";
|
||||
}
|
||||
|
||||
anchorLinkClick( event: any ) {
|
||||
|
||||
@@ -28,21 +28,6 @@ export class DocsComponent implements OnInit {
|
||||
|
||||
ngOnInit(): void {
|
||||
this.websocket.want(['blocks']);
|
||||
const url = this.route.snapshot.url;
|
||||
if (url[0].path === "faq" ) {
|
||||
this.activeTab = 0;
|
||||
this.seoService.setTitle($localize`:@@docs.faq.button-title:FAQ`);
|
||||
} else if( url[1].path === "rest" ) {
|
||||
this.activeTab = 1;
|
||||
this.seoService.setTitle($localize`:@@e351b40b3869a5c7d19c3d4918cb1ac7aaab95c4:API`);
|
||||
} else if( url[1].path === "websocket" ) {
|
||||
this.activeTab = 2;
|
||||
this.seoService.setTitle($localize`:@@e351b40b3869a5c7d19c3d4918cb1ac7aaab95c4:API`);
|
||||
} else {
|
||||
this.activeTab = 3;
|
||||
this.seoService.setTitle($localize`:@@e351b40b3869a5c7d19c3d4918cb1ac7aaab95c4:API`);
|
||||
}
|
||||
|
||||
this.env = this.stateService.env;
|
||||
this.showWebSocketTab = ( ! ( ( this.stateService.network === "bisq" ) || ( this.stateService.network === "liquidtestnet" ) ) );
|
||||
this.showFaqTab = ( this.env.BASE_MODULE === 'mempool' ) ? true : false;
|
||||
@@ -51,6 +36,40 @@ export class DocsComponent implements OnInit {
|
||||
document.querySelector<HTMLElement>( "html" ).style.scrollBehavior = "smooth";
|
||||
}
|
||||
|
||||
ngDoCheck(): void {
|
||||
|
||||
const url = this.route.snapshot.url;
|
||||
|
||||
if (url[0].path === "faq" ) {
|
||||
this.activeTab = 0;
|
||||
this.seoService.setTitle($localize`:@@meta.title.docs.faq:FAQ`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.docs.faq:Get answers to common questions like: What is a mempool? Why isn't my transaction confirming? How can I run my own instance of The Mempool Open Source Project? And more.`);
|
||||
} else if( url[1].path === "rest" ) {
|
||||
this.activeTab = 1;
|
||||
this.seoService.setTitle($localize`:@@meta.title.docs.rest:REST API`);
|
||||
if( this.stateService.network === 'liquid' || this.stateService.network === 'liquidtestnet' ) {
|
||||
this.seoService.setDescription($localize`:@@meta.description.docs.rest-liquid:Documentation for the liquid.network REST API service: get info on addresses, transactions, assets, blocks, and more.`);
|
||||
} else if( this.stateService.network === 'bisq' ) {
|
||||
this.seoService.setDescription($localize`:@@meta.description.docs.rest-bisq:Documentation for the bisq.markets REST API service: get info on recent trades, current offers, transactions, network state, and more.`);
|
||||
} else {
|
||||
this.seoService.setDescription($localize`:@@meta.description.docs.rest-bitcoin:Documentation for the mempool.space REST API service: get info on addresses, transactions, blocks, fees, mining, the Lightning network, and more.`);
|
||||
}
|
||||
} else if( url[1].path === "websocket" ) {
|
||||
this.activeTab = 2;
|
||||
this.seoService.setTitle($localize`:@@meta.title.docs.websocket:WebSocket API`);
|
||||
if( this.stateService.network === 'liquid' || this.stateService.network === 'liquidtestnet' ) {
|
||||
this.seoService.setDescription($localize`:@@meta.description.docs.websocket-liquid:Documentation for the liquid.network WebSocket API service: get real-time info on blocks, mempools, transactions, addresses, and more.`);
|
||||
} else {
|
||||
this.seoService.setDescription($localize`:@@meta.description.docs.websocket-bitcoin:Documentation for the mempool.space WebSocket API service: get real-time info on blocks, mempools, transactions, addresses, and more.`);
|
||||
}
|
||||
} else {
|
||||
this.activeTab = 3;
|
||||
this.seoService.setTitle($localize`:@@meta.title.docs.websocket:Electrum RPC`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.docs.electrumrpc:Documentation for our Electrum RPC interface: get instant, convenient, and reliable access to an Esplora instance.`);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
document.querySelector<HTMLElement>( "html" ).style.scrollBehavior = "auto";
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ export interface Transaction {
|
||||
ancestors?: Ancestor[];
|
||||
bestDescendant?: BestDescendant | null;
|
||||
cpfpChecked?: boolean;
|
||||
acceleration?: number;
|
||||
acceleration?: boolean;
|
||||
deleteAfter?: number;
|
||||
_unblinded?: any;
|
||||
_deduced?: boolean;
|
||||
|
||||
@@ -27,7 +27,7 @@ export interface CpfpInfo {
|
||||
effectiveFeePerVsize?: number;
|
||||
sigops?: number;
|
||||
adjustedVsize?: number;
|
||||
acceleration?: number;
|
||||
acceleration?: boolean;
|
||||
}
|
||||
|
||||
export interface RbfInfo {
|
||||
|
||||
13
frontend/src/app/interfaces/services.interface.ts
Normal file
13
frontend/src/app/interfaces/services.interface.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { IconName } from '@fortawesome/fontawesome-common-types';
|
||||
|
||||
export type MenuItem = {
|
||||
title: string;
|
||||
i18n: string;
|
||||
faIcon: IconName;
|
||||
link: string;
|
||||
};
|
||||
export type MenuGroup = {
|
||||
title: string;
|
||||
i18n: string;
|
||||
items: MenuItem[];
|
||||
}
|
||||
@@ -95,7 +95,7 @@ export interface TransactionStripped {
|
||||
}
|
||||
|
||||
export interface IBackendInfo {
|
||||
hostname: string;
|
||||
hostname?: string;
|
||||
gitCommit: string;
|
||||
version: string;
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ export class ChannelPreviewComponent implements OnInit {
|
||||
this.openGraphService.waitFor('channel-data-' + this.shortId);
|
||||
this.error = null;
|
||||
this.seoService.setTitle(`Channel: ${params.get('short_id')}`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.lightning.channel:Overview for Lightning channel ${params.get('short_id')}. See channel capacity, the Lightning nodes involved, related on-chain transactions, and more.`);
|
||||
return this.lightningApiService.getChannel$(params.get('short_id'))
|
||||
.pipe(
|
||||
tap((data) => {
|
||||
|
||||
@@ -35,6 +35,7 @@ export class ChannelComponent implements OnInit {
|
||||
.pipe(
|
||||
tap((value) => {
|
||||
this.seoService.setTitle($localize`Channel: ${value.short_id}`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.lightning.channel:Overview for Lightning channel ${value.short_id}. See channel capacity, the Lightning nodes involved, related on-chain transactions, and more.`);
|
||||
}),
|
||||
catchError((err) => {
|
||||
this.error = err;
|
||||
|
||||
@@ -31,6 +31,7 @@ export class GroupPreviewComponent implements OnInit {
|
||||
|
||||
ngOnInit(): void {
|
||||
this.seoService.setTitle(`Mempool.Space Lightning Nodes`);
|
||||
this.seoService.setDescription(`See all Lightning nodes run by mempool.space -- these are the nodes that provide the data on the mempool.space Lightning dashboard.`);
|
||||
|
||||
this.nodes$ = this.activatedRoute.paramMap
|
||||
.pipe(
|
||||
|
||||
@@ -39,6 +39,7 @@ export class GroupComponent implements OnInit {
|
||||
});
|
||||
|
||||
this.seoService.setTitle(`Mempool.space Lightning Nodes`);
|
||||
this.seoService.setDescription(`See all Lightning nodes run by mempool.space -- these are the nodes that provide the data on the mempool.space Lightning dashboard.`);
|
||||
|
||||
this.nodes$ = this.lightningApiService.getNodGroupNodes$('mempool.space')
|
||||
.pipe(
|
||||
|
||||
@@ -25,6 +25,7 @@ export class LightningDashboardComponent implements OnInit, AfterViewInit {
|
||||
|
||||
ngOnInit(): void {
|
||||
this.seoService.setTitle($localize`:@@142e923d3b04186ac6ba23387265d22a2fa404e0:Lightning Explorer`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.lightning.dashboard:Get stats on the Lightning network (aggregate capacity, connectivity, etc) and Lightning nodes (channels, liquidity, etc) and Lightning channels (status, fees, etc).`);
|
||||
|
||||
this.nodesRanking$ = this.lightningApiService.getNodesRanking$().pipe(share());
|
||||
this.statistics$ = this.lightningApiService.getLatestStatistics$().pipe(share());
|
||||
|
||||
@@ -49,6 +49,7 @@ export class NodePreviewComponent implements OnInit {
|
||||
}),
|
||||
map((node) => {
|
||||
this.seoService.setTitle(`Node: ${node.alias}`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.lightning.node:Overview for the Lightning network node named ${node.alias}. See channels, capacity, location, fee stats, and more.`);
|
||||
|
||||
const socketsObject = [];
|
||||
const socketTypesMap = {};
|
||||
|
||||
@@ -60,6 +60,7 @@ export class NodeComponent implements OnInit {
|
||||
}),
|
||||
map((node) => {
|
||||
this.seoService.setTitle($localize`Node: ${node.alias}`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.lightning.node:Overview for the Lightning network node named ${node.alias}. See channels, capacity, location, fee stats, and more.`);
|
||||
this.clearnetSocketCount = 0;
|
||||
this.torSocketCount = 0;
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ export class NodesChannelsMap implements OnInit {
|
||||
@Input() disableSpinner = false;
|
||||
@Output() readyEvent = new EventEmitter();
|
||||
|
||||
channelsObservable: Observable<any>;
|
||||
channelsObservable: Observable<any>;
|
||||
|
||||
center: number[] | undefined;
|
||||
zoom: number | undefined;
|
||||
@@ -41,7 +41,7 @@ export class NodesChannelsMap implements OnInit {
|
||||
chartOptions: EChartsOption = {};
|
||||
chartInitOptions = {
|
||||
renderer: 'canvas',
|
||||
};
|
||||
};
|
||||
|
||||
constructor(
|
||||
private seoService: SeoService,
|
||||
@@ -64,15 +64,16 @@ export class NodesChannelsMap implements OnInit {
|
||||
this.zoom = 1.4;
|
||||
this.center = [0, 10];
|
||||
}
|
||||
|
||||
|
||||
if (this.style === 'graph') {
|
||||
this.seoService.setTitle($localize`Lightning Nodes Channels World Map`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.lightning.node-map:See the channels of non-Tor Lightning network nodes visualized on a world map. Hover/tap on points on the map for node names and details.`);
|
||||
}
|
||||
|
||||
if (['nodepage', 'channelpage'].includes(this.style)) {
|
||||
this.nodeSize = 8;
|
||||
}
|
||||
|
||||
|
||||
this.channelsObservable = this.activatedRoute.paramMap
|
||||
.pipe(
|
||||
delay(100),
|
||||
@@ -81,7 +82,7 @@ export class NodesChannelsMap implements OnInit {
|
||||
if (this.style === 'channelpage' && this.channel.length === 0 || !this.hasLocation) {
|
||||
this.isLoading = false;
|
||||
}
|
||||
|
||||
|
||||
return zip(
|
||||
this.assetsService.getWorldMapJson$,
|
||||
this.style !== 'channelpage' ? this.apiService.getChannelsGeo$(params.get('public_key') ?? undefined, this.style) : [''],
|
||||
@@ -140,7 +141,7 @@ export class NodesChannelsMap implements OnInit {
|
||||
// on top of each other
|
||||
let random = Math.random() * 2 * Math.PI;
|
||||
let random2 = Math.random() * 0.01;
|
||||
|
||||
|
||||
if (!nodesPubkeys[node1UniqueId]) {
|
||||
nodes.push([
|
||||
channel[node1GpsLat] + random2 * Math.cos(random),
|
||||
@@ -167,7 +168,7 @@ export class NodesChannelsMap implements OnInit {
|
||||
}
|
||||
|
||||
const channelLoc = [];
|
||||
channelLoc.push(nodesPubkeys[node1UniqueId].slice(0, 2));
|
||||
channelLoc.push(nodesPubkeys[node1UniqueId].slice(0, 2));
|
||||
channelLoc.push(nodesPubkeys[node2UniqueId].slice(0, 2));
|
||||
channelsLoc.push(channelLoc);
|
||||
}
|
||||
@@ -326,7 +327,7 @@ export class NodesChannelsMap implements OnInit {
|
||||
this.chartInstance.on('finished', () => {
|
||||
this.isLoading = false;
|
||||
});
|
||||
|
||||
|
||||
if (this.style === 'widget') {
|
||||
this.chartInstance.getZr().on('click', (e) => {
|
||||
this.zone.run(() => {
|
||||
@@ -335,7 +336,7 @@ export class NodesChannelsMap implements OnInit {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
this.chartInstance.on('click', (e) => {
|
||||
if (e.data) {
|
||||
this.zone.run(() => {
|
||||
|
||||
@@ -48,6 +48,7 @@ export class NodesMap implements OnInit, OnChanges {
|
||||
ngOnInit(): void {
|
||||
if (!this.widget) {
|
||||
this.seoService.setTitle($localize`:@@af8560ca50882114be16c951650f83bca73161a7:Lightning Nodes World Map`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.lightning.node-channel-map:See the locations of non-Tor Lightning network nodes visualized on a world map. Hover/tap on points on the map for node names and details.`);
|
||||
}
|
||||
|
||||
if (!this.inputNodes$) {
|
||||
|
||||
@@ -65,6 +65,7 @@ export class NodesNetworksChartComponent implements OnInit {
|
||||
this.miningWindowPreference = '3y';
|
||||
} else {
|
||||
this.seoService.setTitle($localize`:@@b420668a91f8ebaf6e6409c4ba87f1d45961d2bd:Lightning Nodes Per Network`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.lightning.nodes-network:See the number of Lightning network nodes visualized over time by network: clearnet only (IPv4, IPv6), darknet (Tor, I2p, cjdns), and both.`);
|
||||
this.miningWindowPreference = this.miningService.getDefaultTimespan('all');
|
||||
}
|
||||
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
|
||||
@@ -375,7 +376,7 @@ export class NodesNetworksChartComponent implements OnInit {
|
||||
// We create dummy duplicated series so when we use the data zoom, the y axis
|
||||
// both scales properly
|
||||
const invisibleSerie = {...serie};
|
||||
invisibleSerie.name = 'ignored' + Math.random().toString();
|
||||
invisibleSerie.name = 'ignored' + Math.random().toString();
|
||||
invisibleSerie.stack = 'ignored';
|
||||
invisibleSerie.yAxisIndex = 1;
|
||||
invisibleSerie.lineStyle = {
|
||||
|
||||
@@ -44,7 +44,7 @@ export class NodesPerCountryChartComponent implements OnInit {
|
||||
|
||||
ngOnInit(): void {
|
||||
this.seoService.setTitle($localize`:@@9d3ad4c6623870d96b65fb7a708fed6ce7c20044:Lightning Nodes Per Country`);
|
||||
|
||||
this.seoService.setDescription($localize`:@@meta.description.lightning.nodes-country-overview:See a geographical breakdown of the Lightning network: how many Lightning nodes are hosted in countries around the world, aggregate BTC capacity for each country, and more.`);
|
||||
this.nodesPerCountryObservable$ = this.apiService.getNodesPerCountry$()
|
||||
.pipe(
|
||||
map(data => {
|
||||
|
||||
@@ -33,6 +33,7 @@ export class NodesPerCountry implements OnInit {
|
||||
.pipe(
|
||||
map(response => {
|
||||
this.seoService.setTitle($localize`Lightning nodes in ${response.country.en}`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.lightning.nodes-country:Explore all the Lightning nodes hosted in ${response.country.en} and see an overview of each node's capacity, number of open channels, and more.`);
|
||||
|
||||
this.country = {
|
||||
name: response.country.en,
|
||||
@@ -47,7 +48,7 @@ export class NodesPerCountry implements OnInit {
|
||||
iso: response.nodes[i].iso_code,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
const sumLiquidity = response.nodes.reduce((partialSum, a) => partialSum + a.capacity, 0);
|
||||
const sumChannels = response.nodes.reduce((partialSum, a) => partialSum + a.channels, 0);
|
||||
const isps = {};
|
||||
@@ -70,14 +71,14 @@ export class NodesPerCountry implements OnInit {
|
||||
isps[node.isp].asns.push(node.as_number);
|
||||
}
|
||||
isps[node.isp].count++;
|
||||
|
||||
|
||||
if (isps[node.isp].count > topIsp.count) {
|
||||
topIsp.count = isps[node.isp].count;
|
||||
topIsp.id = isps[node.isp].asns.join(',');
|
||||
topIsp.name = node.isp;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
nodes: response.nodes,
|
||||
sumLiquidity: sumLiquidity,
|
||||
|
||||
@@ -42,6 +42,7 @@ export class NodesPerISPPreview implements OnInit {
|
||||
id: this.route.snapshot.params.isp.split(',').join(', ')
|
||||
};
|
||||
this.seoService.setTitle($localize`Lightning nodes on ISP: ${response.isp} [AS${this.route.snapshot.params.isp}]`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.lightning.nodes-isp:Browse all Bitcoin Lightning nodes using the ${response.isp} [AS${this.route.snapshot.params.isp}] ISP and see aggregate stats like total number of nodes, total capacity, and more for the ISP.`);
|
||||
|
||||
for (const i in response.nodes) {
|
||||
response.nodes[i].geolocation = <GeolocationData>{
|
||||
|
||||
@@ -37,6 +37,7 @@ export class NodesPerISP implements OnInit {
|
||||
id: this.route.snapshot.params.isp.split(',').join(', ')
|
||||
};
|
||||
this.seoService.setTitle($localize`Lightning nodes on ISP: ${response.isp} [AS${this.route.snapshot.params.isp}]`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.lightning.nodes-isp:Browse all Bitcoin Lightning nodes using the ${response.isp} [AS${this.route.snapshot.params.isp}] ISP and see aggregate stats like total number of nodes, total capacity, and more for the ISP.`);
|
||||
|
||||
for (const i in response.nodes) {
|
||||
response.nodes[i].geolocation = <GeolocationData>{
|
||||
|
||||
@@ -25,6 +25,7 @@ export class OldestNodes implements OnInit {
|
||||
ngOnInit(): void {
|
||||
if (!this.widget) {
|
||||
this.seoService.setTitle($localize`Oldest lightning nodes`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.lightning.ranking.oldest:See the oldest nodes on the Lightning network along with their capacity, number of channels, location, etc.`);
|
||||
}
|
||||
|
||||
for (let i = 1; i <= (this.widget ? 10 : 100); ++i) {
|
||||
|
||||
@@ -15,7 +15,7 @@ import { LightningApiService } from '../../lightning-api.service';
|
||||
export class TopNodesPerCapacity implements OnInit {
|
||||
@Input() nodes$: Observable<INodesRanking>;
|
||||
@Input() widget: boolean = false;
|
||||
|
||||
|
||||
topNodesPerCapacity$: Observable<ITopNodesPerCapacity[]>;
|
||||
skeletonRows: number[] = [];
|
||||
currency$: Observable<string>;
|
||||
@@ -31,6 +31,7 @@ export class TopNodesPerCapacity implements OnInit {
|
||||
|
||||
if (!this.widget) {
|
||||
this.seoService.setTitle($localize`:@@2d9883d230a47fbbb2ec969e32a186597ea27405:Liquidity Ranking`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.lightning.ranking.liquidity:See Lightning nodes with the most BTC liquidity deployed along with high-level stats like number of open channels, location, node age, and more.`);
|
||||
}
|
||||
|
||||
for (let i = 1; i <= (this.widget ? 6 : 100); ++i) {
|
||||
|
||||
@@ -35,6 +35,7 @@ export class TopNodesPerChannels implements OnInit {
|
||||
|
||||
if (this.widget === false) {
|
||||
this.seoService.setTitle($localize`:@@c50bf442cf99f6fc5f8b687c460f33234b879869:Connectivity Ranking`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.lightning.ranking.channels:See Lightning nodes with the most channels open along with high-level stats like total node capacity, node age, and more.`);
|
||||
|
||||
this.topNodesPerChannels$ = this.apiService.getTopNodesByChannels$().pipe(
|
||||
map((ranking) => {
|
||||
|
||||
@@ -20,6 +20,7 @@ export class NodesRankingsDashboard implements OnInit {
|
||||
|
||||
ngOnInit(): void {
|
||||
this.seoService.setTitle($localize`Top lightning nodes`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.lightning.rankings-dashboard:See top the Lightning network nodes ranked by liquidity, connectivity, and age.`);
|
||||
this.nodesRanking$ = this.lightningApiService.getNodesRanking$().pipe(share());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,6 +64,7 @@ export class LightningStatisticsChartComponent implements OnInit {
|
||||
this.miningWindowPreference = '3y';
|
||||
} else {
|
||||
this.seoService.setTitle($localize`:@@ea8db27e6db64f8b940711948c001a1100e5fe9f:Lightning Network Capacity`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.lightning.stats-chart:See the capacity of the Lightning network visualized over time in terms of the number of open channels and total bitcoin capacity.`);
|
||||
this.miningWindowPreference = this.miningService.getDefaultTimespan('all');
|
||||
}
|
||||
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
|
||||
|
||||
@@ -4,10 +4,13 @@ import { CpfpInfo, OptimizedMempoolStats, AddressInformation, LiquidPegs, ITrans
|
||||
PoolStat, BlockExtended, TransactionStripped, RewardStats, AuditScore, BlockSizesAndWeights, RbfTree, BlockAudit } from '../interfaces/node-api.interface';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { StateService } from './state.service';
|
||||
import { WebsocketResponse } from '../interfaces/websocket.interface';
|
||||
import { IBackendInfo, WebsocketResponse } from '../interfaces/websocket.interface';
|
||||
import { Outspend, Transaction } from '../interfaces/electrs.interface';
|
||||
import { Conversion } from './price.service';
|
||||
import { MenuGroup } from '../interfaces/services.interface';
|
||||
import { StorageService } from './storage.service';
|
||||
|
||||
// Todo - move to config.json
|
||||
const SERVICES_API_PREFIX = `/api/v1/services`;
|
||||
|
||||
@Injectable({
|
||||
@@ -20,6 +23,7 @@ export class ApiService {
|
||||
constructor(
|
||||
private httpClient: HttpClient,
|
||||
private stateService: StateService,
|
||||
private storageService: StorageService
|
||||
) {
|
||||
this.apiBaseUrl = ''; // use relative URL by default
|
||||
if (!stateService.isBrowser) { // except when inside AU SSR process
|
||||
@@ -32,6 +36,12 @@ export class ApiService {
|
||||
}
|
||||
this.apiBasePath = network ? '/' + network : '';
|
||||
});
|
||||
|
||||
if (this.stateService.env.GIT_COMMIT_HASH_MEMPOOL_SPACE) {
|
||||
this.getServicesBackendInfo$().subscribe(version => {
|
||||
this.stateService.servicesBackendInfo$.next(version);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
list2HStatistics$(): Observable<OptimizedMempoolStats[]> {
|
||||
@@ -95,7 +105,7 @@ export class ApiService {
|
||||
}
|
||||
|
||||
getAboutPageProfiles$(): Observable<any[]> {
|
||||
return this.httpClient.get<any[]>(this.apiBaseUrl + '/api/v1/about-page');
|
||||
return this.httpClient.get<any[]>(this.apiBaseUrl + '/api/v1/services/sponsors');
|
||||
}
|
||||
|
||||
getOgs$(): Observable<any> {
|
||||
@@ -334,9 +344,50 @@ export class ApiService {
|
||||
/**
|
||||
* Services
|
||||
*/
|
||||
getNodeOwner$(publicKey: string) {
|
||||
|
||||
getNodeOwner$(publicKey: string): Observable<any> {
|
||||
let params = new HttpParams()
|
||||
.set('node_public_key', publicKey);
|
||||
return this.httpClient.get<any>(`${SERVICES_API_PREFIX}/lightning/claim/current`, { params, observe: 'response' });
|
||||
}
|
||||
|
||||
getUserMenuGroups$(): Observable<MenuGroup[]> {
|
||||
const auth = this.storageService.getAuth();
|
||||
if (!auth) {
|
||||
return of(null);
|
||||
}
|
||||
|
||||
return this.httpClient.get<MenuGroup[]>(`${SERVICES_API_PREFIX}/account/menu`);
|
||||
}
|
||||
|
||||
getUserInfo$(): Observable<any> {
|
||||
const auth = this.storageService.getAuth();
|
||||
if (!auth) {
|
||||
return of(null);
|
||||
}
|
||||
|
||||
return this.httpClient.get<any>(`${SERVICES_API_PREFIX}/account`);
|
||||
}
|
||||
|
||||
logout$(): Observable<any> {
|
||||
const auth = this.storageService.getAuth();
|
||||
if (!auth) {
|
||||
return of(null);
|
||||
}
|
||||
|
||||
localStorage.removeItem('auth');
|
||||
return this.httpClient.post(`${SERVICES_API_PREFIX}/auth/logout`, {});
|
||||
}
|
||||
|
||||
getServicesBackendInfo$(): Observable<IBackendInfo> {
|
||||
return this.httpClient.get<IBackendInfo>(`${SERVICES_API_PREFIX}/version`);
|
||||
}
|
||||
|
||||
estimate$(txInput: string) {
|
||||
return this.httpClient.post<any>(`${SERVICES_API_PREFIX}/accelerator/estimate`, { txInput: txInput }, { observe: 'response' });
|
||||
}
|
||||
|
||||
accelerate$(txInput: string, userBid: number) {
|
||||
return this.httpClient.post<any>(`${SERVICES_API_PREFIX}/accelerator/accelerate`, { txInput: txInput, userBid: userBid });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { StateService } from './state.service';
|
||||
export class SeoService {
|
||||
network = '';
|
||||
baseTitle = 'mempool';
|
||||
baseDescription = 'Explore the full Bitcoin ecosystem with The Mempool Open Project™.';
|
||||
|
||||
constructor(
|
||||
private titleService: Title,
|
||||
@@ -52,6 +53,18 @@ export class SeoService {
|
||||
this.resetTitle();
|
||||
}
|
||||
|
||||
setDescription(newDescription: string): void {
|
||||
this.metaService.updateTag({ name: 'description', content: newDescription});
|
||||
this.metaService.updateTag({ name: 'twitter:description', content: newDescription});
|
||||
this.metaService.updateTag({ property: 'og:description', content: newDescription});
|
||||
}
|
||||
|
||||
resetDescription(): void {
|
||||
this.metaService.updateTag({ name: 'description', content: this.getDescription()});
|
||||
this.metaService.updateTag({ name: 'twitter:description', content: this.getDescription()});
|
||||
this.metaService.updateTag({ property: 'og:description', content: this.getDescription()});
|
||||
}
|
||||
|
||||
getTitle(): string {
|
||||
if (this.network === 'testnet')
|
||||
return this.baseTitle + ' - Bitcoin Testnet';
|
||||
@@ -66,17 +79,24 @@ export class SeoService {
|
||||
return this.baseTitle + ' - ' + (this.network ? this.ucfirst(this.network) : 'Bitcoin') + ' Explorer';
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
if ( (this.network === 'testnet') || (this.network === 'signet') || (this.network === '') || (this.network == 'mainnet') )
|
||||
return this.baseDescription + ' See the real-time status of your transactions, browse network stats, and more.';
|
||||
if ( (this.network === 'liquid') || (this.network === 'liquidtestnet') )
|
||||
return this.baseDescription + ' See Liquid transactions & assets, get network info, and more.';
|
||||
if (this.network === 'bisq')
|
||||
return this.baseDescription + ' See Bisq market prices, trading activity, and more.';
|
||||
}
|
||||
|
||||
ucfirst(str: string) {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
}
|
||||
|
||||
clearSoft404() {
|
||||
window['soft404'] = false;
|
||||
console.log('cleared soft 404');
|
||||
}
|
||||
|
||||
logSoft404() {
|
||||
window['soft404'] = true;
|
||||
console.log('set soft 404');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,13 @@ import { Inject, Injectable, PLATFORM_ID, LOCALE_ID } from '@angular/core';
|
||||
import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable, merge } from 'rxjs';
|
||||
import { Transaction } from '../interfaces/electrs.interface';
|
||||
import { IBackendInfo, MempoolBlock, MempoolBlockDelta, MempoolInfo, Recommendedfees, ReplacedTransaction, ReplacementInfo, TransactionStripped } from '../interfaces/websocket.interface';
|
||||
import { BlockExtended, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, RbfTree } from '../interfaces/node-api.interface';
|
||||
import { BlockExtended, CpfpInfo, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, RbfTree } from '../interfaces/node-api.interface';
|
||||
import { Router, NavigationStart } from '@angular/router';
|
||||
import { isPlatformBrowser } from '@angular/common';
|
||||
import { filter, map, scan, shareReplay } from 'rxjs/operators';
|
||||
import { StorageService } from './storage.service';
|
||||
import { hasTouchScreen } from '../shared/pipes/bytes-pipe/utils';
|
||||
import { ApiService } from './api.service';
|
||||
|
||||
export interface MarkBlockState {
|
||||
blockHeight?: number;
|
||||
@@ -48,6 +49,8 @@ export interface Env {
|
||||
SIGNET_BLOCK_AUDIT_START_HEIGHT: number;
|
||||
HISTORICAL_PRICE: boolean;
|
||||
ACCELERATOR: boolean;
|
||||
GIT_COMMIT_HASH_MEMPOOL_SPACE?: string;
|
||||
PACKAGE_JSON_VERSION_MEMPOOL_SPACE?: string;
|
||||
}
|
||||
|
||||
const defaultEnv: Env = {
|
||||
@@ -113,13 +116,14 @@ export class StateService {
|
||||
utxoSpent$ = new Subject<object>();
|
||||
difficultyAdjustment$ = new ReplaySubject<DifficultyAdjustment>(1);
|
||||
mempoolTransactions$ = new Subject<Transaction>();
|
||||
mempoolTxPosition$ = new Subject<{ txid: string, position: MempoolPosition}>();
|
||||
mempoolTxPosition$ = new Subject<{ txid: string, position: MempoolPosition, cpfp: CpfpInfo | null}>();
|
||||
blockTransactions$ = new Subject<Transaction>();
|
||||
isLoadingWebSocket$ = new ReplaySubject<boolean>(1);
|
||||
isLoadingMempool$ = new BehaviorSubject<boolean>(true);
|
||||
vbytesPerSecond$ = new ReplaySubject<number>(1);
|
||||
previousRetarget$ = new ReplaySubject<number>(1);
|
||||
backendInfo$ = new ReplaySubject<IBackendInfo>(1);
|
||||
servicesBackendInfo$ = new ReplaySubject<IBackendInfo>(1);
|
||||
loadingIndicators$ = new ReplaySubject<ILoadingIndicators>(1);
|
||||
recommendedFees$ = new ReplaySubject<Recommendedfees>(1);
|
||||
chainTip$ = new ReplaySubject<number>(-1);
|
||||
@@ -143,6 +147,7 @@ export class StateService {
|
||||
rateUnits$: BehaviorSubject<string>;
|
||||
|
||||
searchFocus$: Subject<boolean> = new Subject<boolean>();
|
||||
menuOpen$: BehaviorSubject<boolean> = new BehaviorSubject(false);
|
||||
|
||||
constructor(
|
||||
@Inject(PLATFORM_ID) private platformId: any,
|
||||
|
||||
@@ -56,4 +56,12 @@ export class StorageService {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
getAuth(): any | null {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem('auth'));
|
||||
} catch(e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,4 +135,21 @@ export function haversineDistance(lat1: number, lon1: number, lat2: number, lon2
|
||||
|
||||
export function kmToMiles(km: number): number {
|
||||
return km * 0.62137119;
|
||||
}
|
||||
|
||||
const roundNumbers = [1, 2, 5, 10, 15, 20, 25, 50, 75, 100, 125, 150, 175, 200, 250, 300, 350, 400, 450, 500, 600, 700, 750, 800, 900, 1000];
|
||||
export function nextRoundNumber(num: number): number {
|
||||
const log = Math.floor(Math.log10(num));
|
||||
const factor = log >= 3 ? Math.pow(10, log - 2) : 1;
|
||||
num /= factor;
|
||||
return factor * (roundNumbers.find(val => val >= num) || roundNumbers[roundNumbers.length - 1]);
|
||||
}
|
||||
|
||||
export function seoDescriptionNetwork(network: string): string {
|
||||
if( network === 'liquidtestnet' || network === 'testnet' ) {
|
||||
return ' Testnet';
|
||||
} else if( network === 'signet' || network === 'testnet' ) {
|
||||
return ' ' + network.charAt(0).toUpperCase() + network.slice(1);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
@@ -1,19 +1,19 @@
|
||||
<ng-template [ngIf]="confirmations">
|
||||
<button type="button" class="btn btn-sm btn-success {{buttonClass}}">
|
||||
<button type="button" class="btn btn-sm btn-success no-cursor {{buttonClass}}">
|
||||
<ng-container *ngTemplateOutlet="confirmations == 1 ? confirmationSingular : confirmationPlural; context: {$implicit: confirmations}"></ng-container>
|
||||
<ng-template #confirmationSingular let-i i18n="shared.confirmation-count.singular|Transaction singular confirmation count">{{ i }} confirmation</ng-template>
|
||||
<ng-template #confirmationPlural let-i i18n="shared.confirmation-count.plural|Transaction plural confirmation count">{{ i }} confirmations</ng-template>
|
||||
</button>
|
||||
</ng-template>
|
||||
<ng-template [ngIf]="!confirmations && height != null">
|
||||
<button type="button" class="btn btn-sm btn-success {{buttonClass}}" i18n="transaction.confirmed|Transaction confirmed state">Confirmed</button>
|
||||
<button type="button" class="btn btn-sm btn-success no-cursor {{buttonClass}}" i18n="transaction.confirmed|Transaction confirmed state">Confirmed</button>
|
||||
</ng-template>
|
||||
<ng-template [ngIf]="!hideUnconfirmed && !confirmations && replaced">
|
||||
<button type="button" class="btn btn-sm btn-warning {{buttonClass}}" i18n="transaction.replaced|Transaction replaced state">Replaced</button>
|
||||
<button type="button" class="btn btn-sm btn-warning no-cursor {{buttonClass}}" i18n="transaction.replaced|Transaction replaced state">Replaced</button>
|
||||
</ng-template>
|
||||
<ng-template [ngIf]="!hideUnconfirmed && !confirmations && !replaced && removed">
|
||||
<button type="button" class="btn btn-sm btn-warning {{buttonClass}}" i18n="transaction.audit.removed|Transaction removed state">Removed</button>
|
||||
<button type="button" class="btn btn-sm btn-warning no-cursor {{buttonClass}}" i18n="transaction.audit.removed|Transaction removed state">Removed</button>
|
||||
</ng-template>
|
||||
<ng-template [ngIf]="!hideUnconfirmed && chainTip != null && !confirmations && !replaced && !removed">
|
||||
<button type="button" class="btn btn-sm btn-danger {{buttonClass}}" i18n="transaction.unconfirmed|Transaction unconfirmed state">Unconfirmed</button>
|
||||
<button type="button" class="btn btn-sm btn-danger no-cursor {{buttonClass}}" i18n="transaction.unconfirmed|Transaction unconfirmed state">Unconfirmed</button>
|
||||
</ng-template>
|
||||
@@ -0,0 +1,4 @@
|
||||
.no-cursor {
|
||||
cursor: default !important;
|
||||
pointer-events: none;
|
||||
}
|
||||
@@ -1,12 +1,17 @@
|
||||
<footer>
|
||||
<div class="container-fluid">
|
||||
<div class="row main">
|
||||
<footer [class]="{'services': isServicesPage}">
|
||||
<div class="container-fluid">
|
||||
|
||||
<div class="row main" [class]="{'services': isServicesPage}">
|
||||
<div class="col-md-12 branding mt-2">
|
||||
<div class="main-logo">
|
||||
<div class="main-logo" [class]="{'services': isServicesPage}">
|
||||
<app-svg-images *ngIf="officialMempoolSpace" name="officialMempoolSpace" viewBox="0 0 500 126"></app-svg-images>
|
||||
<app-svg-images *ngIf="!officialMempoolSpace" name="mempoolSpace" viewBox="0 0 500 126"></app-svg-images>
|
||||
</div>
|
||||
<div class="site-options">
|
||||
<p class="d-block d-sm-none">
|
||||
<ng-container i18n="@@7deec1c1520f06170e1f8e8ddfbe4532312f638f">Explore the full Bitcoin ecosystem</ng-container>
|
||||
<ng-template [ngIf]="locale.substr(0, 2) === 'en'"> ™</ng-template>
|
||||
</p>
|
||||
<div class="site-options d-flex justify-content-center align-items-center" [class]="{'services': isServicesPage}">
|
||||
<div class="selector">
|
||||
<app-language-selector></app-language-selector>
|
||||
</div>
|
||||
@@ -16,16 +21,22 @@
|
||||
<div class="selector">
|
||||
<app-rate-unit-selector></app-rate-unit-selector>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="officialMempoolSpace && stateService.env.ACCELERATOR" class="cta">
|
||||
<a class="btn btn-purple sponsor" [routerLink]="['/login' | relativeUrl]">
|
||||
<a *ngIf="servicesEnabled" class="btn btn-purple sponsor d-none d-sm-flex justify-content-center" [routerLink]="['/login' | relativeUrl]">
|
||||
<span *ngIf="loggedIn" i18n="shared.my-account">My Account</span>
|
||||
<span *ngIf="!loggedIn" i18n="shared.sign-in">Sign In / Sign Up</span>
|
||||
</a>
|
||||
</div>
|
||||
<a *ngIf="servicesEnabled" class="btn btn-purple sponsor d-flex d-sm-none justify-content-center ml-auto mr-auto mt-3 mb-2" [routerLink]="['/login' | relativeUrl]">
|
||||
<span *ngIf="loggedIn" i18n="shared.my-account">My Account</span>
|
||||
<span *ngIf="!loggedIn" i18n="shared.sign-in">Sign In / Sign Up</span>
|
||||
</a>
|
||||
<p class="d-none d-sm-block">
|
||||
<ng-container i18n="@@7deec1c1520f06170e1f8e8ddfbe4532312f638f">Explore the full Bitcoin ecosystem</ng-container>
|
||||
<ng-template [ngIf]="locale.substr(0, 2) === 'en'"> ™</ng-template>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row col-md-12 link-tree">
|
||||
<div class="row col-md-12 link-tree" [class]="{'services': isServicesPage}">
|
||||
<div class="links">
|
||||
<p class="category">Explore</p>
|
||||
<p><a [routerLink]="['/mining' | relativeUrl]">Mining Dashboard</a></p>
|
||||
@@ -67,7 +78,6 @@
|
||||
<p><a [routerLink]="['/privacy-policy']" i18n="shared.privacy-policy|Privacy Policy">Privacy Policy</a></p>
|
||||
<p><a [routerLink]="['/trademark-policy']">Trademark Policy</a></p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="row social-links">
|
||||
<div class="col-sm-12">
|
||||
@@ -79,13 +89,15 @@
|
||||
<a href="https://mempool.chat" target="_blank"><svg fill="#fff" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Matrix</title><path d="M.632.55v22.9H2.28V24H0V0h2.28v.55zm7.043 7.26v1.157h.033c.309-.443.683-.784 1.117-1.024.433-.245.936-.365 1.5-.365.54 0 1.033.107 1.481.314.448.208.785.582 1.02 1.108.254-.374.6-.706 1.034-.992.434-.287.95-.43 1.546-.43.453 0 .872.056 1.26.167.388.11.716.286.993.53.276.245.489.559.646.951.152.392.23.863.23 1.417v5.728h-2.349V11.52c0-.286-.01-.559-.032-.812a1.755 1.755 0 0 0-.18-.66 1.106 1.106 0 0 0-.438-.448c-.194-.11-.457-.166-.785-.166-.332 0-.6.064-.803.189a1.38 1.38 0 0 0-.48.499 1.946 1.946 0 0 0-.231.696 5.56 5.56 0 0 0-.06.785v4.768h-2.35v-4.8c0-.254-.004-.503-.018-.752a2.074 2.074 0 0 0-.143-.688 1.052 1.052 0 0 0-.415-.503c-.194-.125-.476-.19-.854-.19-.111 0-.259.024-.439.074-.18.051-.36.143-.53.282-.171.138-.319.337-.439.595-.12.259-.18.6-.18 1.02v4.966H5.46V7.81zm15.693 15.64V.55H21.72V0H24v24h-2.28v-.55z"/></svg></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row version">
|
||||
<div class="row version" [style]="{'background-color': isServicesPage ? '#1d1f31' : ''}">
|
||||
<div class="col-sm-12">
|
||||
<p *ngIf="officialMempoolSpace">{{ (backendInfo$ | async)?.hostname }} (v{{ (backendInfo$ | async )?.version }}) [<a target="_blank" href="https://github.com/mempool/mempool/commit/{{ (backendInfo$ | async )?.gitCommit | slice:0:8 }}">{{ (backendInfo$ | async )?.gitCommit | slice:0:8 }}</a>]</p>
|
||||
|
||||
<p *ngIf="officialMempoolSpace">
|
||||
<span>{{ (backendInfo$ | async)?.hostname }} (v{{ (backendInfo$ | async )?.version }}) [<a target="_blank" href="https://github.com/mempool/mempool/commit/{{ (backendInfo$ | async )?.gitCommit | slice:0:8 }}">{{ (backendInfo$ | async )?.gitCommit | slice:0:8 }}</a>]</span>
|
||||
<span *ngIf="stateService.env.GIT_COMMIT_HASH_MEMPOOL_SPACE"> - (v{{ (servicesBackendInfo$ | async )?.version }}) [{{ (servicesBackendInfo$ | async )?.gitCommit | slice:0:8 }}]</span>
|
||||
</p>
|
||||
<p *ngIf="!officialMempoolSpace">v{{ packetJsonVersion }} [<a target="_blank" href="https://github.com/mempool/mempool/commit/{{ frontendGitCommitHash }}">{{ frontendGitCommitHash }}</a>]</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</footer>
|
||||
|
||||
@@ -15,10 +15,16 @@ footer .row.main {
|
||||
padding: 40px 0 24px 0;
|
||||
max-width: 1140px;
|
||||
margin: 0 auto;
|
||||
&.services {
|
||||
@media (min-width: 1201px) {
|
||||
padding-left: 50px;
|
||||
padding-right: 50px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
footer .row.main .branding > p {
|
||||
margin-bottom: 45px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
footer .row.main .branding .btn {
|
||||
@@ -58,7 +64,7 @@ footer .row.main .links .category:not(:first-child) {
|
||||
|
||||
footer .site-options {
|
||||
float: right;
|
||||
margin-top: -8px;
|
||||
margin-top: -20px;
|
||||
}
|
||||
|
||||
footer .selector {
|
||||
@@ -72,6 +78,12 @@ footer .row.link-tree {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: nowrap;
|
||||
&.services {
|
||||
@media (min-width: 1201px) {
|
||||
padding-left: 65px;
|
||||
padding-right: 65px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
footer .row.social-links {
|
||||
@@ -88,8 +100,9 @@ footer .row.social-links svg {
|
||||
}
|
||||
|
||||
footer .row.version {
|
||||
padding: 20px !important;
|
||||
background-color: #11131f;
|
||||
padding-top: 20px !important;
|
||||
padding-bottom: 20px !important;
|
||||
background-color: #1d1f31;
|
||||
}
|
||||
|
||||
footer .row.version p {
|
||||
@@ -109,6 +122,13 @@ footer .row.version p a {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
footer .sponsor {
|
||||
height: 31px;
|
||||
align-items: center;
|
||||
margin-left: 5px;
|
||||
max-width: 160px;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
|
||||
.main-logo {
|
||||
@@ -156,6 +176,7 @@ footer .row.version p a {
|
||||
|
||||
footer .row.main .branding {
|
||||
text-align: center;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.main-logo {
|
||||
@@ -165,18 +186,61 @@ footer .row.version p a {
|
||||
|
||||
footer .site-options {
|
||||
float: none;
|
||||
margin-top: 30px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
footer .row.social-links {
|
||||
margin: 48px 0 24px 0;
|
||||
}
|
||||
|
||||
footer .selector {
|
||||
margin: 10px 0 0 0;
|
||||
}
|
||||
|
||||
footer .selector:not(:last-child) {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1147px) {
|
||||
|
||||
.services.main-logo {
|
||||
width: 220px;
|
||||
}
|
||||
|
||||
footer .services.row.link-tree {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
footer .services.row.social-links svg {
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
footer .services.row.link-tree {
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
footer .services.link-tree .links {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
footer .services.row.main .branding {
|
||||
text-align: center;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.services.main-logo {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
footer .services.site-options {
|
||||
float: none;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
footer .services.row.social-links {
|
||||
margin: 48px 0 24px 0;
|
||||
}
|
||||
|
||||
footer .services.selector:not(:last-child) {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit, Inject, LOCALE_ID } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit, Inject, LOCALE_ID, HostListener } from '@angular/core';
|
||||
import { Router, ActivatedRoute } from '@angular/router';
|
||||
import { Observable, merge, of, Subject, Subscription } from 'rxjs';
|
||||
import { tap, takeUntil } from 'rxjs/operators';
|
||||
import { Env, StateService } from '../../../services/state.service';
|
||||
@@ -20,6 +20,7 @@ export class GlobalFooterComponent implements OnInit {
|
||||
env: Env;
|
||||
officialMempoolSpace = this.stateService.env.OFFICIAL_MEMPOOL_SPACE;
|
||||
backendInfo$: Observable<IBackendInfo>;
|
||||
servicesBackendInfo$: Observable<IBackendInfo>;
|
||||
frontendGitCommitHash = this.stateService.env.GIT_COMMIT_HASH;
|
||||
packetJsonVersion = this.stateService.env.PACKAGE_JSON_VERSION;
|
||||
urlLanguage: string;
|
||||
@@ -27,8 +28,9 @@ export class GlobalFooterComponent implements OnInit {
|
||||
networkPaths: { [network: string]: string };
|
||||
currentNetwork = '';
|
||||
loggedIn = false;
|
||||
username = null;
|
||||
urlSubscription: Subscription;
|
||||
isServicesPage = false;
|
||||
servicesEnabled = false;
|
||||
|
||||
constructor(
|
||||
public stateService: StateService,
|
||||
@@ -38,12 +40,17 @@ export class GlobalFooterComponent implements OnInit {
|
||||
private storageService: StorageService,
|
||||
private route: ActivatedRoute,
|
||||
private cd: ChangeDetectorRef,
|
||||
private websocketService: WebsocketService
|
||||
private websocketService: WebsocketService,
|
||||
private router: Router
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.servicesEnabled = this.officialMempoolSpace && this.stateService.env.ACCELERATOR === true && this.stateService.network === '';
|
||||
this.isServicesPage = this.router.url.includes('/services/');
|
||||
|
||||
this.env = this.stateService.env;
|
||||
this.backendInfo$ = this.stateService.backendInfo$;
|
||||
this.servicesBackendInfo$ = this.stateService.servicesBackendInfo$;
|
||||
this.urlLanguage = this.languageService.getLanguageForUrl();
|
||||
this.navigationService.subnetPaths.subscribe((paths) => {
|
||||
this.networkPaths = paths;
|
||||
@@ -58,13 +65,7 @@ export class GlobalFooterComponent implements OnInit {
|
||||
});
|
||||
|
||||
this.urlSubscription = this.route.url.subscribe((url) => {
|
||||
this.loggedIn = JSON.parse(this.storageService.getValue('auth')) !== null;
|
||||
const auth = JSON.parse(this.storageService.getValue('auth'));
|
||||
if (auth?.user?.username) {
|
||||
this.username = auth.user.username;
|
||||
} else {
|
||||
this.username = null;
|
||||
}
|
||||
this.loggedIn = this.storageService.getAuth() !== null;
|
||||
this.cd.markForCheck();
|
||||
})
|
||||
}
|
||||
@@ -87,5 +88,4 @@ export class GlobalFooterComponent implements OnInit {
|
||||
return (this.env.BASE_MODULE === 'bisq' ? '' : this.env.BISQ_WEBSITE_URL + this.urlLanguage) + this.networkPaths[thisNetwork] || '/';
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
<div class="alert alert-danger" [innerHTML]="errorContent">
|
||||
</div>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user