Merge branch 'master' into tooltip-overflow-bug

This commit is contained in:
ncois 2023-11-22 12:14:03 +01:00 committed by GitHub
commit f57436f511
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 826 additions and 234 deletions

View File

@ -480,14 +480,15 @@ class RbfCache {
};
if (config.MEMPOOL.BACKEND === 'esplora') {
let processedCount = 0;
const sliceLength = Math.ceil(config.ESPLORA.BATCH_QUERY_BASE_SIZE / 40);
for (let i = 0; i < Math.ceil(txids.length / sliceLength); i++) {
const slice = txids.slice(i * sliceLength, (i + 1) * sliceLength);
processedCount += slice.length;
try {
const txs = await bitcoinApi.$getRawTransactions(slice);
logger.debug(`fetched ${slice.length} cached rbf transactions`);
processTxs(txs);
logger.debug(`processed ${slice.length} cached rbf transactions`);
logger.debug(`fetched and processed ${processedCount} of ${txids.length} cached rbf transactions (${(processedCount / txids.length * 100).toFixed(2)}%)`);
} catch (err) {
logger.err(`failed to fetch or process ${slice.length} cached rbf transactions`);
}

View File

@ -31,10 +31,10 @@
"bootstrap": "~4.6.2",
"browserify": "^17.0.0",
"clipboard": "^2.0.11",
"cypress": "^13.6.0",
"domino": "^2.1.6",
"echarts": "~5.4.3",
"lightweight-charts": "~3.8.0",
"mock-socket": "~9.3.1",
"ngx-echarts": "~16.2.0",
"ngx-infinite-scroll": "^16.0.0",
"qrcode": "1.5.1",
@ -59,7 +59,7 @@
"optionalDependencies": {
"@cypress/schematic": "^2.5.0",
"@types/cypress": "^1.1.3",
"cypress": "^13.5.0",
"cypress": "^13.6.0",
"cypress-fail-on-console-error": "~5.0.0",
"cypress-wait-until": "^2.0.1",
"mock-socket": "~9.3.1",
@ -7148,9 +7148,9 @@
"peer": true
},
"node_modules/cypress": {
"version": "13.5.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.5.0.tgz",
"integrity": "sha512-oh6U7h9w8wwHfzNDJQ6wVcAeXu31DlIYlNOBvfd6U4CcB8oe4akawQmH+QJVOMZlM42eBoCne015+svVqdwdRQ==",
"version": "13.6.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.6.0.tgz",
"integrity": "sha512-quIsnFmtj4dBUEJYU4OH0H12bABJpSujvWexC24Ju1gTlKMJbeT6tTO0vh7WNfiBPPjoIXLN+OUqVtiKFs6SGw==",
"hasInstallScript": true,
"optional": true,
"dependencies": {
@ -22096,9 +22096,9 @@
"peer": true
},
"cypress": {
"version": "13.5.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.5.0.tgz",
"integrity": "sha512-oh6U7h9w8wwHfzNDJQ6wVcAeXu31DlIYlNOBvfd6U4CcB8oe4akawQmH+QJVOMZlM42eBoCne015+svVqdwdRQ==",
"version": "13.6.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.6.0.tgz",
"integrity": "sha512-quIsnFmtj4dBUEJYU4OH0H12bABJpSujvWexC24Ju1gTlKMJbeT6tTO0vh7WNfiBPPjoIXLN+OUqVtiKFs6SGw==",
"optional": true,
"requires": {
"@cypress/request": "^3.0.0",

View File

@ -110,7 +110,7 @@
"optionalDependencies": {
"@cypress/schematic": "^2.5.0",
"@types/cypress": "^1.1.3",
"cypress": "^13.5.0",
"cypress": "^13.6.0",
"cypress-fail-on-console-error": "~5.0.0",
"cypress-wait-until": "^2.0.1",
"mock-socket": "~9.3.1",

View File

@ -2,6 +2,7 @@ import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { AppPreloadingStrategy } from './app.preloading-strategy'
import { BlockViewComponent } from './components/block-view/block-view.component';
import { EightBlocksComponent } from './components/eight-blocks/eight-blocks.component';
import { MempoolBlockViewComponent } from './components/mempool-block-view/mempool-block-view.component';
import { ClockComponent } from './components/clock/clock.component';
import { StatusViewComponent } from './components/status-view/status-view.component';
@ -124,6 +125,10 @@ let routes: Routes = [
path: 'view/mempool-block/:index',
component: MempoolBlockViewComponent,
},
{
path: 'view/blocks',
component: EightBlocksComponent,
},
{
path: 'status',
data: { networks: ['bitcoin', 'liquid'] },

View File

@ -32,11 +32,22 @@
<track label="Português" kind="captions" srclang="pt" src="/resources/promo-video/pt.vtt" [attr.default]="showSubtitles('pt') ? '' : null">
</video>
<ng-container *ngIf="false && officialMempoolSpace">
<h3 class="mt-5">Sponsor the project</h3>
<div class="d-flex justify-content-center" style="max-width: 90%; margin: 35px auto 75px auto; column-gap: 15px">
<a href="/sponsor" class="btn" style="background-color: rgba(152, 88, 255, 0.75); box-shadow: 0px 0px 50px 5px rgba(152, 88, 255, 0.75)" i18n="about.community-sponsor-button">Community</a>
<a href="/enterprise" class="btn" style="background-color: rgba(152, 88, 255, 0.75); box-shadow: 0px 0px 50px 5px rgba(152, 88, 255, 0.75)" i18n="about.enterprise-sponsor-button">Enterprise</a>
<ng-container *ngIf="officialMempoolSpace">
<div id="become-sponsor-container">
<div class="become-sponsor community">
<p style="font-weight: 700; font-size: 18px;">If you're an individual...</p>
<a href="/sponsor" class="btn" style="background-color: rgba(152, 88, 255, 0.75); box-shadow: 0px 0px 50px 5px rgba(152, 88, 255, 0.75)" i18n="about.community-sponsor-button">Become a Community Sponsor</a>
<p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> Exclusive swag</p>
<p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> Your avatar on the About page</p>
<p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> And more coming soon :)</p>
</div>
<div class="become-sponsor enterprise">
<p style="font-weight: 700; font-size: 18px;">If you're a business...</p>
<a href="/enterprise" class="btn" style="background-color: rgba(152, 88, 255, 0.75); box-shadow: 0px 0px 50px 5px rgba(152, 88, 255, 0.75)" i18n="about.enterprise-sponsor-button">Become an Enterprise Sponsor</a>
<p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> Increased API limits</p>
<p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> Co-branded instance</p>
<p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> 99% service-level agreement</p>
</div>
</div>
</ng-container>
@ -193,7 +204,7 @@
<ng-container>
<ng-template ngFor let-sponsor [ngForOf]="profiles.whales">
<a [href]="'https://twitter.com/' + sponsor.username" target="_blank" rel="sponsored" [title]="sponsor.username">
<img class="image" [src]="'data:' + sponsor.image_mime + ';base64,' + sponsor.image" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
<img class="image" [src]="'/api/v1/services/account/images/' + sponsor.username + '?md5=' + sponsor.imageMd5" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
</a>
</ng-template>
</ng-container>
@ -205,7 +216,7 @@
<div class="wrapper">
<ng-template ngFor let-sponsor [ngForOf]="profiles.chads">
<a [href]="'https://twitter.com/' + sponsor.username" target="_blank" rel="sponsored" [title]="sponsor.username">
<img class="image" [src]="'data:' + sponsor.image_mime + ';base64,' + sponsor.image" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
<img class="image" [src]="'/api/v1/services/account/images/' + sponsor.username + '?md5=' + sponsor.imageMd5" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
</a>
</ng-template>
</div>

View File

@ -246,3 +246,48 @@
width: 64px;
height: 64px;
}
#become-sponsor-container {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: center;
align-items: center;
gap: 20px;
margin: 68px auto;
}
.become-sponsor {
background-color: #1d1f31;
border-radius: 16px;
padding: 12px 20px;
width: 400px;
padding: 40px 20px;
}
.become-sponsor a {
margin-top: 10px;
}
#become-sponsor-container .btn {
margin-bottom: 24px;
}
#become-sponsor-container .ng-fa-icon {
color: #2ecc71;
margin-right: 5px;
}
#become-sponsor-container .sponsor-feature {
text-align: left;
width: 250px;
margin: 12px auto;
}
@media (max-width: 992px) {
#become-sponsor-container {
flex-wrap: wrap;
}
}

View File

@ -2,7 +2,6 @@
height: 100%;
min-width: 120px;
width: 120px;
max-height: 90vh;
margin-left: 4em;
margin-right: 1.5em;
padding-bottom: 63px;

View File

@ -1,7 +1,7 @@
<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>.
Transaction has now been <a class="alert-link" routerLink="/services/accelerator/history">submitted</a> to mining pools for acceleration.
</div>
</div>
</div>
@ -74,8 +74,8 @@
<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>
<span class="fee">{{ option.fee + estimate.mempoolBaseFee + estimate.vsizeFee | 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>
@ -87,18 +87,10 @@
<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'">
<ng-container>
<tr class="group-first">
<td class="item">
Next block market rate
@ -121,32 +113,6 @@
</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>
@ -166,7 +132,7 @@
<span class="fiat"><app-fiat [value]="estimate.mempoolBaseFee"></app-fiat></span>
</td>
</tr>
<tr class="info group-last" style="border-bottom: 1px solid lightgrey">
<tr class="info group-last">
<td class="info">
<i><small>Transaction vsize fee</small></i>
</td>
@ -180,8 +146,8 @@
</tr>
<!-- NEXT BLOCK ESTIMATE -->
<ng-container *ngIf="showTable === 'estimated'">
<tr class="group-first">
<ng-container>
<tr class="group-first" style="border-top: 1px dashed grey; border-collapse: collapse;">
<td class="item">
<b style="background-color: #5E35B1" class="p-1 pl-0">Estimated acceleration cost</b>
</td>
@ -195,7 +161,7 @@
<span class="fiat"><app-fiat [value]="estimate.cost + estimate.mempoolBaseFee + estimate.vsizeFee"></app-fiat></span>
</td>
</tr>
<tr class="info group-last">
<tr class="info group-last" style="border-bottom: 1px solid lightgrey">
<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>
@ -203,7 +169,7 @@
</ng-container>
<!-- MAX COST -->
<ng-container *ngIf="showTable === 'maximum'">
<ng-container>
<tr class="group-first">
<td class="item">
<b style="background-color: #105fb0;" class="p-1 pl-0">Maximum acceleration cost</b>
@ -228,7 +194,7 @@
</ng-container>
<!-- USER BALANCE -->
<ng-container *ngIf="estimate.userBalance < maxCost">
<ng-container *ngIf="isLoggedIn() && estimate.userBalance < maxCost">
<tr class="group-first group-last" style="border-top: 1px dashed grey">
<td class="item">
Available balance
@ -244,6 +210,17 @@
</td>
</tr>
</ng-container>
<!-- LOGIN CTA -->
<ng-container *ngIf="!isLoggedIn()">
<tr class="group-first group-last" style="border-top: 1px dashed grey">
<td class="item"></td>
<td class="amt"></td>
<td class="units d-flex">
<a [routerLink]="['/login']" [queryParams]="{redirectTo: '/tx/' + tx.txid + '#accelerate'}" class="btn btn-purple flex-grow-1">Login</a>
</td>
</tr>
</ng-container>
</tbody>
</table>
</div>

View File

@ -28,7 +28,10 @@
.feerate.active {
background-color: #105fb0 !important;
opacity: 1;
border: 1px solid white !important;
border: 1px solid #007fff !important;
}
.feerate:focus {
box-shadow: none !important;
}
.estimateDisabled {
@ -41,6 +44,24 @@
margin-top: 0.5em;
}
.tab {
&:first-child {
margin-right: 1px;
}
border: solid 1px black;
border-bottom: none;
background-color: #323655;
border-top-left-radius: 10px !important;
border-top-right-radius: 10px !important;
}
.tab.active {
background-color: #5d659d !important;
opacity: 1;
}
.tab:focus {
box-shadow: none !important;
}
.table-accelerator {
tr {
text-wrap: wrap;

View File

@ -1,4 +1,5 @@
import { Component, OnInit, Input, OnDestroy, OnChanges, SimpleChanges, HostListener } from '@angular/core';
import { Router } from '@angular/router';
import { ApiService } from '../../services/api.service';
import { Subscription, catchError, of, tap } from 'rxjs';
import { StorageService } from '../../services/storage.service';
@ -55,14 +56,14 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
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
private storageService: StorageService,
private router: Router,
) { }
ngOnDestroy(): void {
@ -73,7 +74,7 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
ngOnChanges(changes: SimpleChanges): void {
if (changes.scrollEvent) {
this.scrollToPreview('acceleratePreviewAnchor', 'center');
this.scrollToPreview('acceleratePreviewAnchor', 'start');
}
}
@ -126,7 +127,7 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
this.maxCost = this.userBid + this.estimate.mempoolBaseFee + this.estimate.vsizeFee;
if (!this.error) {
this.scrollToPreview('acceleratePreviewAnchor', 'center');
this.scrollToPreview('acceleratePreviewAnchor', 'start');
}
}
}),
@ -187,7 +188,11 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
this.estimateSubscription.unsubscribe();
},
error: (response) => {
this.error = response.error;
if (response.status === 403 && response.error === 'not_available') {
this.error = 'waitlisted';
} else {
this.error = response.error;
}
this.scrollToPreviewWithTimeout('mempoolError', 'center');
}
});

View File

@ -1,4 +1,4 @@
<header>
<header class="sticky-header">
<nav class="navbar navbar-expand-md navbar-dark bg-dark">
<a class="navbar-brand" [routerLink]="['/' | relativeUrl]" style="position: relative;">
<ng-container *ngIf="{ val: connectionState$ | async } as connectionState">

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

View File

@ -20,6 +20,8 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
@Input() blockLimit: number;
@Input() orientation = 'left';
@Input() flip = true;
@Input() animationDuration: number = 1000;
@Input() animationOffset: number | null = null;
@Input() disableSpinner = false;
@Input() mirrorTxid: string | void;
@Input() unavailable: boolean = false;
@ -141,9 +143,9 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
}
}
replace(transactions: TransactionStripped[], direction: string, sort: boolean = true): void {
replace(transactions: TransactionStripped[], direction: string, sort: boolean = true, startTime?: number): void {
if (this.scene) {
this.scene.replace(transactions || [], direction, sort);
this.scene.replace(transactions || [], direction, sort, startTime);
this.start();
this.updateSearchHighlight();
}
@ -226,7 +228,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
} else {
this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: this.resolution,
blockLimit: this.blockLimit, orientation: this.orientation, flip: this.flip, vertexArray: this.vertexArray,
highlighting: this.auditHighlighting });
highlighting: this.auditHighlighting, animationDuration: this.animationDuration, animationOffset: this.animationOffset });
this.start();
}
}

View File

@ -9,6 +9,9 @@ export default class BlockScene {
txs: { [key: string]: TxView };
orientation: string;
flip: boolean;
animationDuration: number = 1000;
configAnimationOffset: number | null;
animationOffset: number;
highlightingEnabled: boolean;
width: number;
height: number;
@ -23,11 +26,11 @@ export default class BlockScene {
animateUntil = 0;
dirty: boolean;
constructor({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting }:
{ width: number, height: number, resolution: number, blockLimit: number,
constructor({ width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, highlighting }:
{ width: number, height: number, resolution: number, blockLimit: number, animationDuration: number, animationOffset: number,
orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean }
) {
this.init({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting });
this.init({ width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, highlighting });
}
resize({ width = this.width, height = this.height, animate = true }: { width?: number, height?: number, animate: boolean }): void {
@ -36,6 +39,7 @@ export default class BlockScene {
this.gridSize = this.width / this.gridWidth;
this.unitPadding = Math.max(1, Math.floor(this.gridSize / 5));
this.unitWidth = this.gridSize - (this.unitPadding * 2);
this.animationOffset = this.configAnimationOffset == null ? (this.width * 1.4) : this.configAnimationOffset;
this.dirty = true;
if (this.initialised && this.scene) {
@ -90,8 +94,8 @@ export default class BlockScene {
}
// Animate new block entering scene
enter(txs: TransactionStripped[], direction) {
this.replace(txs, direction);
enter(txs: TransactionStripped[], direction, startTime?: number) {
this.replace(txs, direction, false, startTime);
}
// Animate block leaving scene
@ -108,8 +112,7 @@ export default class BlockScene {
}
// Reset layout and replace with new set of transactions
replace(txs: TransactionStripped[], direction: string = 'left', sort: boolean = true): void {
const startTime = performance.now();
replace(txs: TransactionStripped[], direction: string = 'left', sort: boolean = true, startTime: number = performance.now()): void {
const nextIds = {};
const remove = [];
txs.forEach(tx => {
@ -133,7 +136,7 @@ export default class BlockScene {
removed.forEach(tx => {
tx.destroy();
});
}, 1000);
}, (startTime - performance.now()) + this.animationDuration + 1000);
this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight });
@ -147,7 +150,7 @@ export default class BlockScene {
});
}
this.updateAll(startTime, 200, direction);
this.updateAll(startTime, 50, direction);
}
update(add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: boolean | undefined }[], direction: string = 'left', resetLayout: boolean = false): void {
@ -214,10 +217,13 @@ export default class BlockScene {
this.animateUntil = Math.max(this.animateUntil, tx.setHighlight(value));
}
private init({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting }:
{ width: number, height: number, resolution: number, blockLimit: number,
private init({ width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, highlighting }:
{ width: number, height: number, resolution: number, blockLimit: number, animationDuration: number, animationOffset: number,
orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean }
): void {
this.animationDuration = animationDuration || 1000;
this.configAnimationOffset = animationOffset;
this.animationOffset = this.configAnimationOffset == null ? (this.width * 1.4) : this.configAnimationOffset;
this.orientation = orientation;
this.flip = flip;
this.vertexArray = vertexArray;
@ -261,8 +267,8 @@ export default class BlockScene {
this.applyTxUpdate(tx, {
display: {
position: {
x: tx.screenPosition.x + (direction === 'right' ? -this.width : (direction === 'left' ? this.width : 0)) * 1.4,
y: tx.screenPosition.y + (direction === 'up' ? -this.height : (direction === 'down' ? this.height : 0)) * 1.4,
x: tx.screenPosition.x + (direction === 'right' ? -this.width - this.animationOffset : (direction === 'left' ? this.width + this.animationOffset : 0)),
y: tx.screenPosition.y + (direction === 'up' ? -this.height - this.animationOffset : (direction === 'down' ? this.height + this.animationOffset : 0)),
s: tx.screenPosition.s
},
color: txColor,
@ -275,7 +281,7 @@ export default class BlockScene {
position: tx.screenPosition,
color: txColor
},
duration: animate ? 1000 : 1,
duration: animate ? this.animationDuration : 1,
start: startTime,
delay: animate ? delay : 0,
});
@ -284,8 +290,8 @@ export default class BlockScene {
display: {
position: tx.screenPosition
},
duration: animate ? 1000 : 0,
minDuration: animate ? 500 : 0,
duration: animate ? this.animationDuration : 0,
minDuration: animate ? (this.animationDuration / 2) : 0,
start: startTime,
delay: animate ? delay : 0,
adjust: animate
@ -322,11 +328,11 @@ export default class BlockScene {
this.applyTxUpdate(tx, {
display: {
position: {
x: tx.screenPosition.x + (direction === 'right' ? this.width : (direction === 'left' ? -this.width : 0)) * 1.4,
y: tx.screenPosition.y + (direction === 'up' ? this.height : (direction === 'down' ? -this.height : 0)) * 1.4,
x: tx.screenPosition.x + (direction === 'right' ? this.width + this.animationOffset : (direction === 'left' ? -this.width - this.animationOffset : 0)),
y: tx.screenPosition.y + (direction === 'up' ? this.height + this.animationOffset : (direction === 'down' ? -this.height - this.animationOffset : 0)),
}
},
duration: 1000,
duration: this.animationDuration,
start: startTime,
delay: 50
});

View File

@ -0,0 +1,24 @@
<div class="blocks" [class.wrap]="wrapBlocks">
<ng-container *ngFor="let i of blockIndices">
<div class="block-wrapper" [style]="wrapperStyle">
<div class="block-container" [style]="containerStyle">
<app-block-overview-graph
#blockGraph
[isLoading]="false"
[resolution]="resolution"
[blockLimit]="stateService.blockVSize"
[orientation]="'top'"
[flip]="false"
[animationDuration]="animationDuration"
[animationOffset]="animationOffset"
[disableSpinner]="true"
(txClickEvent)="onTxClick($event)"
></app-block-overview-graph>
<div *ngIf="showInfo && blockInfo[i]" class="info" @infoChange>
<h1 class="height">{{ blockInfo[i].height }}</h1>
<h2 class="mined-by">by {{ blockInfo[i].extras.pool.name || 'Unknown' }}</h2>
</div>
</div>
</div>
</ng-container>
</div>

View File

@ -0,0 +1,69 @@
.blocks {
width: 100%;
height: 100%;
min-width: 100vw;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: flex-start;
align-items: flex-start;
align-content: flex-start;
&.wrap {
flex-wrap: wrap;
}
.block-wrapper {
flex-grow: 0;
flex-shrink: 0;
position: relative;
--block-width: 1080px;
.info {
position: absolute;
left: 8%;
top: 8%;
right: 8%;
bottom: 8%;
height: 84%;
width: 84%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: calc(var(--block-width) * 0.03);
text-shadow: 0 0 calc(var(--block-width) * 0.05) black;
h1 {
font-size: 6em;
line-height: 1;
margin-bottom: calc(var(--block-width) * 0.03);
}
h2 {
font-size: 1.8em;
line-height: 1;
margin-bottom: calc(var(--block-width) * 0.03);
}
.hash {
font-family: monospace;
word-wrap: break-word;
font-size: 1.4em;
line-height: 1;
margin-bottom: calc(var(--block-width) * 0.03);
}
.mined-by {
position: absolute;
bottom: 0;
margin: auto;
text-align: center;
}
}
}
.block-container {
overflow: hidden;
}
}

View File

@ -0,0 +1,253 @@
import { Component, OnInit, OnDestroy, ViewChildren, QueryList } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { catchError, startWith } from 'rxjs/operators';
import { Subject, Subscription, of } from 'rxjs';
import { StateService } from '../../services/state.service';
import { WebsocketService } from '../../services/websocket.service';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
import { BlockExtended, TransactionStripped } from '../../interfaces/node-api.interface';
import { ApiService } from '../../services/api.service';
import { BlockOverviewGraphComponent } from '../block-overview-graph/block-overview-graph.component';
import { detectWebGL } from '../../shared/graphs.utils';
import { animate, style, transition, trigger } from '@angular/animations';
import { BytesPipe } from '../../shared/pipes/bytes-pipe/bytes.pipe';
function bestFitResolution(min, max, n): number {
const target = (min + max) / 2;
let bestScore = Infinity;
let best = null;
for (let i = min; i <= max; i++) {
const remainder = (n % i);
if (remainder < bestScore || (remainder === bestScore && (Math.abs(i - target) < Math.abs(best - target)))) {
bestScore = remainder;
best = i;
}
}
return best;
}
interface BlockInfo extends BlockExtended {
timeString: string;
}
@Component({
selector: 'app-eight-blocks',
templateUrl: './eight-blocks.component.html',
styleUrls: ['./eight-blocks.component.scss'],
animations: [
trigger('infoChange', [
transition(':enter', [
style({ opacity: 0 }),
animate('1000ms', style({ opacity: 1 })),
]),
transition(':leave', [
animate('1000ms 500ms', style({ opacity: 0 }))
])
]),
],
})
export class EightBlocksComponent implements OnInit, OnDestroy {
network = '';
latestBlocks: BlockExtended[] = [];
isLoadingTransactions = true;
strippedTransactions: { [height: number]: TransactionStripped[] } = {};
webGlEnabled = true;
hoverTx: string | null = null;
blocksSubscription: Subscription;
cacheBlocksSubscription: Subscription;
networkChangedSubscription: Subscription;
queryParamsSubscription: Subscription;
graphChangeSubscription: Subscription;
numBlocks: number = 8;
blockIndices: number[] = [...Array(8).keys()];
autofit: boolean = false;
padding: number = 0;
wrapBlocks: boolean = false;
blockWidth: number = 1080;
animationDuration: number = 2000;
animationOffset: number = 0;
stagger: number = 0;
testing: boolean = true;
testHeight: number = 800000;
testShiftTimeout: number;
showInfo: boolean = true;
blockInfo: BlockInfo[] = [];
wrapperStyle = {
'--block-width': '1080px',
width: '1080px',
maxWidth: '1080px',
padding: '',
};
containerStyle = {};
resolution: number = 86;
@ViewChildren('blockGraph') blockGraphs: QueryList<BlockOverviewGraphComponent>;
constructor(
private route: ActivatedRoute,
private router: Router,
public stateService: StateService,
private websocketService: WebsocketService,
private apiService: ApiService,
private bytesPipe: BytesPipe,
) {
this.webGlEnabled = detectWebGL();
}
ngOnInit(): void {
this.websocketService.want(['blocks']);
this.network = this.stateService.network;
this.queryParamsSubscription = this.route.queryParams.subscribe((params) => {
this.numBlocks = Number.isInteger(Number(params.numBlocks)) ? Number(params.numBlocks) : 8;
this.blockIndices = [...Array(this.numBlocks).keys()];
this.autofit = params.autofit !== 'false';
this.padding = Number.isInteger(Number(params.padding)) ? Number(params.padding) : 10;
this.blockWidth = Number.isInteger(Number(params.blockWidth)) ? Number(params.blockWidth) : 540;
this.wrapBlocks = params.wrap !== 'false';
this.stagger = Number.isInteger(Number(params.stagger)) ? Number(params.stagger) : 0;
this.animationDuration = Number.isInteger(Number(params.animationDuration)) ? Number(params.animationDuration) : 2000;
this.animationOffset = this.padding * 2;
if (this.autofit) {
this.resolution = bestFitResolution(76, 96, this.blockWidth - this.padding * 2);
} else {
this.resolution = 86;
}
this.wrapperStyle = {
'--block-width': this.blockWidth + 'px',
width: this.blockWidth + 'px',
maxWidth: this.blockWidth + 'px',
padding: (this.padding || 0) +'px 0px',
};
if (params.test === 'true') {
if (this.blocksSubscription) {
this.blocksSubscription.unsubscribe();
}
this.blocksSubscription = (new Subject<BlockExtended[]>()).subscribe((blocks) => {
this.handleNewBlock(blocks.slice(0, this.numBlocks));
});
this.shiftTestBlocks();
} else if (!this.blocksSubscription) {
this.blocksSubscription = this.stateService.blocks$
.subscribe((blocks) => {
this.handleNewBlock(blocks.slice(0, this.numBlocks));
});
}
});
this.setupBlockGraphs();
this.networkChangedSubscription = this.stateService.networkChanged$
.subscribe((network) => this.network = network);
}
ngAfterViewInit(): void {
this.graphChangeSubscription = this.blockGraphs.changes.pipe(startWith(null)).subscribe(() => {
this.setupBlockGraphs();
});
}
ngOnDestroy(): void {
this.stateService.markBlock$.next({});
if (this.blocksSubscription) {
this.blocksSubscription?.unsubscribe();
}
this.cacheBlocksSubscription?.unsubscribe();
this.networkChangedSubscription?.unsubscribe();
this.queryParamsSubscription?.unsubscribe();
}
shiftTestBlocks(): void {
const sub = this.apiService.getBlocks$(this.testHeight).subscribe(result => {
sub.unsubscribe();
this.handleNewBlock(result.slice(0, this.numBlocks));
this.testHeight++;
clearTimeout(this.testShiftTimeout);
this.testShiftTimeout = window.setTimeout(() => { this.shiftTestBlocks(); }, 10000);
});
}
async handleNewBlock(blocks: BlockExtended[]): Promise<void> {
const readyPromises: Promise<TransactionStripped[]>[] = [];
const previousBlocks = this.latestBlocks;
const newHeights = {};
this.latestBlocks = blocks;
for (const block of blocks) {
newHeights[block.height] = true;
if (!this.strippedTransactions[block.height]) {
readyPromises.push(new Promise((resolve) => {
const subscription = this.apiService.getStrippedBlockTransactions$(block.id).pipe(
catchError(() => {
return of([]);
}),
).subscribe((transactions) => {
this.strippedTransactions[block.height] = transactions;
subscription.unsubscribe();
resolve(transactions);
});
}));
}
}
await Promise.allSettled(readyPromises);
this.updateBlockGraphs(blocks);
// free up old transactions
previousBlocks.forEach(block => {
if (!newHeights[block.height]) {
delete this.strippedTransactions[block.height];
}
});
}
updateBlockGraphs(blocks): void {
const startTime = performance.now() + 1000 - (this.stagger < 0 ? this.stagger * 8 : 0);
if (this.blockGraphs) {
this.blockGraphs.forEach((graph, index) => {
graph.replace(this.strippedTransactions[blocks?.[index]?.height] || [], 'right', false, startTime + (this.stagger * index));
});
}
this.showInfo = false;
setTimeout(() => {
this.blockInfo = blocks.map(block => {
return {
...block,
timeString: (new Date(block.timestamp * 1000)).toLocaleTimeString(),
};
});
this.showInfo = true;
}, 1600); // Should match the animation time.
}
setupBlockGraphs(): void {
if (this.blockGraphs) {
this.blockGraphs.forEach((graph, index) => {
graph.destroy();
graph.setup(this.strippedTransactions[this.latestBlocks?.[index]?.height] || []);
});
}
}
onTxClick(event: { tx: TransactionStripped, keyModifier: boolean }): void {
const url = new RelativeUrlPipe(this.stateService).transform(`/tx/${event.tx.txid}`);
if (!event.keyModifier) {
this.router.navigate([url]);
} else {
window.open(url, '_blank');
}
}
onTxHover(txid: string): void {
if (txid && txid.length) {
this.hoverTx = txid;
} else {
this.hoverTx = null;
}
}
}

View File

@ -1,5 +1,5 @@
<ng-container *ngIf="{ val: network$ | async } as network">
<header>
<header class="sticky-header">
<nav class="navbar navbar-expand-md navbar-dark bg-dark">
<a class="navbar-brand" [routerLink]="['/' | relativeUrl]" style="position: relative;">
<ng-container *ngIf="{ val: connectionState$ | async } as connectionState">

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

View File

@ -5,7 +5,7 @@
<!-- 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">
<img *ngIf="user.imageMd5" [src]="'/api/v1/services/account/images/' + 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)">
@ -18,7 +18,7 @@
<a class="navbar-brand" [ngClass]="{'dual-logos': subdomain}" [routerLink]="['/' | relativeUrl]" (click)="brandClick($event)">
<ng-template [ngIf]="subdomain">
<div class="subdomain_container">
<img [src]="'/api/v1/enterprise/images/' + subdomain + '/logo'" class="subdomain_logo">
<img [src]="'/api/v1/services/enterprise/images/' + subdomain + '/logo'" class="subdomain_logo">
</div>
</ng-template>
<ng-container *ngIf="{ val: connectionState$ | async } as connectionState">
@ -71,13 +71,14 @@
<a class="nav-link" [routerLink]="['/about']" (click)="collapse()"><fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true" i18n-title="master-page.about" title="About"></fa-icon></a>
</li>
</ul>
<app-search-form class="search-form-container" location="top" (searchTriggered)="collapse()"></app-search-form>
<app-search-form [hamburgerOpen]="user != null" class="search-form-container" location="top" (searchTriggered)="collapse()"></app-search-form>
</div>
</nav>
</header>
<div class="d-flex" style="overflow: clip">
<app-menu *ngIf="servicesEnabled" [navOpen]="menuOpen" (loggedOut)="onLoggedOut()" (menuToggled)="menuToggled($event)"></app-menu>
<div *ngIf="!servicesEnabled" class="sidenav"><!-- empty sidenav needed to push footer down the screen --></div>
<div class="flex-grow-1 d-flex flex-column">
<app-testnet-alert *ngIf="network.val === 'testnet' || network.val === 'signet'"></app-testnet-alert>

View File

@ -238,4 +238,16 @@ nav {
main {
transition: 0.2s;
transition-property: max-width;
}
}
// empty sidenav
.sidenav {
z-index: 1;
background-color: transparent;
width: 225px;
height: calc(100vh - 65px);
position: sticky;
top: 65px;
margin-left: -225px;
padding-bottom: 20px;
}

View File

@ -230,7 +230,7 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
positions[['left', 'right'][+(pos[0] < size.viewSize[0] / 2)]] = 100;
return positions;
},
extraCssText: `width: ${(this.template === 'advanced') ? '310px' : '200px'};
extraCssText: `width: ${(this.template === 'advanced') ? '300px' : '200px'};
background: transparent;
border: none;
box-shadow: none;`,
@ -254,7 +254,7 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
const axisValueLabel: string = formatterXAxis(this.locale, this.windowPreference, params[0].axisValue);
const { totalValue, totalValueArray } = this.getTotalValues(params);
const itemFormatted = [];
let totalParcial = 0;
let sum = 0;
let progressPercentageText = '';
let countItem;
let items = this.inverted ? [...params].reverse() : params;
@ -262,7 +262,7 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
countItem = items.pop();
}
items.map((item: any, index: number) => {
totalParcial += item.value[1];
sum += item.value[1];
const progressPercentage = (item.value[1] / totalValue) * 100;
const progressPercentageSum = (totalValueArray[index] / totalValue) * 100;
let activeItemClass = '';
@ -279,7 +279,7 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
<span class="symbol">%</span>
</span>
<span class="total-parcial-vbytes">
${this.vbytesPipe.transform(totalParcial, 2, 'vB', 'MvB', false)}
${this.vbytesPipe.transform(sum, 2, 'vB', 'MvB', false)}
</span>
<div class="total-percentage-bar">
<span class="total-percentage-bar-background">
@ -303,12 +303,12 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
</td>
<td class="total-progress-sum">
<span>
${this.vbytesPipe.transform(item.value[1], 2, 'vB', 'MvB', false)}
${(item.value[1] / 1_000_000).toFixed(2)} <span class="symbol">MvB</span>
</span>
</td>
<td class="total-progress-sum">
<span>
${this.vbytesPipe.transform(totalValueArray[index], 2, 'vB', 'MvB', false)}
${(totalValueArray[index] / 1_000_000).toFixed(2)} <span class="symbol">MvB</span>
</span>
</td>
<td class="total-progress-sum-bar">

View File

@ -3,7 +3,7 @@
<nav class="scrollable menu-click">
<span *ngIf="userAuth" class="menu-click">
<strong class="menu-click">@ {{ userAuth.user.username }}</strong>
<strong class="menu-click text-nowrap ellipsis">@ {{ 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>

View File

@ -9,11 +9,20 @@
margin-left: -250px;
box-shadow: 5px 0px 30px 0px #000;
padding-bottom: 20px;
@media (max-width: 613px) {
top: 105px;
}
}
.ellipsis {
display: block;
overflow: hidden;
text-overflow: ellipsis;
}
.scrollable {
overflow-x: hidden;
overflow-y: scroll;
overflow-y: auto;
}
.sidenav.open {

View File

@ -53,6 +53,26 @@
<br>
<h4>SIGNING UP FOR AN ACCOUNT ON MEMPOOL.SPACE</h4>
<p>If you sign up for an account on mempool.space, we may collect the following:</p>
<ol>
<li>If you provide your e-mail address, we may contact you regarding your account, billing purposess, or to update you about our services. We will not share this with any third-party.</li>
<li>If you connect your Twitter account, we may store your Twitter identity, e-mail address, and profile photo. We may publicly display your profile photo or link to your profile on our website, if you sponsor The Mempool Open Source Project, claim your Lightning node, or other such use cases.</li>
<li>If you make a credit card payment, we will process your payment using Square (Block, Inc.), and we will store details about the transaction in our database. Please see "Information we collect about customers" on Square's website at https://squareup.com/us/en/legal/general/privacy</li>
<li>If you make a Bitcoin or Liquid payment, we will process your payment using our self-hosted BTCPay Server instance and not share these details with any third-party.</li>
<li>If you accelerate transactions using Mempool Accelerator(tm), we will store the TXID of your transactions you accelerate with us. We share this information with our mining pool partners, as well as publicly display accelerated transaction details on our website and APIs.</li>
</ol>
<br>
<p>EOF</p>
</div>

View File

@ -1,4 +1,4 @@
<form [formGroup]="searchForm" (submit)="searchForm.valid && search()" novalidate>
<form [class]="{hamburgerOpen: hamburgerOpen}" [formGroup]="searchForm" (submit)="searchForm.valid && search()" novalidate>
<div class="d-flex">
<div class="search-box-container mr-2">
<input #searchInput (focus)="focus$.next($any($event).target.value)" (click)="click$.next($any($event).target.value)" formControlName="searchText" type="text" class="form-control" i18n-placeholder="search-form.searchbar-placeholder" placeholder="Explore the full Bitcoin ecosystem">

View File

@ -26,6 +26,13 @@ form {
@media (min-width: 992px) {
width: 100%;
}
&.hamburgerOpen {
@media (max-width: 613px) {
margin-left: 0px;
margin-right: 0px;
}
}
}
.btn-block {

View File

@ -1,4 +1,4 @@
import { Component, OnInit, ChangeDetectionStrategy, EventEmitter, Output, ViewChild, HostListener, ElementRef } from '@angular/core';
import { Component, OnInit, ChangeDetectionStrategy, EventEmitter, Output, ViewChild, HostListener, ElementRef, Input } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { EventType, NavigationStart, Router } from '@angular/router';
import { AssetsService } from '../../services/assets.service';
@ -17,6 +17,8 @@ import { SearchResultsComponent } from './search-results/search-results.componen
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SearchFormComponent implements OnInit {
@Input() hamburgerOpen = false;
network = '';
assets: object = {};
isSearching = false;

View File

@ -6,12 +6,15 @@
<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>
<!-- <div *ngIf="tx && acceleratorAvailable && accelerateCtaType === 'alert' && !tx.status.confirmed && !tx.acceleration" class="alert alert-dismissible alert-purple" role="alert">
<div>
<a class="btn btn-sm blink-bg" (click)="onAccelerateClicked()">Accelerate</a>
<span class="align-middle">this transaction using Mempool Accelerator &trade;</span>
</div>
<button type="button" class="close" aria-label="Close" (click)="dismissAccelAlert()">
<span aria-hidden="true">&times;</span>
</button>
</div>
</div> -->
<ng-container *ngIf="!rbfTransaction || rbfTransaction?.size || tx">
<h1 i18n="shared.transaction">Transaction</h1>
@ -80,13 +83,12 @@
<!-- Accelerator -->
<ng-container *ngIf="!tx?.status?.confirmed && showAccelerationSummary">
<div class="title mt-3" id="acceleratePreviewAnchor">
<div class="title mt-3">
<h2>Accelerate</h2>
</div>
<div class="box">
<app-accelerate-preview [tx]="tx" [scrollEvent]="scrollIntoAccelPreview"></app-accelerate-preview>
</div>
</ng-container>
<ng-template #unconfirmedTemplate>
@ -118,7 +120,7 @@
<ng-template [ngIf]="this.mempoolPosition.block >= 7" [ngIfElse]="belowBlockLimit">
<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>
<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>
<a *ngIf="!tx.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !tx?.acceleration" [href]="'/services/accelerator/accelerate?txid=' + tx.txid" class="btn btn-sm accelerateDeepMempool btn-small-height" i18n="transaction.accelerate|Accelerate button label" (click)="onAccelerateClicked()">Accelerate</a>
</span>
</ng-template>
<ng-template #belowBlockLimit>
@ -128,14 +130,14 @@
<ng-template #timeEstimateDefault>
<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>
<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>
<a *ngIf="!tx.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !tx?.acceleration" [href]="'/services/accelerator/accelerate?txid=' + tx.txid" class="btn btn-sm accelerate btn-small-height" i18n="transaction.accelerate|Accelerate button label" (click)="onAccelerateClicked()">Accelerate</a>
</span>
</ng-template>
</ng-template>
</ng-template>
</td>
</tr>
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'" id="acceleratePreviewAnchor">
<td class="td-width" i18n="transaction.features|Transaction Features">Features</td>
<td>
<app-tx-features [tx]="tx"></app-tx-features>

View File

@ -61,7 +61,7 @@
}
.btn-small-height {
line-height: 1.1;
line-height: 1;
}
.arrow-green {
@ -218,8 +218,33 @@
}
}
.link.accelerator {
cursor: pointer;
.alert-purple {
background-color: #5c3a88;
width: 100%;
}
// Blinking block
@keyframes shadowyBackground {
0% {
box-shadow: 0px 0px 20px rgba(#eba814, 1);
}
50% {
box-shadow: 0px 0px 20px rgba(#eba814, .3);
}
100% {
box-shadow: 0px 0px 20px rgba(#ffae00, 1);
}
}
.blink-bg {
color: #fff;
background: repeating-linear-gradient(#daad0a 0%, #daad0a 5%, #987805 100%) !important;
animation: shadowyBackground 1s infinite;
box-shadow: 0px 0px 20px rgba(#eba814, 1);
transition: 100ms all ease-in;
margin-right: 8px;
font-size: 16px;
border: 1px solid gold;
}
.eta {
@ -234,7 +259,6 @@
.accelerate {
display: flex !important;
align-self: auto;
margin-top: 3px;
margin-left: auto;
background-color: #653b9c;
@media (max-width: 849px) {

View File

@ -92,7 +92,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
rbfEnabled: boolean;
taprootEnabled: boolean;
hasEffectiveFeeRate: boolean;
accelerateCtaType: 'alert' | 'button' = 'alert';
accelerateCtaType: 'alert' | 'button' = 'button';
acceleratorAvailable: boolean = this.stateService.env.OFFICIAL_MEMPOOL_SPACE && this.stateService.env.ACCELERATOR && this.stateService.network === '';
showAccelerationSummary = false;
scrollIntoAccelPreview = false;
@ -126,7 +126,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
}
);
this.accelerateCtaType = (this.storageService.getValue('accel-cta-type') as 'alert' | 'button') ?? 'alert';
this.accelerateCtaType = (this.storageService.getValue('accel-cta-type') as 'alert' | 'button') ?? 'button';
this.setFlowEnabled();
this.flowPrefSubscription = this.stateService.hideFlow.subscribe((hide) => {
@ -633,10 +633,14 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
// simulate normal anchor fragment behavior
applyFragment(): void {
const anchor = Array.from(this.fragmentParams.entries()).find(([frag, value]) => value === '');
if (anchor) {
const anchorElement = document.getElementById(anchor[0]);
if (anchorElement) {
anchorElement.scrollIntoView();
if (anchor?.length) {
if (anchor[0] === 'accelerate') {
setTimeout(this.onAccelerateClicked.bind(this), 100);
} else {
const anchorElement = document.getElementById(anchor[0]);
if (anchorElement) {
anchorElement.scrollIntoView();
}
}
}
}

View File

@ -237,7 +237,7 @@
<div class="card-text" *ngIf="(isLoadingWebSocket$ | async) === false && mempoolInfoData.value; else loadingbig">
<div class="progress">
<div class="progress-bar {{ mempoolInfoData.value.mempoolSizeProgress }}" role="progressbar" [ngStyle]="{'width': (mempoolInfoData.value.memPoolInfo.usage / mempoolInfoData.value.memPoolInfo.maxmempool * 100) + '%' }">&nbsp;</div>
<div class="progress-text">&lrm;<span [innerHTML]="mempoolInfoData.value.memPoolInfo.usage | bytes : 2 : 'B' : null : 3"></span> / <span [innerHTML]="mempoolInfoData.value.memPoolInfo.maxmempool | bytes"></span></div>
<div class="progress-text">&lrm;<span [innerHTML]="mempoolInfoData.value.memPoolInfo.usage | bytes : 2 : 'B' : null : false : 3"></span> / <span [innerHTML]="mempoolInfoData.value.memPoolInfo.maxmempool | bytes"></span></div>
</div>
</div>
</div>

View File

@ -7,6 +7,7 @@ import { SharedModule } from './shared/shared.module';
import { StartComponent } from './components/start/start.component';
import { AddressComponent } from './components/address/address.component';
import { PushTransactionComponent } from './components/push-transaction/push-transaction.component';
import { CalculatorComponent } from './components/calculator/calculator.component';
import { BlocksList } from './components/blocks-list/blocks-list.component';
import { RbfList } from './components/rbf-list/rbf-list.component';
@ -87,6 +88,10 @@ const routes: Routes = [
loadChildren: () => import('./lightning/lightning.module').then(m => m.LightningModule),
data: { preload: browserWindowEnv && browserWindowEnv.LIGHTNING === true, networks: ['bitcoin'] },
},
{
path: 'tools/calculator',
component: CalculatorComponent
},
],
}
];

View File

@ -311,7 +311,7 @@ export class ApiService {
}
getEnterpriseInfo$(name: string): Observable<any> {
return this.httpClient.get<any>(this.apiBaseUrl + this.apiBasePath + `/api/v1/enterprise/info/` + name);
return this.httpClient.get<any>(this.apiBaseUrl + this.apiBasePath + `/api/v1/services/enterprise/info/` + name);
}
getChannelByTxIds$(txIds: string[]): Observable<any[]> {

View File

@ -23,12 +23,12 @@
</div>
<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>
<span *ngIf="!loggedIn" i18n="shared.sign-in">Sign In</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>
<span *ngIf="!loggedIn" i18n="shared.sign-in">Sign In</span>
</a>
<p class="d-none d-sm-block">
<ng-container i18n="@@7deec1c1520f06170e1f8e8ddfbe4532312f638f">Explore the full Bitcoin ecosystem</ng-container>

View File

@ -13,7 +13,7 @@ const MempoolErrors = {
'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_available': `You current subscription does not allow you to access this feature.`,
'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.`,

View File

@ -1,5 +1,4 @@
<span class="truncate" [style.max-width]="maxWidth ? maxWidth + 'px' : null" [style.justify-content]="textAlign" [class.inline]="inline">
<div class="hidden">{{ text }}</div>
<ng-container *ngIf="link">
<a [routerLink]="link" class="truncate-link">
<ng-container *ngIf="rtl; then rtlTruncated; else ltrTruncated;"></ng-container>
@ -12,10 +11,11 @@
</span>
<ng-template #ltrTruncated>
<span class="first">{{text.slice(0,-lastChars)}}</span><span class="last-four">{{text.slice(-lastChars)}}</span>
<div class="hidden-content">{{ text }}</div>
</ng-template>
<ng-template #rtlTruncated>
<span class="first">{{text.slice(lastChars)}}</span><span class="last-four">{{text.slice(0,lastChars)}}</span>
<div class="hidden-content">{{ text }}</div>
</ng-template>

View File

@ -3,6 +3,7 @@
display: flex;
flex-direction: row;
align-items: baseline;
position: relative;
.truncate-link {
display: flex;
@ -27,17 +28,17 @@
&.inline {
display: inline-flex;
}
}
.hidden {
color: transparent;
position: absolute;
max-width: 300px;
overflow: hidden;
.hidden-content {
color: transparent;
position: absolute;
max-width: 300px;
overflow: hidden;
}
}
@media (max-width: 567px) {
.hidden {
max-width: 150px;
.hidden-content {
max-width: 150px !important;
}
}

View File

@ -17,7 +17,7 @@ export class BytesPipe implements PipeTransform {
'TB': {max: Number.MAX_SAFE_INTEGER, prev: 'GB'}
};
transform(input: any, decimal: number = 0, from: ByteUnit = 'B', to?: ByteUnit, sigfigs?: number): any {
transform(input: any, decimal: number = 0, from: ByteUnit = 'B', to?: ByteUnit, plaintext = false, sigfigs?: number): any {
if (!(isNumberFinite(input) &&
isNumberFinite(decimal) &&
@ -42,7 +42,7 @@ export class BytesPipe implements PipeTransform {
const result = numberFormat(BytesPipe.calculateResult(format, bytes));
return BytesPipe.formatResult(result, to);
return BytesPipe.formatResult(result, to, plaintext);
}
for (const key in BytesPipe.formats) {
@ -51,13 +51,17 @@ export class BytesPipe implements PipeTransform {
const result = numberFormat(BytesPipe.calculateResult(format, bytes));
return BytesPipe.formatResult(result, key);
return BytesPipe.formatResult(result, key, plaintext);
}
}
}
static formatResult(result: string, unit: string): string {
return `${result} <span class="symbol">${unit}</span>`;
static formatResult(result: string, unit: string, plaintext): string {
if (plaintext) {
return `${result} ${unit}`;
} else {
return `${result} <span class="symbol">${unit}</span>`;
}
}
static calculateResult(format: { max: number, prev?: ByteUnit }, bytes: number) {

View File

@ -4,7 +4,7 @@ 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, faFastForward, faWallet, faUserClock, faWrench, faUserFriends, faQuestionCircle, faHistory, faSignOutAlt, faKey, faSuitcase, faIdCardAlt, faNetworkWired, faUserCheck, faCircleCheck, faUserCircle } 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, faCheck } from '@fortawesome/free-solid-svg-icons';
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
import { MenuComponent } from '../components/menu/menu.component';
import { PreviewTitleComponent } from '../components/master-page-preview/preview-title.component';
@ -87,6 +87,7 @@ import { AccelerateFeeGraphComponent } from '../components/accelerate-preview/ac
import { MempoolErrorComponent } from './components/mempool-error/mempool-error.component';
import { BlockViewComponent } from '../components/block-view/block-view.component';
import { EightBlocksComponent } from '../components/eight-blocks/eight-blocks.component';
import { MempoolBlockViewComponent } from '../components/mempool-block-view/mempool-block-view.component';
import { MempoolBlockOverviewComponent } from '../components/mempool-block-overview/mempool-block-overview.component';
import { ClockchainComponent } from '../components/clockchain/clockchain.component';
@ -126,6 +127,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
ColoredPriceDirective,
BlockchainComponent,
BlockViewComponent,
EightBlocksComponent,
MempoolBlockViewComponent,
MempoolBlocksComponent,
BlockchainBlocksComponent,
@ -179,6 +181,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
CalculatorComponent,
BitcoinsatoshisPipe,
BlockViewComponent,
EightBlocksComponent,
MempoolBlockViewComponent,
MempoolBlockOverviewComponent,
ClockchainComponent,
@ -202,6 +205,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
FontAwesomeModule,
],
providers: [
BytesPipe,
VbytesPipe,
WuBytesPipe,
RelativeUrlPipe,
@ -364,5 +368,6 @@ export class SharedModule {
library.addIcons(faUserCheck);
library.addIcons(faCircleCheck);
library.addIcons(faUserCircle);
library.addIcons(faCheck);
}
}

View File

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" data-v-4fa90e7f=""><path d="M14.33 7.17C13.588 7.058 12.807 7 12 7c-4.97 0-9 2.239-9 5 0 1.44 1.096 2.738 2.85 3.65l2.362-2.362a4 4 0 015.076-5.076l1.043-1.043zM11.23 15.926a4 4 0 004.695-4.695l2.648-2.647C20.078 9.478 21 10.68 21 12c0 2.761-4.03 5-9 5-.598 0-1.183-.032-1.749-.094l.98-.98zM17.793 5.207a1 1 0 111.414 1.414L6.48 19.35a1 1 0 11-1.414-1.414L17.793 5.207z"></path></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="lightgrey" data-v-4fa90e7f=""><path d="M14.33 7.17C13.588 7.058 12.807 7 12 7c-4.97 0-9 2.239-9 5 0 1.44 1.096 2.738 2.85 3.65l2.362-2.362a4 4 0 015.076-5.076l1.043-1.043zM11.23 15.926a4 4 0 004.695-4.695l2.648-2.647C20.078 9.478 21 10.68 21 12c0 2.761-4.03 5-9 5-.598 0-1.183-.032-1.749-.094l.98-.98zM17.793 5.207a1 1 0 111.414 1.414L6.48 19.35a1 1 0 11-1.414-1.414L17.793 5.207z"></path></svg>

Before

Width:  |  Height:  |  Size: 464 B

After

Width:  |  Height:  |  Size: 461 B

View File

@ -519,7 +519,7 @@ html:lang(ru) .card-title {
.fees-wrapper-tooltip-chart-advanced,
.tx-wrapper-tooltip-chart-advanced {
background: rgba(#1d1f31, 0.98);
width: 310px;
width: 300px;
thead {
th {

View File

@ -23,7 +23,7 @@
"PASSWORD": "__BITCOIN_RPC_PASS__"
},
"ESPLORA": {
"UNIX_SOCKET_PATH": "/elements/socket/esplora-liquid-mainnet",
"UNIX_SOCKET_PATH": "/elements/socket/esplora-elements-liquid",
"FALLBACK": [
"http://node201.va1.mempool.space:3001",
"http://node202.va1.mempool.space:3001",

View File

@ -23,7 +23,7 @@
"PASSWORD": "__BITCOIN_RPC_PASS__"
},
"ESPLORA": {
"UNIX_SOCKET_PATH": "/elements/socket/esplora-liquid-testnet",
"UNIX_SOCKET_PATH": "/elements/socket/esplora-elements-liquidtestnet",
"FALLBACK": [
"http://node201.va1.mempool.space:3004",
"http://node202.va1.mempool.space:3004",

View File

@ -1,96 +1,158 @@
location /api/v1/services {
proxy_pass $mempoolSpaceServices;
proxy_cache services;
proxy_cache_background_update on;
proxy_cache_use_stale updating;
proxy_cache_valid 200 1d;
expires 1d;
proxy_hide_header onion-location;
proxy_hide_header strict-transport-security;
proxy_hide_header content-security-policy;
proxy_hide_header x-frame-options;
}
location /api/v1/contributors/images {
proxy_pass $mempoolSpaceServices;
proxy_cache services;
proxy_cache_background_update on;
proxy_cache_use_stale updating;
proxy_cache_valid 200 1w;
expires 1w;
proxy_hide_header onion-location;
proxy_hide_header strict-transport-security;
proxy_hide_header content-security-policy;
proxy_hide_header x-frame-options;
###########
# routing #
###########
location /api/v1/assets {
try_files /dev/null @mempool-api-v1-services-cache-short;
}
location /api/v1/contributors {
proxy_pass $mempoolSpaceServices;
proxy_cache services;
proxy_cache_background_update on;
proxy_cache_use_stale updating;
proxy_cache_valid 200 1d;
expires 1d;
proxy_hide_header onion-location;
proxy_hide_header strict-transport-security;
proxy_hide_header content-security-policy;
proxy_hide_header x-frame-options;
try_files /dev/null @mempool-api-v1-services-cache-medium;
}
location /api/v1/donations/images {
proxy_pass $mempoolSpaceServices;
proxy_cache services;
proxy_cache_background_update on;
proxy_cache_use_stale updating;
proxy_cache_valid 200 1w;
expires 1w;
proxy_hide_header onion-location;
proxy_hide_header strict-transport-security;
proxy_hide_header content-security-policy;
proxy_hide_header x-frame-options;
location /api/v1/contributors/images {
try_files /dev/null @mempool-api-v1-services-cache-long;
}
location /api/v1/donations {
proxy_pass $mempoolSpaceServices;
proxy_cache services;
proxy_cache_background_update on;
proxy_cache_use_stale updating;
proxy_cache_valid 200 1d;
expires 1d;
proxy_hide_header onion-location;
proxy_hide_header strict-transport-security;
proxy_hide_header content-security-policy;
proxy_hide_header x-frame-options;
try_files /dev/null @mempool-api-v1-services-cache-medium;
}
location /api/v1/translators/images {
proxy_pass $mempoolSpaceServices;
proxy_cache services;
proxy_cache_background_update on;
proxy_cache_use_stale updating;
proxy_cache_valid 200 1w;
expires 1w;
proxy_hide_header onion-location;
proxy_hide_header strict-transport-security;
proxy_hide_header content-security-policy;
proxy_hide_header x-frame-options;
location /api/v1/donations/images {
try_files /dev/null @mempool-api-v1-services-cache-long;
}
location /api/v1/translators {
try_files /dev/null @mempool-api-v1-services-cache-medium;
}
location /api/v1/translators/images {
try_files /dev/null @mempool-api-v1-services-cache-long;
}
location /api/v1/services/internal/ {
return 403;
}
location /api/v1/services/ {
try_files /dev/null @mempool-api-v1-services-cache-disabled;
}
###########
# caching #
###########
location @mempool-api-v1-services-cache-disabled {
proxy_pass $mempoolSpaceServices;
# remove these just in case double proxied
proxy_hide_header Onion-Location;
proxy_hide_header Strict-Transport-Security;
proxy_hide_header Content-Security-Policy;
proxy_hide_header X-Frame-Options;
# remove cache headers from services backend
proxy_hide_header Cache-Control;
proxy_hide_header Expires;
proxy_hide_header Pragma;
# pass these headers to services backend
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# add our own cache headers
add_header 'Pragma' 'no-cache';
add_header 'Cache-Control' 'private, no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0';
proxy_redirect off;
proxy_buffering off;
expires -1;
}
location @mempool-api-v1-services-cache-short {
proxy_pass $mempoolSpaceServices;
proxy_cache services;
proxy_cache_background_update on;
proxy_cache_use_stale updating;
proxy_cache_valid 200 1d;
expires 1d;
proxy_hide_header onion-location;
proxy_hide_header strict-transport-security;
proxy_hide_header content-security-policy;
proxy_hide_header x-frame-options;
proxy_redirect off;
# remove these just in case double proxied
proxy_hide_header Onion-Location;
proxy_hide_header Strict-Transport-Security;
proxy_hide_header Content-Security-Policy;
proxy_hide_header X-Frame-Options;
# pass these headers to services backend
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# remove cache headers from services backend
proxy_hide_header Cache-Control;
proxy_hide_header Expires;
proxy_hide_header Pragma;
# add our own cache headers
add_header 'Pragma' 'public';
add_header 'Cache-Control' 'public';
expires 2s;
# server-side cache validity
proxy_cache_valid 200 2s;
}
location /api/v1/assets {
location @mempool-api-v1-services-cache-medium {
proxy_pass $mempoolSpaceServices;
proxy_cache services;
proxy_cache_background_update on;
proxy_cache_use_stale updating;
proxy_cache_valid 200 10m;
expires 10m;
proxy_hide_header onion-location;
proxy_hide_header strict-transport-security;
proxy_hide_header content-security-policy;
proxy_hide_header x-frame-options;
proxy_redirect off;
# remove these just in case double proxied
proxy_hide_header Onion-Location;
proxy_hide_header Strict-Transport-Security;
proxy_hide_header Content-Security-Policy;
proxy_hide_header X-Frame-Options;
# pass these headers to services backend
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# remove cache headers from services backend
proxy_hide_header Cache-Control;
proxy_hide_header Expires;
proxy_hide_header Pragma;
# add our own cache headers
add_header 'Pragma' 'public';
add_header 'Cache-Control' 'public';
expires 7d;
# server-side cache validity
proxy_cache_valid 200 7d;
}
location @mempool-api-v1-services-cache-long {
proxy_pass $mempoolSpaceServices;
proxy_cache services;
proxy_redirect off;
# remove these just in case double proxied
proxy_hide_header Onion-Location;
proxy_hide_header Strict-Transport-Security;
proxy_hide_header Content-Security-Policy;
proxy_hide_header X-Frame-Options;
# pass these headers to services backend
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# remove cache headers from services backend
proxy_hide_header Cache-Control;
proxy_hide_header Expires;
proxy_hide_header Pragma;
# add our own cache headers
add_header 'Pragma' 'public';
add_header 'Cache-Control' 'public';
expires 30d;
# server-side cache validity
proxy_cache_valid 200 30d;
}

View File

@ -4,10 +4,10 @@
# Block the internal APIs of esplora
location /api/internal/ {
return 404;
return 403;
}
location /api/v1/internal/ {
return 404;
return 403;
}
# websocket has special HTTP headers