Merge pull request #4188 from mempool/nymkappa/menu

User menu + integrated accelerator if available
This commit is contained in:
wiz 2023-08-31 01:55:52 +09:00 committed by GitHub
commit 7744146ef7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
47 changed files with 1612 additions and 97 deletions

View File

@ -112,6 +112,14 @@ PROXY_CONFIG.push(...[
"^/testnet": ""
},
},
{
context: ['/api/v1/services/**'],
target: `http://localhost:9000`,
secure: false,
ws: true,
changeOrigin: true,
proxyTimeout: 30000,
},
{
context: ['/api/v1/**'],
target: `http://127.0.0.1:8999`,

View File

@ -112,6 +112,14 @@ PROXY_CONFIG.push(...[
"^/testnet": ""
},
},
{
context: ['/api/v1/services/**'],
target: `http://localhost:9000`,
secure: false,
ws: true,
changeOrigin: true,
proxyTimeout: 30000,
},
{
context: ['/api/v1/**'],
target: `http://localhost:8999`,

View File

@ -95,6 +95,14 @@ if (configContent && configContent.BASE_MODULE === 'bisq') {
}
PROXY_CONFIG.push(...[
{
context: ['/api/v1/services/**'],
target: `http://localhost:9000`,
secure: false,
ws: true,
changeOrigin: true,
proxyTimeout: 30000,
},
{
context: ['/api/v1/**'],
target: `http://localhost:8999`,

View File

@ -4,7 +4,8 @@
<span style="margin-left: auto; margin-right: -20px; margin-bottom: -20px">&reg;</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>

View File

@ -22,6 +22,7 @@
.intro {
margin: 25px auto 30px;
margin-top: 25px;
width: 250px;
display: flex;
flex-direction: column;

View File

@ -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>

View File

@ -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;
}
}
}
}
}

View File

@ -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 };
}
}

View File

@ -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]="'&lrm;' + (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>

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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();

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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;
}

View File

@ -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;
}
}

View 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>

View 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;}
}

View 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);
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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 {

View File

@ -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();
}
}

View File

@ -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">

View File

@ -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 &trade;</span>
<button type="button" class="close" aria-label="Close" (click)="dismissAccelAlert()">
<span aria-hidden="true">&times;</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 && stateService.network === ''" [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 && stateService.network === ''" [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>

View File

@ -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;
}
}

View File

@ -19,6 +19,7 @@ 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';
@ -89,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;
@ -105,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;
@ -465,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);

View File

@ -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;

View File

@ -43,7 +43,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;
}
@ -113,7 +113,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 ) {

View 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[];
}

View File

@ -95,7 +95,7 @@ export interface TransactionStripped {
}
export interface IBackendInfo {
hostname: string;
hostname?: string;
gitCommit: string;
version: string;
}

View File

@ -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 });
}
}

View File

@ -8,6 +8,7 @@ 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 = {
@ -120,6 +123,7 @@ export class StateService {
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,

View File

@ -56,4 +56,12 @@ export class StorageService {
console.log(e);
}
}
getAuth(): any | null {
try {
return JSON.parse(localStorage.getItem('auth'));
} catch(e) {
return null;
}
}
}

View File

@ -137,6 +137,14 @@ 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';
@ -144,4 +152,4 @@ export function seoDescriptionNetwork(network: string): string {
return ' ' + network.charAt(0).toUpperCase() + network.slice(1);
}
return '';
}
}

View File

@ -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>

View File

@ -0,0 +1,4 @@
.no-cursor {
cursor: default !important;
pointer-events: none;
}

View File

@ -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'"> &trade;</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'"> &trade;</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>

View File

@ -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;
}
}

View File

@ -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] || '/';
}
}
}

View File

@ -0,0 +1,2 @@
<div class="alert alert-danger" [innerHTML]="errorContent">
</div>

View File

@ -0,0 +1,47 @@
import { Component, Input, OnInit } from "@angular/core";
import { DomSanitizer, SafeHtml } from "@angular/platform-browser";
const MempoolErrors = {
'acceleration_duplicated': `This transaction has already been accelerated.`,
'acceleration_outbid': `Your fee delta is too low.`,
'cannot_accelerate_tx': `Cannot accelerate this transaction.`,
'cannot_decode_raw_tx': `Cannot decode this raw transaction.`,
'cannot_fetch_raw_tx': `Cannot find this transaction.`,
'database_error': `Something went wrong. Please try again later.`,
'high_sigop_tx': `This transaction cannot be accelerated.`,
'invalid_acceleration_request': `This acceleration request is not valid.`,
'invalid_tx_dependencies': `This transaction dependencies are not valid.`,
'mempool_rejected_raw_tx': `Our mempool rejected this transaction`,
'no_mining_pool_available': `No mining pool available at the moment`,
'not_available': `You current subscription does not allow you to access this feature. Consider <strong><a style="color: #105fb0;" href="/sponsor" target="_blank">upgrading.</a><strong>`,
'not_enough_balance': `Your account balance is too low. Please make a <a style="color:#105fb0" href="/services/accelerator/overview">deposit.</a>`,
'not_verified': `You must verify your account to use this feature.`,
'recommended_fees_not_available': `Recommended fees are not available right now.`,
'too_many_relatives': `This transaction has too many relatives.`,
'txid_not_in_mempool': `This transaction is not in the mempool.`,
'waitlisted': `You are currently on the wait list. You will get notified once you are granted access.`,
'not_whitelisted_by_any_pool': `You are not whitelisted by any mining pool`,
} as { [error: string]: string };
export function isMempoolError(error: string) {
return Object.keys(MempoolErrors).includes(error);
}
@Component({
selector: 'app-mempool-error',
templateUrl: './mempool-error.component.html'
})
export class MempoolErrorComponent implements OnInit {
@Input() error: string;
errorContent: SafeHtml;
constructor(private sanitizer: DomSanitizer) { }
ngOnInit(): void {
if (Object.keys(MempoolErrors).includes(this.error)) {
this.errorContent = this.sanitizer.bypassSecurityTrustHtml(MempoolErrors[this.error]);
} else {
this.errorContent = this.error;
}
}
}

View File

@ -4,9 +4,10 @@ import { NgbCollapseModule, NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstra
import { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontawesome';
import { faFilter, faAngleDown, faAngleUp, faAngleRight, faAngleLeft, faBolt, faChartArea, faCogs, faCubes, faHammer, faDatabase, faExchangeAlt, faInfoCircle,
faLink, faList, faSearch, faCaretUp, faCaretDown, faTachometerAlt, faThList, faTint, faTv, faClock, faAngleDoubleDown, faSortUp, faAngleDoubleUp, faChevronDown,
faFileAlt, faRedoAlt, faArrowAltCircleRight, faExternalLinkAlt, faBook, faListUl, faDownload, faQrcode, faArrowRightArrowLeft, faArrowsRotate, faCircleLeft } from '@fortawesome/free-solid-svg-icons';
faFileAlt, faRedoAlt, faArrowAltCircleRight, faExternalLinkAlt, faBook, faListUl, faDownload, faQrcode, faArrowRightArrowLeft, faArrowsRotate, faCircleLeft, faFastForward, faWallet, faUserClock, faWrench, faUserFriends, faQuestionCircle, faHistory, faSignOutAlt, faKey, faSuitcase, faIdCardAlt, faNetworkWired, faUserCheck, faCircleCheck, faUserCircle } from '@fortawesome/free-solid-svg-icons';
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
import { MasterPageComponent } from '../components/master-page/master-page.component';
import { MenuComponent } from '../components/menu/menu.component';
import { PreviewTitleComponent } from '../components/master-page-preview/preview-title.component';
import { BisqMasterPageComponent } from '../components/bisq-master-page/bisq-master-page.component';
import { LiquidMasterPageComponent } from '../components/liquid-master-page/liquid-master-page.component';
@ -92,6 +93,9 @@ import { ToggleComponent } from './components/toggle/toggle.component';
import { GeolocationComponent } from '../shared/components/geolocation/geolocation.component';
import { TestnetAlertComponent } from './components/testnet-alert/testnet-alert.component';
import { GlobalFooterComponent } from './components/global-footer/global-footer.component';
import { AcceleratePreviewComponent } from '../components/accelerate-preview/accelerate-preview.component';
import { AccelerateFeeGraphComponent } from '../components/accelerate-preview/accelerate-fee-graph.component';
import { MempoolErrorComponent } from './components/mempool-error/mempool-error.component';
import { MempoolBlockOverviewComponent } from '../components/mempool-block-overview/mempool-block-overview.component';
import { ClockchainComponent } from '../components/clockchain/clockchain.component';
@ -135,6 +139,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
AmountComponent,
AboutComponent,
MasterPageComponent,
MenuComponent,
PreviewTitleComponent,
BisqMasterPageComponent,
LiquidMasterPageComponent,
@ -187,6 +192,8 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
GeolocationComponent,
TestnetAlertComponent,
GlobalFooterComponent,
AcceleratePreviewComponent,
AccelerateFeeGraphComponent,
CalculatorComponent,
BitcoinsatoshisPipe,
MempoolBlockOverviewComponent,
@ -194,7 +201,8 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
ClockComponent,
ClockFaceComponent,
OnlyVsizeDirective,
OnlyWeightDirective
OnlyWeightDirective,
MempoolErrorComponent,
],
imports: [
CommonModule,
@ -220,6 +228,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
],
exports: [
MasterPageComponent,
MenuComponent,
RouterModule,
ReactiveFormsModule,
NgbNavModule,
@ -307,6 +316,9 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
GeolocationComponent,
PreviewTitleComponent,
GlobalFooterComponent,
AcceleratePreviewComponent,
AccelerateFeeGraphComponent,
MempoolErrorComponent,
MempoolBlockOverviewComponent,
ClockchainComponent,
@ -359,5 +371,21 @@ export class SharedModule {
library.addIcons(faQrcode);
library.addIcons(faArrowRightArrowLeft);
library.addIcons(faExchangeAlt);
library.addIcons(faList);
library.addIcons(faFastForward);
library.addIcons(faWallet);
library.addIcons(faUserClock);
library.addIcons(faWrench);
library.addIcons(faUserFriends);
library.addIcons(faQuestionCircle);
library.addIcons(faHistory);
library.addIcons(faSignOutAlt);
library.addIcons(faKey);
library.addIcons(faSuitcase);
library.addIcons(faIdCardAlt);
library.addIcons(faNetworkWired);
library.addIcons(faUserCheck);
library.addIcons(faCircleCheck);
library.addIcons(faUserCircle);
}
}

View File

@ -20,7 +20,8 @@
<meta name="twitter:image:src" content="https://bisq.markets/resources/bisq/bisq-markets-preview.png" />
<meta name="twitter:domain" content="bisq.markets">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="msapplication-TileColor" content="#000000">
<meta name="msapplication-config" content="/resources/bisq/favicons/browserconfig.xml">
<meta name="theme-color" content="#1d1f31">

View File

@ -29,7 +29,8 @@
<link rel="manifest" href="/resources/liquid/favicons/site.webmanifest">
<link id="canonical" rel="canonical" href="https://liquid.network">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="msapplication-TileColor" content="#000000">
<meta name="msapplication-config" content="/resources/liquid/favicons/browserconfig.xml">
<meta name="theme-color" content="#1d1f31">

View File

@ -28,7 +28,8 @@
<link rel="shortcut icon" href="/resources/favicons/favicon.ico">
<link id="canonical" rel="canonical" href="https://mempool.space">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="msapplication-TileColor" content="#000000">
<meta name="msapplication-config" content="/resources/favicons/browserconfig.xml">
<meta name="theme-color" content="#1d1f31">