Merge branch 'master' into nymkappa/bugfix/node-count
This commit is contained in:
@@ -74,12 +74,14 @@ let routes: Routes = [
|
||||
children: [],
|
||||
component: AddressComponent,
|
||||
data: {
|
||||
ogImage: true
|
||||
ogImage: true,
|
||||
networkSpecific: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'tx',
|
||||
component: StartComponent,
|
||||
data: { networkSpecific: true },
|
||||
children: [
|
||||
{
|
||||
path: ':id',
|
||||
@@ -90,6 +92,7 @@ let routes: Routes = [
|
||||
{
|
||||
path: 'block',
|
||||
component: StartComponent,
|
||||
data: { networkSpecific: true },
|
||||
children: [
|
||||
{
|
||||
path: ':id',
|
||||
@@ -102,6 +105,7 @@ let routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: 'block-audit',
|
||||
data: { networkSpecific: true },
|
||||
children: [
|
||||
{
|
||||
path: ':id',
|
||||
@@ -121,12 +125,13 @@ let routes: Routes = [
|
||||
{
|
||||
path: 'lightning',
|
||||
loadChildren: () => import('./lightning/lightning.module').then(m => m.LightningModule),
|
||||
data: { preload: browserWindowEnv && browserWindowEnv.LIGHTNING === true },
|
||||
data: { preload: browserWindowEnv && browserWindowEnv.LIGHTNING === true, networks: ['bitcoin'] },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'status',
|
||||
data: { networks: ['bitcoin', 'liquid'] },
|
||||
component: StatusViewComponent
|
||||
},
|
||||
{
|
||||
@@ -185,11 +190,13 @@ let routes: Routes = [
|
||||
children: [],
|
||||
component: AddressComponent,
|
||||
data: {
|
||||
ogImage: true
|
||||
ogImage: true,
|
||||
networkSpecific: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'tx',
|
||||
data: { networkSpecific: true },
|
||||
component: StartComponent,
|
||||
children: [
|
||||
{
|
||||
@@ -200,6 +207,7 @@ let routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: 'block',
|
||||
data: { networkSpecific: true },
|
||||
component: StartComponent,
|
||||
children: [
|
||||
{
|
||||
@@ -213,6 +221,7 @@ let routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: 'block-audit',
|
||||
data: { networkSpecific: true },
|
||||
children: [
|
||||
{
|
||||
path: ':id',
|
||||
@@ -230,12 +239,14 @@ let routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: 'lightning',
|
||||
data: { networks: ['bitcoin'] },
|
||||
loadChildren: () => import('./lightning/lightning.module').then(m => m.LightningModule)
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'status',
|
||||
data: { networks: ['bitcoin', 'liquid'] },
|
||||
component: StatusViewComponent
|
||||
},
|
||||
{
|
||||
@@ -291,11 +302,13 @@ let routes: Routes = [
|
||||
children: [],
|
||||
component: AddressComponent,
|
||||
data: {
|
||||
ogImage: true
|
||||
ogImage: true,
|
||||
networkSpecific: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'tx',
|
||||
data: { networkSpecific: true },
|
||||
component: StartComponent,
|
||||
children: [
|
||||
{
|
||||
@@ -306,6 +319,7 @@ let routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: 'block',
|
||||
data: { networkSpecific: true },
|
||||
component: StartComponent,
|
||||
children: [
|
||||
{
|
||||
@@ -319,6 +333,7 @@ let routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: 'block-audit',
|
||||
data: { networkSpecific: true },
|
||||
children: [
|
||||
{
|
||||
path: ':id',
|
||||
@@ -336,6 +351,7 @@ let routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: 'lightning',
|
||||
data: { networks: ['bitcoin'] },
|
||||
loadChildren: () => import('./lightning/lightning.module').then(m => m.LightningModule)
|
||||
},
|
||||
],
|
||||
@@ -359,6 +375,7 @@ let routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: 'status',
|
||||
data: { networks: ['bitcoin', 'liquid'] },
|
||||
component: StatusViewComponent
|
||||
},
|
||||
{
|
||||
@@ -422,11 +439,13 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
||||
children: [],
|
||||
component: AddressComponent,
|
||||
data: {
|
||||
ogImage: true
|
||||
ogImage: true,
|
||||
networkSpecific: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'tx',
|
||||
data: { networkSpecific: true },
|
||||
component: StartComponent,
|
||||
children: [
|
||||
{
|
||||
@@ -437,6 +456,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
||||
},
|
||||
{
|
||||
path: 'block',
|
||||
data: { networkSpecific: true },
|
||||
component: StartComponent,
|
||||
children: [
|
||||
{
|
||||
@@ -450,18 +470,22 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
||||
},
|
||||
{
|
||||
path: 'assets',
|
||||
data: { networks: ['liquid'] },
|
||||
component: AssetsNavComponent,
|
||||
children: [
|
||||
{
|
||||
path: 'all',
|
||||
data: { networks: ['liquid'] },
|
||||
component: AssetsComponent,
|
||||
},
|
||||
{
|
||||
path: 'asset/:id',
|
||||
data: { networkSpecific: true },
|
||||
component: AssetComponent
|
||||
},
|
||||
{
|
||||
path: 'group/:id',
|
||||
data: { networkSpecific: true },
|
||||
component: AssetGroupComponent
|
||||
},
|
||||
{
|
||||
@@ -482,6 +506,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
||||
},
|
||||
{
|
||||
path: 'status',
|
||||
data: { networks: ['bitcoin', 'liquid'] },
|
||||
component: StatusViewComponent
|
||||
},
|
||||
{
|
||||
@@ -532,11 +557,13 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
||||
children: [],
|
||||
component: AddressComponent,
|
||||
data: {
|
||||
ogImage: true
|
||||
ogImage: true,
|
||||
networkSpecific: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'tx',
|
||||
data: { networkSpecific: true },
|
||||
component: StartComponent,
|
||||
children: [
|
||||
{
|
||||
@@ -547,6 +574,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
||||
},
|
||||
{
|
||||
path: 'block',
|
||||
data: { networkSpecific: true },
|
||||
component: StartComponent,
|
||||
children: [
|
||||
{
|
||||
@@ -560,22 +588,27 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
||||
},
|
||||
{
|
||||
path: 'assets',
|
||||
data: { networks: ['liquid'] },
|
||||
component: AssetsNavComponent,
|
||||
children: [
|
||||
{
|
||||
path: 'featured',
|
||||
data: { networkSpecific: true },
|
||||
component: AssetsFeaturedComponent,
|
||||
},
|
||||
{
|
||||
path: 'all',
|
||||
data: { networks: ['liquid'] },
|
||||
component: AssetsComponent,
|
||||
},
|
||||
{
|
||||
path: 'asset/:id',
|
||||
data: { networkSpecific: true },
|
||||
component: AssetComponent
|
||||
},
|
||||
{
|
||||
path: 'group/:id',
|
||||
data: { networkSpecific: true },
|
||||
component: AssetGroupComponent
|
||||
},
|
||||
{
|
||||
@@ -609,6 +642,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
||||
},
|
||||
{
|
||||
path: 'status',
|
||||
data: { networks: ['bitcoin', 'liquid']},
|
||||
component: StatusViewComponent
|
||||
},
|
||||
{
|
||||
|
||||
@@ -20,14 +20,17 @@ const routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: 'markets',
|
||||
data: { networks: ['bisq'] },
|
||||
component: BisqDashboardComponent,
|
||||
},
|
||||
{
|
||||
path: 'transactions',
|
||||
data: { networks: ['bisq'] },
|
||||
component: BisqTransactionsComponent
|
||||
},
|
||||
{
|
||||
path: 'market/:pair',
|
||||
data: { networkSpecific: true },
|
||||
component: BisqMarketComponent,
|
||||
},
|
||||
{
|
||||
@@ -36,6 +39,7 @@ const routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: 'tx/:id',
|
||||
data: { networkSpecific: true },
|
||||
component: BisqTransactionComponent
|
||||
},
|
||||
{
|
||||
@@ -45,14 +49,17 @@ const routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: 'block/:id',
|
||||
data: { networkSpecific: true },
|
||||
component: BisqBlockComponent,
|
||||
},
|
||||
{
|
||||
path: 'address/:id',
|
||||
data: { networkSpecific: true },
|
||||
component: BisqAddressComponent,
|
||||
},
|
||||
{
|
||||
path: 'stats',
|
||||
data: { networks: ['bisq'] },
|
||||
component: BisqStatsComponent,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -25,6 +25,8 @@ export class AppComponent implements OnInit {
|
||||
if (this.locale.startsWith('ar') || this.locale.startsWith('fa') || this.locale.startsWith('he')) {
|
||||
this.dir = 'rtl';
|
||||
this.class = 'rtl-layout';
|
||||
} else {
|
||||
this.class = 'ltr-layout';
|
||||
}
|
||||
|
||||
tooltipConfig.animation = false;
|
||||
|
||||
@@ -44,13 +44,13 @@
|
||||
<app-svg-images name="bisq" width="20" height="20" viewBox="0 0 75 75" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images>
|
||||
</button>
|
||||
<div ngbDropdownMenu [ngClass]="{'dropdown-menu-right' : isMobile}">
|
||||
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage" ngbDropdownItem class="mainnet"><app-svg-images name="bitcoin" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Mainnet</a>
|
||||
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + '/signet'" ngbDropdownItem *ngIf="env.SIGNET_ENABLED" class="signet"><app-svg-images name="signet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Signet</a>
|
||||
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + '/testnet'" ngbDropdownItem *ngIf="env.TESTNET_ENABLED" class="testnet"><app-svg-images name="testnet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Testnet</a>
|
||||
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['mainnet'] || '/')" ngbDropdownItem class="mainnet"><app-svg-images name="bitcoin" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Mainnet</a>
|
||||
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['signet'] || '/signet')" ngbDropdownItem *ngIf="env.SIGNET_ENABLED" class="signet"><app-svg-images name="signet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Signet</a>
|
||||
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['testnet'] || '/testnet')" ngbDropdownItem *ngIf="env.TESTNET_ENABLED" class="testnet"><app-svg-images name="testnet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Testnet</a>
|
||||
<h6 class="dropdown-header" i18n="master-page.layer2-networks-header">Layer 2 Networks</h6>
|
||||
<a ngbDropdownItem class="mainnet active" routerLink="/"><app-svg-images name="bisq" width="20" height="20" viewBox="0 0 75 75" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Bisq</a>
|
||||
<a [href]="env.LIQUID_WEBSITE_URL + urlLanguage" ngbDropdownItem *ngIf="env.LIQUID_ENABLED" class="liquid"><app-svg-images name="liquid" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid</a>
|
||||
<a [href]="env.LIQUID_WEBSITE_URL + urlLanguage + '/testnet'" ngbDropdownItem *ngIf="env.LIQUID_TESTNET_ENABLED" class="liquidtestnet"><app-svg-images name="liquidtestnet" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid Testnet</a>
|
||||
<a ngbDropdownItem class="mainnet active" [routerLink]="networkPaths['bisq'] || '/'"><app-svg-images name="bisq" width="20" height="20" viewBox="0 0 75 75" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Bisq</a>
|
||||
<a [href]="env.LIQUID_WEBSITE_URL + urlLanguage + (networkPaths['liquid'] || '/')" ngbDropdownItem *ngIf="env.LIQUID_ENABLED" class="liquid"><app-svg-images name="liquid" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid</a>
|
||||
<a [href]="env.LIQUID_WEBSITE_URL + urlLanguage + (networkPaths['liquidtestnet'] || '/testnet')" ngbDropdownItem *ngIf="env.LIQUID_TESTNET_ENABLED" class="liquidtestnet"><app-svg-images name="liquidtestnet" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid Testnet</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Env, StateService } from '../../services/state.service';
|
||||
import { Observable } from 'rxjs';
|
||||
import { LanguageService } from '../../services/language.service';
|
||||
import { EnterpriseService } from '../../services/enterprise.service';
|
||||
import { NavigationService } from '../../services/navigation.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bisq-master-page',
|
||||
@@ -15,17 +16,23 @@ export class BisqMasterPageComponent implements OnInit {
|
||||
env: Env;
|
||||
isMobile = window.innerWidth <= 767.98;
|
||||
urlLanguage: string;
|
||||
networkPaths: { [network: string]: string };
|
||||
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
private languageService: LanguageService,
|
||||
private enterpriseService: EnterpriseService,
|
||||
private navigationService: NavigationService,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.env = this.stateService.env;
|
||||
this.connectionState$ = this.stateService.connectionState$;
|
||||
this.urlLanguage = this.languageService.getLanguageForUrl();
|
||||
this.navigationService.subnetPaths.subscribe((paths) => {
|
||||
console.log('network paths updated...');
|
||||
this.networkPaths = paths;
|
||||
});
|
||||
}
|
||||
|
||||
collapse(): void {
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
<div class="container-xl" (window:resize)="onResize($event)">
|
||||
|
||||
<div *ngIf="(auditObservable$ | async) as blockAudit; else skeleton">
|
||||
<div class="title-block" id="block">
|
||||
<h1>
|
||||
<span class="next-previous-blocks">
|
||||
<span i18n="shared.block-title">Block </span>
|
||||
|
||||
<a [routerLink]="['/block/' | relativeUrl, blockAudit.id]">{{ blockAudit.height }}</a>
|
||||
|
||||
<span i18n="shared.template-vs-mined">Template vs Mined</span>
|
||||
</span>
|
||||
</h1>
|
||||
<div class="title-block" id="block">
|
||||
<h1>
|
||||
<span class="next-previous-blocks">
|
||||
<span i18n="shared.block-audit-title">Block Audit</span>
|
||||
|
||||
<a *ngIf="blockAudit" [routerLink]="['/block/' | relativeUrl, blockHash]">{{ blockAudit.height }}</a>
|
||||
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
<div class="grow"></div>
|
||||
<div class="grow"></div>
|
||||
|
||||
<button [routerLink]="['/' | relativeUrl]" class="btn btn-sm">✕</button>
|
||||
</div>
|
||||
<button [routerLink]="['/block/' | relativeUrl, blockHash]" class="btn btn-sm">✕</button>
|
||||
</div>
|
||||
|
||||
<div *ngIf="!error && !isLoading">
|
||||
|
||||
|
||||
<!-- OVERVIEW -->
|
||||
<div class="box mb-3">
|
||||
@@ -26,8 +27,8 @@
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="td-width" i18n="block.hash">Hash</td>
|
||||
<td><a [routerLink]="['/block/' | relativeUrl, blockAudit.id]" title="{{ blockAudit.id }}">{{ blockAudit.id | shortenString : 13 }}</a>
|
||||
<app-clipboard class="d-none d-sm-inline-block" [text]="blockAudit.id"></app-clipboard>
|
||||
<td><a [routerLink]="['/block/' | relativeUrl, blockHash]" title="{{ blockHash }}">{{ blockHash | shortenString : 13 }}</a>
|
||||
<app-clipboard class="d-none d-sm-inline-block" [text]="blockHash"></app-clipboard>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -40,6 +41,10 @@
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="td-width" i18n="shared.transaction-count">Transactions</td>
|
||||
<td>{{ blockAudit.tx_count }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="blockAudit.size">Size</td>
|
||||
<td [innerHTML]="'‎' + (blockAudit.size | bytes: 2)"></td>
|
||||
@@ -57,21 +62,25 @@
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="td-width" i18n="shared.transaction-count">Transactions</td>
|
||||
<td>{{ blockAudit.tx_count }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="block.match-rate">Match rate</td>
|
||||
<td i18n="block.health">Block health</td>
|
||||
<td>{{ blockAudit.matchRate }}%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="block.missing-txs">Missing txs</td>
|
||||
<td i18n="block.missing-txs">Removed txs</td>
|
||||
<td>{{ blockAudit.missingTxs.length }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="block.missing-txs">Omitted txs</td>
|
||||
<td>{{ numMissing }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="block.added-txs">Added txs</td>
|
||||
<td>{{ blockAudit.addedTxs.length }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="block.missing-txs">Included txs</td>
|
||||
<td>{{ numUnexpected }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -79,33 +88,110 @@
|
||||
</div> <!-- box -->
|
||||
|
||||
<!-- ADDED vs MISSING button -->
|
||||
<div class="d-flex justify-content-center menu mt-3" *ngIf="isMobile">
|
||||
<a routerLinkActive="active" class="btn btn-primary w-50 mr-1 ml-1 menu-button" i18n="block.missing-txs"
|
||||
fragment="missing" (click)="changeMode('missing')">Missing</a>
|
||||
<a routerLinkActive="active" class="btn btn-primary w-50 mr-1 ml-1 menu-button" i18n="block.added-txs"
|
||||
fragment="added" (click)="changeMode('added')">Added</a>
|
||||
<div class="d-flex justify-content-center menu mt-3 mb-3" *ngIf="isMobile">
|
||||
<a class="btn btn-primary w-50 mr-1 ml-1 menu-button" [class.active]="mode === 'projected'" i18n="block.projected"
|
||||
fragment="projected" (click)="changeMode('projected')">Projected</a>
|
||||
<a class="btn btn-primary w-50 mr-1 ml-1 menu-button" [class.active]="mode === 'actual'" i18n="block.actual"
|
||||
fragment="actual" (click)="changeMode('actual')">Actual</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-template [ngIf]="!error && isLoading">
|
||||
<div class="title-block" id="block">
|
||||
<h1>
|
||||
<span class="next-previous-blocks">
|
||||
<span i18n="shared.block-audit-title">Block Audit</span>
|
||||
|
||||
<a *ngIf="blockAudit" [routerLink]="['/block/' | relativeUrl, blockHash]">{{ blockAudit.height }}</a>
|
||||
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
<div class="grow"></div>
|
||||
|
||||
<button [routerLink]="['/' | relativeUrl]" class="btn btn-sm">✕</button>
|
||||
</div>
|
||||
|
||||
<!-- OVERVIEW -->
|
||||
<div class="box mb-3">
|
||||
<div class="row">
|
||||
<!-- LEFT COLUMN -->
|
||||
<div class="col-sm">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr><td class="td-width" colspan="2"><span class="skeleton-loader"></span></td></tr>
|
||||
<tr><td class="td-width" colspan="2"><span class="skeleton-loader"></span></td></tr>
|
||||
<tr><td class="td-width" colspan="2"><span class="skeleton-loader"></span></td></tr>
|
||||
<tr><td class="td-width" colspan="2"><span class="skeleton-loader"></span></td></tr>
|
||||
<tr><td class="td-width" colspan="2"><span class="skeleton-loader"></span></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- RIGHT COLUMN -->
|
||||
<div class="col-sm">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr><td class="td-width" colspan="2"><span class="skeleton-loader"></span></td></tr>
|
||||
<tr><td class="td-width" colspan="2"><span class="skeleton-loader"></span></td></tr>
|
||||
<tr><td class="td-width" colspan="2"><span class="skeleton-loader"></span></td></tr>
|
||||
<tr><td class="td-width" colspan="2"><span class="skeleton-loader"></span></td></tr>
|
||||
<tr><td class="td-width" colspan="2"><span class="skeleton-loader"></span></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div> <!-- row -->
|
||||
</div> <!-- box -->
|
||||
|
||||
<!-- ADDED vs MISSING button -->
|
||||
<div class="d-flex justify-content-center menu mt-3 mb-3" *ngIf="isMobile">
|
||||
<a class="btn btn-primary w-50 mr-1 ml-1 menu-button" [class.active]="mode === 'projected'" i18n="block.projected"
|
||||
fragment="projected" (click)="changeMode('projected')">Projected</a>
|
||||
<a class="btn btn-primary w-50 mr-1 ml-1 menu-button" [class.active]="mode === 'actual'" i18n="block.actual"
|
||||
fragment="actual" (click)="changeMode('actual')">Actual</a>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template [ngIf]="error">
|
||||
<div *ngIf="error && error.status === 404; else generalError" class="text-center">
|
||||
<br>
|
||||
<b i18n="error.audit-unavailable">audit unavailable</b>
|
||||
<br><br>
|
||||
<i>{{ error.error }}</i>
|
||||
<br>
|
||||
<br>
|
||||
</div>
|
||||
<ng-template #generalError>
|
||||
<div class="text-center">
|
||||
<br>
|
||||
<span i18n="error.general-loading-data">Error loading data.</span>
|
||||
<br><br>
|
||||
<i>{{ error }}</i>
|
||||
<br>
|
||||
<br>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
|
||||
<!-- VISUALIZATIONS -->
|
||||
<div class="box">
|
||||
<div class="box" *ngIf="!error">
|
||||
<div class="row">
|
||||
<!-- MISSING TX RENDERING -->
|
||||
<div class="col-sm" *ngIf="webGlEnabled">
|
||||
<app-block-overview-graph #blockGraphTemplate [isLoading]="isLoading" [resolution]="75"
|
||||
<h3 class="block-subtitle" *ngIf="!isMobile" i18n="block.projected-block">Projected Block</h3>
|
||||
<app-block-overview-graph #blockGraphProjected [isLoading]="isLoading" [resolution]="75"
|
||||
[blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false"
|
||||
(txClickEvent)="onTxClick($event)"></app-block-overview-graph>
|
||||
</div>
|
||||
|
||||
<!-- ADDED TX RENDERING -->
|
||||
<div class="col-sm" *ngIf="webGlEnabled && !isMobile">
|
||||
<app-block-overview-graph #blockGraphMined [isLoading]="isLoading" [resolution]="75"
|
||||
<h3 class="block-subtitle" *ngIf="!isMobile" i18n="block.actual-block">Actual Block</h3>
|
||||
<app-block-overview-graph #blockGraphActual [isLoading]="isLoading" [resolution]="75"
|
||||
[blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false"
|
||||
(txClickEvent)="onTxClick($event)"></app-block-overview-graph>
|
||||
</div>
|
||||
</div> <!-- row -->
|
||||
</div> <!-- box -->
|
||||
|
||||
<ng-template #skeleton></ng-template>
|
||||
|
||||
</div>
|
||||
@@ -37,4 +37,8 @@
|
||||
@media (min-width: 768px) {
|
||||
max-width: 150px;
|
||||
}
|
||||
}
|
||||
|
||||
.block-subtitle {
|
||||
text-align: center;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
||||
import { Component, OnDestroy, OnInit, AfterViewInit, ViewChildren, QueryList } from '@angular/core';
|
||||
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map, share, switchMap, tap } from 'rxjs/operators';
|
||||
import { Subscription, combineLatest } from 'rxjs';
|
||||
import { map, switchMap, startWith, catchError } from 'rxjs/operators';
|
||||
import { BlockAudit, TransactionStripped } from '../../interfaces/node-api.interface';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { StateService } from '../../services/state.service';
|
||||
@@ -22,22 +22,30 @@ import { BlockOverviewGraphComponent } from '../block-overview-graph/block-overv
|
||||
}
|
||||
`],
|
||||
})
|
||||
export class BlockAuditComponent implements OnInit, OnDestroy {
|
||||
export class BlockAuditComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
blockAudit: BlockAudit = undefined;
|
||||
transactions: string[];
|
||||
auditObservable$: Observable<BlockAudit>;
|
||||
auditSubscription: Subscription;
|
||||
urlFragmentSubscription: Subscription;
|
||||
|
||||
paginationMaxSize: number;
|
||||
page = 1;
|
||||
itemsPerPage: number;
|
||||
|
||||
mode: 'missing' | 'added' = 'missing';
|
||||
mode: 'projected' | 'actual' = 'projected';
|
||||
error: any;
|
||||
isLoading = true;
|
||||
webGlEnabled = true;
|
||||
isMobile = window.innerWidth <= 767.98;
|
||||
|
||||
@ViewChild('blockGraphTemplate') blockGraphTemplate: BlockOverviewGraphComponent;
|
||||
@ViewChild('blockGraphMined') blockGraphMined: BlockOverviewGraphComponent;
|
||||
childChangeSubscription: Subscription;
|
||||
|
||||
blockHash: string;
|
||||
numMissing: number = 0;
|
||||
numUnexpected: number = 0;
|
||||
|
||||
@ViewChildren('blockGraphProjected') blockGraphProjected: QueryList<BlockOverviewGraphComponent>;
|
||||
@ViewChildren('blockGraphActual') blockGraphActual: QueryList<BlockOverviewGraphComponent>;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
@@ -48,73 +56,137 @@ export class BlockAuditComponent implements OnInit, OnDestroy {
|
||||
this.webGlEnabled = detectWebGL();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
ngOnDestroy() {
|
||||
this.childChangeSubscription.unsubscribe();
|
||||
this.urlFragmentSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.paginationMaxSize = window.matchMedia('(max-width: 670px)').matches ? 3 : 5;
|
||||
this.itemsPerPage = this.stateService.env.ITEMS_PER_PAGE;
|
||||
|
||||
this.auditObservable$ = this.route.paramMap.pipe(
|
||||
this.urlFragmentSubscription = this.route.fragment.subscribe((fragment) => {
|
||||
if (fragment === 'actual') {
|
||||
this.mode = 'actual';
|
||||
} else {
|
||||
this.mode = 'projected'
|
||||
}
|
||||
this.setupBlockGraphs();
|
||||
});
|
||||
|
||||
this.auditSubscription = this.route.paramMap.pipe(
|
||||
switchMap((params: ParamMap) => {
|
||||
const blockHash: string = params.get('id') || '';
|
||||
return this.apiService.getBlockAudit$(blockHash)
|
||||
this.blockHash = params.get('id') || null;
|
||||
if (!this.blockHash) {
|
||||
return null;
|
||||
}
|
||||
return this.apiService.getBlockAudit$(this.blockHash)
|
||||
.pipe(
|
||||
map((response) => {
|
||||
const blockAudit = response.body;
|
||||
for (let i = 0; i < blockAudit.template.length; ++i) {
|
||||
if (blockAudit.missingTxs.includes(blockAudit.template[i].txid)) {
|
||||
blockAudit.template[i].status = 'missing';
|
||||
} else if (blockAudit.addedTxs.includes(blockAudit.template[i].txid)) {
|
||||
blockAudit.template[i].status = 'added';
|
||||
const inTemplate = {};
|
||||
const inBlock = {};
|
||||
const isAdded = {};
|
||||
const isCensored = {};
|
||||
const isMissing = {};
|
||||
const isSelected = {};
|
||||
this.numMissing = 0;
|
||||
this.numUnexpected = 0;
|
||||
for (const tx of blockAudit.template) {
|
||||
inTemplate[tx.txid] = true;
|
||||
}
|
||||
for (const tx of blockAudit.transactions) {
|
||||
inBlock[tx.txid] = true;
|
||||
}
|
||||
for (const txid of blockAudit.addedTxs) {
|
||||
isAdded[txid] = true;
|
||||
}
|
||||
for (const txid of blockAudit.missingTxs) {
|
||||
isCensored[txid] = true;
|
||||
}
|
||||
// set transaction statuses
|
||||
for (const tx of blockAudit.template) {
|
||||
if (isCensored[tx.txid]) {
|
||||
tx.status = 'censored';
|
||||
} else if (inBlock[tx.txid]) {
|
||||
tx.status = 'found';
|
||||
} else {
|
||||
blockAudit.template[i].status = 'found';
|
||||
tx.status = 'missing';
|
||||
isMissing[tx.txid] = true;
|
||||
this.numMissing++;
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < blockAudit.transactions.length; ++i) {
|
||||
if (blockAudit.missingTxs.includes(blockAudit.transactions[i].txid)) {
|
||||
blockAudit.transactions[i].status = 'missing';
|
||||
} else if (blockAudit.addedTxs.includes(blockAudit.transactions[i].txid)) {
|
||||
blockAudit.transactions[i].status = 'added';
|
||||
for (const [index, tx] of blockAudit.transactions.entries()) {
|
||||
if (isAdded[tx.txid]) {
|
||||
tx.status = 'added';
|
||||
} else if (index === 0 || inTemplate[tx.txid]) {
|
||||
tx.status = 'found';
|
||||
} else {
|
||||
blockAudit.transactions[i].status = 'found';
|
||||
tx.status = 'selected';
|
||||
isSelected[tx.txid] = true;
|
||||
this.numUnexpected++;
|
||||
}
|
||||
}
|
||||
for (const tx of blockAudit.transactions) {
|
||||
inBlock[tx.txid] = true;
|
||||
}
|
||||
return blockAudit;
|
||||
}),
|
||||
tap((blockAudit) => {
|
||||
this.changeMode(this.mode);
|
||||
if (this.blockGraphTemplate) {
|
||||
this.blockGraphTemplate.destroy();
|
||||
this.blockGraphTemplate.setup(blockAudit.template);
|
||||
}
|
||||
if (this.blockGraphMined) {
|
||||
this.blockGraphMined.destroy();
|
||||
this.blockGraphMined.setup(blockAudit.transactions);
|
||||
}
|
||||
this.isLoading = false;
|
||||
}),
|
||||
})
|
||||
);
|
||||
}),
|
||||
share()
|
||||
);
|
||||
catchError((err) => {
|
||||
console.log(err);
|
||||
this.error = err;
|
||||
this.isLoading = false;
|
||||
return null;
|
||||
}),
|
||||
).subscribe((blockAudit) => {
|
||||
this.blockAudit = blockAudit;
|
||||
this.setupBlockGraphs();
|
||||
this.isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.childChangeSubscription = combineLatest([this.blockGraphProjected.changes.pipe(startWith(null)), this.blockGraphActual.changes.pipe(startWith(null))]).subscribe(() => {
|
||||
this.setupBlockGraphs();
|
||||
})
|
||||
}
|
||||
|
||||
setupBlockGraphs() {
|
||||
if (this.blockAudit) {
|
||||
this.blockGraphProjected.forEach(graph => {
|
||||
graph.destroy();
|
||||
if (this.isMobile && this.mode === 'actual') {
|
||||
graph.setup(this.blockAudit.transactions);
|
||||
} else {
|
||||
graph.setup(this.blockAudit.template);
|
||||
}
|
||||
})
|
||||
this.blockGraphActual.forEach(graph => {
|
||||
graph.destroy();
|
||||
graph.setup(this.blockAudit.transactions);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onResize(event: any) {
|
||||
this.isMobile = event.target.innerWidth <= 767.98;
|
||||
const isMobile = event.target.innerWidth <= 767.98;
|
||||
const changed = isMobile !== this.isMobile;
|
||||
this.isMobile = isMobile;
|
||||
this.paginationMaxSize = event.target.innerWidth < 670 ? 3 : 5;
|
||||
|
||||
if (changed) {
|
||||
this.changeMode(this.mode);
|
||||
}
|
||||
}
|
||||
|
||||
changeMode(mode: 'missing' | 'added') {
|
||||
changeMode(mode: 'projected' | 'actual') {
|
||||
this.router.navigate([], { fragment: mode });
|
||||
this.mode = mode;
|
||||
}
|
||||
|
||||
onTxClick(event: TransactionStripped): void {
|
||||
const url = new RelativeUrlPipe(this.stateService).transform(`/tx/${event.txid}`);
|
||||
this.router.navigate([url]);
|
||||
}
|
||||
|
||||
pageChange(page: number, target: HTMLElement) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,15 @@ import { feeLevels, mempoolFeeColors } from '../../app.constants';
|
||||
const hoverTransitionTime = 300;
|
||||
const defaultHoverColor = hexToColor('1bd8f4');
|
||||
|
||||
const feeColors = mempoolFeeColors.map(hexToColor);
|
||||
const auditFeeColors = feeColors.map((color) => desaturate(color, 0.3));
|
||||
const auditColors = {
|
||||
censored: hexToColor('f344df'),
|
||||
missing: darken(desaturate(hexToColor('f344df'), 0.3), 0.7),
|
||||
added: hexToColor('03E1E5'),
|
||||
selected: darken(desaturate(hexToColor('039BE5'), 0.3), 0.7),
|
||||
}
|
||||
|
||||
// convert from this class's update format to TxSprite's update format
|
||||
function toSpriteUpdate(params: ViewUpdateParams): SpriteUpdateParams {
|
||||
return {
|
||||
@@ -25,7 +34,7 @@ export default class TxView implements TransactionStripped {
|
||||
vsize: number;
|
||||
value: number;
|
||||
feerate: number;
|
||||
status?: 'found' | 'missing' | 'added';
|
||||
status?: 'found' | 'missing' | 'added' | 'censored' | 'selected';
|
||||
|
||||
initialised: boolean;
|
||||
vertexArray: FastVertexArray;
|
||||
@@ -142,16 +151,23 @@ export default class TxView implements TransactionStripped {
|
||||
}
|
||||
|
||||
getColor(): Color {
|
||||
// Block audit
|
||||
if (this.status === 'missing') {
|
||||
return hexToColor('039BE5');
|
||||
} else if (this.status === 'added') {
|
||||
return hexToColor('D81B60');
|
||||
}
|
||||
|
||||
// Block component
|
||||
const feeLevelIndex = feeLevels.findIndex((feeLvl) => Math.max(1, this.feerate) < feeLvl) - 1;
|
||||
return hexToColor(mempoolFeeColors[feeLevelIndex] || mempoolFeeColors[mempoolFeeColors.length - 1]);
|
||||
const feeLevelColor = feeColors[feeLevelIndex] || feeColors[mempoolFeeColors.length - 1];
|
||||
// Block audit
|
||||
switch(this.status) {
|
||||
case 'censored':
|
||||
return auditColors.censored;
|
||||
case 'missing':
|
||||
return auditColors.missing;
|
||||
case 'added':
|
||||
return auditColors.added;
|
||||
case 'selected':
|
||||
return auditColors.selected;
|
||||
case 'found':
|
||||
return auditFeeColors[feeLevelIndex] || auditFeeColors[mempoolFeeColors.length - 1];
|
||||
default:
|
||||
return feeLevelColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,3 +179,22 @@ function hexToColor(hex: string): Color {
|
||||
a: 1
|
||||
};
|
||||
}
|
||||
|
||||
function desaturate(color: Color, amount: number): Color {
|
||||
const gray = (color.r + color.g + color.b) / 6;
|
||||
return {
|
||||
r: color.r + ((gray - color.r) * amount),
|
||||
g: color.g + ((gray - color.g) * amount),
|
||||
b: color.b + ((gray - color.b) * amount),
|
||||
a: color.a,
|
||||
};
|
||||
}
|
||||
|
||||
function darken(color: Color, amount: number): Color {
|
||||
return {
|
||||
r: color.r * amount,
|
||||
g: color.g * amount,
|
||||
b: color.b * amount,
|
||||
a: color.a,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,16 @@
|
||||
<td class="td-width" i18n="transaction.vsize|Transaction Virtual Size">Virtual size</td>
|
||||
<td [innerHTML]="'‎' + (vsize | vbytes: 2)"></td>
|
||||
</tr>
|
||||
<tr *ngIf="tx && tx.status && tx.status.length">
|
||||
<td class="td-width" i18n="transaction.audit-status">Audit status</td>
|
||||
<ng-container [ngSwitch]="tx?.status">
|
||||
<td *ngSwitchCase="'found'" i18n="transaction.audit.match">match</td>
|
||||
<td *ngSwitchCase="'censored'" i18n="transaction.audit.removed">removed</td>
|
||||
<td *ngSwitchCase="'missing'" i18n="transaction.audit.missing">missing</td>
|
||||
<td *ngSwitchCase="'added'" i18n="transaction.audit.added">added</td>
|
||||
<td *ngSwitchCase="'selected'" i18n="transaction.audit.included">included</td>
|
||||
</ng-container>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -110,6 +110,13 @@
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr *ngIf="indexingAvailable">
|
||||
<td i18n="block.health">Block health</td>
|
||||
<td>
|
||||
<a *ngIf="block.extras?.matchRate != null" [routerLink]="['/block-audit/' | relativeUrl, blockHash]">{{ block.extras.matchRate }}%</a>
|
||||
<span *ngIf="block.extras?.matchRate == null" i18n="unknown">Unknown</span>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -47,6 +47,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
transactionsError: any = null;
|
||||
overviewError: any = null;
|
||||
webGlEnabled = true;
|
||||
indexingAvailable = false;
|
||||
|
||||
transactionSubscription: Subscription;
|
||||
overviewSubscription: Subscription;
|
||||
@@ -86,6 +87,9 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
this.timeLtr = !!ltr;
|
||||
});
|
||||
|
||||
this.indexingAvailable = (this.stateService.env.BASE_MODULE === 'mempool' &&
|
||||
this.stateService.env.MINING_DASHBOARD === true);
|
||||
|
||||
this.txsLoadingStatus$ = this.route.paramMap
|
||||
.pipe(
|
||||
switchMap(() => this.stateService.loadingIndicators$),
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
left: 0;
|
||||
top: 75px;
|
||||
transform: translateX(50vw);
|
||||
transition: transform 1s;
|
||||
}
|
||||
|
||||
.position-container.liquid, .position-container.liquidtestnet {
|
||||
@@ -84,9 +83,9 @@
|
||||
|
||||
.time-toggle {
|
||||
color: white;
|
||||
font-size: 1rem;
|
||||
font-size: 0.8rem;
|
||||
position: absolute;
|
||||
bottom: -1.5em;
|
||||
bottom: -1.8em;
|
||||
left: 1px;
|
||||
transform: translateX(-50%);
|
||||
background: none;
|
||||
@@ -97,14 +96,31 @@
|
||||
}
|
||||
|
||||
.blockchain-wrapper.ltr-transition .blocks-wrapper,
|
||||
.blockchain-wrapper.ltr-transition .position-container,
|
||||
.blockchain-wrapper.ltr-transition .time-toggle {
|
||||
transition: transform 1s;
|
||||
}
|
||||
|
||||
.blockchain-wrapper.time-ltr .blocks-wrapper {
|
||||
transform: scaleX(-1);
|
||||
.blockchain-wrapper.time-ltr {
|
||||
.blocks-wrapper {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
.time-toggle {
|
||||
transform: translateX(-50%) scaleX(-1);
|
||||
}
|
||||
}
|
||||
|
||||
.blockchain-wrapper.time-ltr .time-toggle {
|
||||
transform: translateX(-50%) scaleX(-1);
|
||||
:host-context(.ltr-layout) {
|
||||
.blockchain-wrapper.time-ltr .blocks-wrapper,
|
||||
.blockchain-wrapper .blocks-wrapper {
|
||||
direction: ltr;
|
||||
}
|
||||
}
|
||||
|
||||
:host-context(.rtl-layout) {
|
||||
.blockchain-wrapper.time-ltr .blocks-wrapper,
|
||||
.blockchain-wrapper .blocks-wrapper {
|
||||
direction: rtl;
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,8 @@
|
||||
i18n-ngbTooltip="mining.pool-name" ngbTooltip="Pool" placement="bottom" #miningpool [disableTooltip]="!isEllipsisActive(miningpool)">Pool</th>
|
||||
<th class="timestamp" i18n="latest-blocks.timestamp" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'">Timestamp</th>
|
||||
<th class="mined" i18n="latest-blocks.mined" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'">Mined</th>
|
||||
<th *ngIf="indexingAvailable" class="health text-left" i18n="latest-blocks.health" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}"
|
||||
i18n-ngbTooltip="latest-blocks.health" ngbTooltip="Health" placement="bottom" #health [disableTooltip]="!isEllipsisActive(health)">Health</th>
|
||||
<th *ngIf="indexingAvailable" class="reward text-right" i18n="latest-blocks.reward" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}"
|
||||
i18n-ngbTooltip="latest-blocks.reward" ngbTooltip="Reward" placement="bottom" #reward [disableTooltip]="!isEllipsisActive(reward)">Reward</th>
|
||||
<th *ngIf="indexingAvailable && !widget" class="fees text-right" i18n="latest-blocks.fees" [class]="indexingAvailable ? '' : 'legacy'">Fees</th>
|
||||
@@ -37,12 +39,30 @@
|
||||
<span *ngIf="!widget" class="tooltiptext badge badge-secondary scriptmessage">{{ block.extras.coinbaseRaw | hex2ascii }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="timestamp" *ngIf="!widget">
|
||||
<td class="timestamp" *ngIf="!widget" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
|
||||
‎{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}
|
||||
</td>
|
||||
<td class="mined" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'">
|
||||
<app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since>
|
||||
</td>
|
||||
<td *ngIf="indexingAvailable" class="health text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
|
||||
<a *ngIf="block.extras?.matchRate != null" class="clear-link" [routerLink]="['/block-audit/' | relativeUrl, block.id]">
|
||||
<div class="progress progress-health">
|
||||
<div class="progress-bar progress-bar-health" role="progressbar"
|
||||
[ngStyle]="{'width': (100 - (block.extras?.matchRate || 0)) + '%' }"></div>
|
||||
<div class="progress-text">
|
||||
<span>{{ block.extras.matchRate }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<div *ngIf="block.extras?.matchRate == null" class="progress progress-health">
|
||||
<div class="progress-bar progress-bar-health" role="progressbar"
|
||||
[ngStyle]="{'width': '100%' }"></div>
|
||||
<div class="progress-text">
|
||||
<span>~</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td *ngIf="indexingAvailable" class="reward text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
|
||||
<app-amount [satoshis]="block.extras.reward" [noFiat]="true" digitsInfo="1.2-2"></app-amount>
|
||||
</td>
|
||||
@@ -77,6 +97,9 @@
|
||||
<td class="mined" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'">
|
||||
<span class="skeleton-loader" style="max-width: 125px"></span>
|
||||
</td>
|
||||
<td *ngIf="indexingAvailable" class="health text-left" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
|
||||
<span class="skeleton-loader" style="max-width: 75px"></span>
|
||||
</td>
|
||||
<td *ngIf="indexingAvailable" class="reward text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
|
||||
<span class="skeleton-loader" style="max-width: 75px"></span>
|
||||
</td>
|
||||
|
||||
@@ -63,7 +63,7 @@ tr, td, th {
|
||||
}
|
||||
|
||||
.height {
|
||||
width: 10%;
|
||||
width: 8%;
|
||||
}
|
||||
.height.widget {
|
||||
width: 15%;
|
||||
@@ -77,12 +77,18 @@ tr, td, th {
|
||||
|
||||
.timestamp {
|
||||
width: 18%;
|
||||
@media (max-width: 900px) {
|
||||
@media (max-width: 1100px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.timestamp.legacy {
|
||||
width: 20%;
|
||||
@media (max-width: 1100px) {
|
||||
display: table-cell;
|
||||
}
|
||||
@media (max-width: 850px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.mined {
|
||||
@@ -93,6 +99,10 @@ tr, td, th {
|
||||
}
|
||||
.mined.legacy {
|
||||
width: 15%;
|
||||
@media (max-width: 1000px) {
|
||||
padding-right: 20px;
|
||||
width: 20%;
|
||||
}
|
||||
@media (max-width: 576px) {
|
||||
display: table-cell;
|
||||
}
|
||||
@@ -100,6 +110,7 @@ tr, td, th {
|
||||
|
||||
.txs {
|
||||
padding-right: 40px;
|
||||
width: 8%;
|
||||
@media (max-width: 1100px) {
|
||||
padding-right: 10px;
|
||||
}
|
||||
@@ -113,17 +124,21 @@ tr, td, th {
|
||||
}
|
||||
.txs.widget {
|
||||
padding-right: 0;
|
||||
display: none;
|
||||
@media (max-width: 650px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.txs.legacy {
|
||||
padding-right: 80px;
|
||||
width: 10%;
|
||||
width: 18%;
|
||||
display: table-cell;
|
||||
@media (max-width: 1000px) {
|
||||
padding-right: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.fees {
|
||||
width: 10%;
|
||||
width: 8%;
|
||||
@media (max-width: 650px) {
|
||||
display: none;
|
||||
}
|
||||
@@ -133,7 +148,7 @@ tr, td, th {
|
||||
}
|
||||
|
||||
.reward {
|
||||
width: 10%;
|
||||
width: 8%;
|
||||
@media (max-width: 576px) {
|
||||
width: 7%;
|
||||
padding-right: 30px;
|
||||
@@ -152,8 +167,11 @@ tr, td, th {
|
||||
}
|
||||
|
||||
.size {
|
||||
width: 12%;
|
||||
width: 10%;
|
||||
@media (max-width: 1000px) {
|
||||
width: 13%;
|
||||
}
|
||||
@media (max-width: 950px) {
|
||||
width: 15%;
|
||||
}
|
||||
@media (max-width: 650px) {
|
||||
@@ -164,12 +182,34 @@ tr, td, th {
|
||||
}
|
||||
}
|
||||
.size.legacy {
|
||||
width: 20%;
|
||||
width: 30%;
|
||||
@media (max-width: 576px) {
|
||||
display: table-cell;
|
||||
}
|
||||
}
|
||||
|
||||
.health {
|
||||
width: 10%;
|
||||
@media (max-width: 1000px) {
|
||||
width: 13%;
|
||||
}
|
||||
@media (max-width: 950px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.health.widget {
|
||||
width: 25%;
|
||||
@media (max-width: 1000px) {
|
||||
display: none;
|
||||
}
|
||||
@media (max-width: 767px) {
|
||||
display: table-cell;
|
||||
}
|
||||
@media (max-width: 500px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Tooltip text */
|
||||
.tooltip-custom {
|
||||
position: relative;
|
||||
|
||||
@@ -31,17 +31,17 @@
|
||||
<button class="btn btn-primary w-100" id="dropdownBasic1" ngbDropdownToggle i18n="lightning">Lightning</button>
|
||||
<div ngbDropdownMenu aria-labelledby="dropdownBasic1">
|
||||
<a class="dropdown-item" routerLinkActive="active" [routerLink]="['/graphs/lightning/nodes-networks' | relativeUrl]"
|
||||
i18n="lightning.nodes-networks">Lightning nodes per network</a>
|
||||
i18n="lightning.nodes-networks">Lightning Nodes Per Network</a>
|
||||
<a class="dropdown-item" routerLinkActive="active" [routerLink]="['/graphs/lightning/capacity' | relativeUrl]"
|
||||
i18n="lightning.capacity">Network capacity</a>
|
||||
i18n="lightning.network-capacity">Lightning Network Capacity</a>
|
||||
<a class="dropdown-item" routerLinkActive="active" [routerLink]="['/graphs/lightning/nodes-per-isp' | relativeUrl]"
|
||||
i18n="lightning.nodes-per-isp">Lightning nodes per ISP</a>
|
||||
i18n="lightning.nodes-per-isp">Lightning Nodes Per ISP</a>
|
||||
<a class="dropdown-item" routerLinkActive="active" [routerLink]="['/graphs/lightning/nodes-per-country' | relativeUrl]"
|
||||
i18n="lightning.nodes-per-country">Lightning nodes per country</a>
|
||||
i18n="lightning.nodes-per-country">Lightning Nodes Per Country</a>
|
||||
<a class="dropdown-item" routerLinkActive="active" [routerLink]="['/graphs/lightning/nodes-map' | relativeUrl]"
|
||||
i18n="lightning.lightning.nodes-heatmap">Lightning nodes world map</a>
|
||||
i18n="lightning.lightning.nodes-heatmap">Lightning Nodes World Map</a>
|
||||
<a class="dropdown-item" routerLinkActive="active" [routerLink]="['/graphs/lightning/nodes-channels-map' | relativeUrl]"
|
||||
i18n="lightning.nodes-channels-world-map">Lightning nodes channels world map</a>
|
||||
i18n="lightning.nodes-channels-world-map">Lightning Nodes Channels World Map</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -49,13 +49,13 @@
|
||||
<app-svg-images [name]="network.val === '' ? 'liquid' : network.val" width="22" height="22" viewBox="0 0 125 125" style="width: 30px; height: 30px; margin-right: 5px;"></app-svg-images>
|
||||
</button>
|
||||
<div ngbDropdownMenu [ngClass]="{'dropdown-menu-right' : isMobile}">
|
||||
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage" ngbDropdownItem class="mainnet"><app-svg-images name="bitcoin" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Mainnet</a>
|
||||
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + '/signet'" ngbDropdownItem *ngIf="env.SIGNET_ENABLED" class="signet"><app-svg-images name="signet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Signet</a>
|
||||
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + '/testnet'" ngbDropdownItem *ngIf="env.TESTNET_ENABLED" class="testnet"><app-svg-images name="testnet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Testnet</a>
|
||||
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['mainnet'] || '')" ngbDropdownItem class="mainnet"><app-svg-images name="bitcoin" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Mainnet</a>
|
||||
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['signet'] || '/signet')" ngbDropdownItem *ngIf="env.SIGNET_ENABLED" class="signet"><app-svg-images name="signet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Signet</a>
|
||||
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['testnet'] || '/testnet')" ngbDropdownItem *ngIf="env.TESTNET_ENABLED" class="testnet"><app-svg-images name="testnet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Testnet</a>
|
||||
<h6 class="dropdown-header" i18n="master-page.layer2-networks-header">Layer 2 Networks</h6>
|
||||
<a [href]="env.BISQ_WEBSITE_URL + urlLanguage" ngbDropdownItem class="mainnet"><app-svg-images name="bisq" width="22" height="22" viewBox="0 0 75 75" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Bisq</a>
|
||||
<a ngbDropdownItem class="liquid mr-1" [class.active]="network.val === 'liquid'" routerLink="/"><app-svg-images name="liquid" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid</a>
|
||||
<a ngbDropdownItem *ngIf="env.LIQUID_TESTNET_ENABLED" class="liquidtestnet" [class.active]="network.val === 'liquidtestnet'" routerLink="/testnet"><app-svg-images name="liquidtestnet" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid Testnet</a>
|
||||
<a [href]="env.BISQ_WEBSITE_URL + urlLanguage + (networkPaths['bisq'] || '')" ngbDropdownItem class="mainnet"><app-svg-images name="bisq" width="22" height="22" viewBox="0 0 75 75" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Bisq</a>
|
||||
<a ngbDropdownItem class="liquid mr-1" [class.active]="network.val === 'liquid'" [routerLink]="networkPaths['liquid'] || '/'"><app-svg-images name="liquid" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid</a>
|
||||
<a ngbDropdownItem *ngIf="env.LIQUID_TESTNET_ENABLED" class="liquidtestnet" [class.active]="network.val === 'liquidtestnet'" [routerLink]="networkPaths['liquidtestnet'] || '/testnet'"><app-svg-images name="liquidtestnet" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid Testnet</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Env, StateService } from '../../services/state.service';
|
||||
import { merge, Observable, of} from 'rxjs';
|
||||
import { LanguageService } from '../../services/language.service';
|
||||
import { EnterpriseService } from '../../services/enterprise.service';
|
||||
import { NavigationService } from '../../services/navigation.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-liquid-master-page',
|
||||
@@ -17,11 +18,13 @@ export class LiquidMasterPageComponent implements OnInit {
|
||||
officialMempoolSpace = this.stateService.env.OFFICIAL_MEMPOOL_SPACE;
|
||||
network$: Observable<string>;
|
||||
urlLanguage: string;
|
||||
networkPaths: { [network: string]: string };
|
||||
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
private languageService: LanguageService,
|
||||
private enterpriseService: EnterpriseService,
|
||||
private navigationService: NavigationService,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
@@ -29,6 +32,10 @@ export class LiquidMasterPageComponent implements OnInit {
|
||||
this.connectionState$ = this.stateService.connectionState$;
|
||||
this.network$ = merge(of(''), this.stateService.networkChanged$);
|
||||
this.urlLanguage = this.languageService.getLanguageForUrl();
|
||||
this.navigationService.subnetPaths.subscribe((paths) => {
|
||||
console.log('network paths updated...');
|
||||
this.networkPaths = paths;
|
||||
});
|
||||
}
|
||||
|
||||
collapse(): void {
|
||||
|
||||
@@ -22,13 +22,13 @@
|
||||
<app-svg-images [name]="network.val === '' ? 'bitcoin' : network.val" width="20" height="20" viewBox="0 0 65 65" style="width: 30px; height: 30px; margin-right: 5px;"></app-svg-images>
|
||||
</button>
|
||||
<div ngbDropdownMenu [ngClass]="{'dropdown-menu-right' : isMobile}">
|
||||
<a ngbDropdownItem class="mainnet" routerLink="/"><app-svg-images name="bitcoin" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Mainnet</a>
|
||||
<a ngbDropdownItem *ngIf="env.SIGNET_ENABLED" class="signet" [class.active]="network.val === 'signet'" routerLink="/signet"><app-svg-images name="signet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Signet</a>
|
||||
<a ngbDropdownItem *ngIf="env.TESTNET_ENABLED" class="testnet" [class.active]="network.val === 'testnet'" routerLink="/testnet"><app-svg-images name="testnet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Testnet</a>
|
||||
<a ngbDropdownItem class="mainnet" [routerLink]="networkPaths['mainnet'] || '/'"><app-svg-images name="bitcoin" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Mainnet</a>
|
||||
<a ngbDropdownItem *ngIf="env.SIGNET_ENABLED" class="signet" [class.active]="network.val === 'signet'" [routerLink]="networkPaths['signet'] || '/signet'"><app-svg-images name="signet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Signet</a>
|
||||
<a ngbDropdownItem *ngIf="env.TESTNET_ENABLED" class="testnet" [class.active]="network.val === 'testnet'" [routerLink]="networkPaths['testnet'] || '/testnet'"><app-svg-images name="testnet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Testnet</a>
|
||||
<h6 *ngIf="env.LIQUID_ENABLED || env.BISQ_ENABLED" class="dropdown-header" i18n="master-page.layer2-networks-header">Layer 2 Networks</h6>
|
||||
<a [href]="env.BISQ_WEBSITE_URL + urlLanguage" ngbDropdownItem *ngIf="env.BISQ_ENABLED" class="bisq"><app-svg-images name="bisq" width="20" height="20" viewBox="0 0 75 75" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Bisq</a>
|
||||
<a [href]="env.LIQUID_WEBSITE_URL + urlLanguage" ngbDropdownItem *ngIf="env.LIQUID_ENABLED" class="liquid" [class.active]="network.val === 'liquid'"><app-svg-images name="liquid" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid</a>
|
||||
<a [href]="env.LIQUID_WEBSITE_URL + urlLanguage + '/testnet'" ngbDropdownItem *ngIf="env.LIQUID_TESTNET_ENABLED" class="liquidtestnet" [class.active]="network.val === 'liquid'"><app-svg-images name="liquidtestnet" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid Testnet</a>
|
||||
<a [href]="env.BISQ_WEBSITE_URL + urlLanguage + (networkPaths['bisq'] || '')" ngbDropdownItem *ngIf="env.BISQ_ENABLED" class="bisq"><app-svg-images name="bisq" width="20" height="20" viewBox="0 0 75 75" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Bisq</a>
|
||||
<a [href]="env.LIQUID_WEBSITE_URL + urlLanguage + (networkPaths['liquid'] || '')" ngbDropdownItem *ngIf="env.LIQUID_ENABLED" class="liquid" [class.active]="network.val === 'liquid'"><app-svg-images name="liquid" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid</a>
|
||||
<a [href]="env.LIQUID_WEBSITE_URL + urlLanguage + (networkPaths['liquidtestnet'] || '/testnet')" ngbDropdownItem *ngIf="env.LIQUID_TESTNET_ENABLED" class="liquidtestnet" [class.active]="network.val === 'liquid'"><app-svg-images name="liquidtestnet" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid Testnet</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ 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';
|
||||
|
||||
@Component({
|
||||
selector: 'app-master-page',
|
||||
@@ -18,11 +19,13 @@ export class MasterPageComponent implements OnInit {
|
||||
officialMempoolSpace = this.stateService.env.OFFICIAL_MEMPOOL_SPACE;
|
||||
urlLanguage: string;
|
||||
subdomain = '';
|
||||
networkPaths: { [network: string]: string };
|
||||
|
||||
constructor(
|
||||
public stateService: StateService,
|
||||
private languageService: LanguageService,
|
||||
private enterpriseService: EnterpriseService,
|
||||
private navigationService: NavigationService,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
@@ -31,6 +34,10 @@ export class MasterPageComponent implements OnInit {
|
||||
this.network$ = merge(of(''), this.stateService.networkChanged$);
|
||||
this.urlLanguage = this.languageService.getLanguageForUrl();
|
||||
this.subdomain = this.enterpriseService.getSubdomain();
|
||||
this.navigationService.subnetPaths.subscribe((paths) => {
|
||||
console.log('network paths updated...');
|
||||
this.networkPaths = paths;
|
||||
});
|
||||
}
|
||||
|
||||
collapse(): void {
|
||||
|
||||
@@ -146,4 +146,10 @@
|
||||
.block-body {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
}
|
||||
|
||||
:host-context(.rtl-layout) {
|
||||
#arrow-up {
|
||||
transform: translateX(70px);
|
||||
}
|
||||
}
|
||||
@@ -287,11 +287,12 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
|
||||
|
||||
this.arrowVisible = true;
|
||||
|
||||
for (const block of this.mempoolBlocks) {
|
||||
for (let i = 0; i < block.feeRange.length - 1; i++) {
|
||||
let found = false;
|
||||
for (let txInBlockIndex = 0; txInBlockIndex < this.mempoolBlocks.length && !found; txInBlockIndex++) {
|
||||
const block = this.mempoolBlocks[txInBlockIndex];
|
||||
for (let i = 0; i < block.feeRange.length - 1 && !found; i++) {
|
||||
if (this.txFeePerVSize < block.feeRange[i + 1] && this.txFeePerVSize >= block.feeRange[i]) {
|
||||
const txInBlockIndex = this.mempoolBlocks.indexOf(block);
|
||||
const feeRangeIndex = block.feeRange.findIndex((val, index) => this.txFeePerVSize < block.feeRange[index + 1]);
|
||||
const feeRangeIndex = i;
|
||||
const feeRangeChunkSize = 1 / (block.feeRange.length - 1);
|
||||
|
||||
const txFee = this.txFeePerVSize - block.feeRange[i];
|
||||
@@ -306,9 +307,13 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
|
||||
+ ((1 - feePosition) * blockedFilledPercentage * this.blockWidth);
|
||||
|
||||
this.rightPosition = arrowRightPosition;
|
||||
break;
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
if (this.txFeePerVSize >= block.feeRange[block.feeRange.length - 1]) {
|
||||
this.rightPosition = txInBlockIndex * (this.blockWidth + this.blockPadding);
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
<div class="d-flex">
|
||||
<div class="search-box-container mr-2">
|
||||
<input (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">
|
||||
|
||||
<app-search-results #searchResults [results]="typeAhead$ | async" (selectedResult)="selectedResult($event)"></app-search-results>
|
||||
|
||||
<app-search-results #searchResults [hidden]="dropdownHidden" [results]="typeAhead$ | async" (selectedResult)="selectedResult($event)"></app-search-results>
|
||||
</div>
|
||||
<div>
|
||||
<button [disabled]="isSearching" type="submit" class="btn btn-block btn-primary">
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Component, OnInit, ChangeDetectionStrategy, EventEmitter, Output, ViewChild, HostListener } from '@angular/core';
|
||||
import { Component, OnInit, ChangeDetectionStrategy, EventEmitter, Output, ViewChild, HostListener, ElementRef } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { AssetsService } from '../../services/assets.service';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { Observable, of, Subject, zip, BehaviorSubject } from 'rxjs';
|
||||
import { debounceTime, distinctUntilChanged, switchMap, catchError, map } from 'rxjs/operators';
|
||||
import { Observable, of, Subject, zip, BehaviorSubject, combineLatest } from 'rxjs';
|
||||
import { debounceTime, distinctUntilChanged, switchMap, catchError, map, startWith, tap } from 'rxjs/operators';
|
||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
||||
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
@@ -23,8 +23,18 @@ export class SearchFormComponent implements OnInit {
|
||||
isTypeaheading$ = new BehaviorSubject<boolean>(false);
|
||||
typeAhead$: Observable<any>;
|
||||
searchForm: FormGroup;
|
||||
dropdownHidden = false;
|
||||
|
||||
regexAddress = /^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[a-z]{2,5}1[ac-hj-np-z02-9]{8,100}|[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100})$/;
|
||||
@HostListener('document:click', ['$event'])
|
||||
onDocumentClick(event) {
|
||||
if (this.elementRef.nativeElement.contains(event.target)) {
|
||||
this.dropdownHidden = false;
|
||||
} else {
|
||||
this.dropdownHidden = true;
|
||||
}
|
||||
}
|
||||
|
||||
regexAddress = /^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[A-z]{2,5}1[a-zA-HJ-NP-Z0-9]{39,59})$/;
|
||||
regexBlockhash = /^[0]{8}[a-fA-F0-9]{56}$/;
|
||||
regexTransaction = /^([a-fA-F0-9]{64})(:\d+)?$/;
|
||||
regexBlockheight = /^[0-9]{1,9}$/;
|
||||
@@ -33,7 +43,7 @@ export class SearchFormComponent implements OnInit {
|
||||
|
||||
@Output() searchTriggered = new EventEmitter();
|
||||
@ViewChild('searchResults') searchResults: SearchResultsComponent;
|
||||
@HostListener('keydown', ['$event']) keydown($event) {
|
||||
@HostListener('keydown', ['$event']) keydown($event): void {
|
||||
this.handleKeyDown($event);
|
||||
}
|
||||
|
||||
@@ -45,9 +55,10 @@ export class SearchFormComponent implements OnInit {
|
||||
private electrsApiService: ElectrsApiService,
|
||||
private apiService: ApiService,
|
||||
private relativeUrlPipe: RelativeUrlPipe,
|
||||
private elementRef: ElementRef,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
ngOnInit(): void {
|
||||
this.stateService.networkChanged$.subscribe((network) => this.network = network);
|
||||
|
||||
this.searchForm = this.formBuilder.group({
|
||||
@@ -61,70 +72,111 @@ export class SearchFormComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
this.typeAhead$ = this.searchForm.get('searchText').valueChanges
|
||||
.pipe(
|
||||
map((text) => {
|
||||
if (this.network === 'bisq' && text.match(/^(b)[^c]/i)) {
|
||||
return text.substr(1);
|
||||
}
|
||||
return text.trim();
|
||||
}),
|
||||
debounceTime(200),
|
||||
distinctUntilChanged(),
|
||||
switchMap((text) => {
|
||||
if (!text.length) {
|
||||
return of([
|
||||
'',
|
||||
[],
|
||||
{
|
||||
nodes: [],
|
||||
channels: [],
|
||||
}
|
||||
]);
|
||||
}
|
||||
this.isTypeaheading$.next(true);
|
||||
if (!this.stateService.env.LIGHTNING) {
|
||||
return zip(
|
||||
of(text),
|
||||
this.electrsApiService.getAddressesByPrefix$(text).pipe(catchError(() => of([]))),
|
||||
[{ nodes: [], channels: [] }],
|
||||
of(this.regexBlockheight.test(text)),
|
||||
);
|
||||
}
|
||||
const searchText$ = this.searchForm.get('searchText').valueChanges
|
||||
.pipe(
|
||||
map((text) => {
|
||||
if (this.network === 'bisq' && text.match(/^(b)[^c]/i)) {
|
||||
return text.substr(1);
|
||||
}
|
||||
return text.trim();
|
||||
}),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
|
||||
const searchResults$ = searchText$.pipe(
|
||||
debounceTime(200),
|
||||
switchMap((text) => {
|
||||
if (!text.length) {
|
||||
return of([
|
||||
[],
|
||||
{ nodes: [], channels: [] }
|
||||
]);
|
||||
}
|
||||
this.isTypeaheading$.next(true);
|
||||
if (!this.stateService.env.LIGHTNING) {
|
||||
return zip(
|
||||
of(text),
|
||||
this.electrsApiService.getAddressesByPrefix$(text).pipe(catchError(() => of([]))),
|
||||
this.apiService.lightningSearch$(text).pipe(catchError(() => of({
|
||||
[{ nodes: [], channels: [] }],
|
||||
);
|
||||
}
|
||||
return zip(
|
||||
this.electrsApiService.getAddressesByPrefix$(text).pipe(catchError(() => of([]))),
|
||||
this.apiService.lightningSearch$(text).pipe(catchError(() => of({
|
||||
nodes: [],
|
||||
channels: [],
|
||||
}))),
|
||||
);
|
||||
}),
|
||||
tap((result: any[]) => {
|
||||
this.isTypeaheading$.next(false);
|
||||
})
|
||||
);
|
||||
|
||||
this.typeAhead$ = combineLatest(
|
||||
[
|
||||
searchText$,
|
||||
searchResults$.pipe(
|
||||
startWith([
|
||||
[],
|
||||
{
|
||||
nodes: [],
|
||||
channels: [],
|
||||
}
|
||||
]))
|
||||
]
|
||||
).pipe(
|
||||
map((latestData) => {
|
||||
const searchText = latestData[0];
|
||||
if (!searchText.length) {
|
||||
return {
|
||||
searchText: '',
|
||||
hashQuickMatch: false,
|
||||
blockHeight: false,
|
||||
txId: false,
|
||||
address: false,
|
||||
addresses: [],
|
||||
nodes: [],
|
||||
channels: [],
|
||||
}))),
|
||||
);
|
||||
}),
|
||||
map((result: any[]) => {
|
||||
this.isTypeaheading$.next(false);
|
||||
if (this.network === 'bisq') {
|
||||
return result[0].map((address: string) => 'B' + address);
|
||||
};
|
||||
}
|
||||
|
||||
const result = latestData[1];
|
||||
const addressPrefixSearchResults = result[0];
|
||||
const lightningResults = result[1];
|
||||
|
||||
if (this.network === 'bisq') {
|
||||
return searchText.map((address: string) => 'B' + address);
|
||||
}
|
||||
|
||||
const matchesBlockHeight = this.regexBlockheight.test(searchText);
|
||||
const matchesTxId = this.regexTransaction.test(searchText) && !this.regexBlockhash.test(searchText);
|
||||
const matchesBlockHash = this.regexBlockhash.test(searchText);
|
||||
const matchesAddress = this.regexAddress.test(searchText);
|
||||
|
||||
return {
|
||||
searchText: result[0],
|
||||
blockHeight: this.regexBlockheight.test(result[0]) ? [parseInt(result[0], 10)] : [],
|
||||
addresses: result[1],
|
||||
nodes: result[2].nodes,
|
||||
channels: result[2].channels,
|
||||
totalResults: result[1].length + result[2].nodes.length + result[2].channels.length,
|
||||
searchText: searchText,
|
||||
hashQuickMatch: +(matchesBlockHeight || matchesBlockHash || matchesTxId || matchesAddress),
|
||||
blockHeight: matchesBlockHeight,
|
||||
txId: matchesTxId,
|
||||
blockHash: matchesBlockHash,
|
||||
address: matchesAddress,
|
||||
addresses: addressPrefixSearchResults,
|
||||
nodes: lightningResults.nodes,
|
||||
channels: lightningResults.channels,
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
handleKeyDown($event) {
|
||||
|
||||
handleKeyDown($event): void {
|
||||
this.searchResults.handleKeyDown($event);
|
||||
}
|
||||
|
||||
itemSelected() {
|
||||
itemSelected(): void {
|
||||
setTimeout(() => this.search());
|
||||
}
|
||||
|
||||
selectedResult(result: any) {
|
||||
selectedResult(result: any): void {
|
||||
if (typeof result === 'string') {
|
||||
this.search(result);
|
||||
} else if (typeof result === 'number') {
|
||||
@@ -136,7 +188,7 @@ export class SearchFormComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
search(result?: string) {
|
||||
search(result?: string): void {
|
||||
const searchText = result || this.searchForm.value.searchText.trim();
|
||||
if (searchText) {
|
||||
this.isSearching = true;
|
||||
@@ -170,7 +222,7 @@ export class SearchFormComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
navigate(url: string, searchText: string, extras?: any) {
|
||||
navigate(url: string, searchText: string, extras?: any): void {
|
||||
this.router.navigate([this.relativeUrlPipe.transform(url), searchText], extras);
|
||||
this.searchTriggered.emit();
|
||||
this.searchForm.setValue({
|
||||
|
||||
@@ -1,14 +1,32 @@
|
||||
<div class="dropdown-menu show" *ngIf="results" [hidden]="!results.blockHeight.length && !results.addresses.length && !results.nodes.length && !results.channels.length">
|
||||
<ng-template [ngIf]="results.blockHeight.length">
|
||||
<div class="dropdown-menu show" *ngIf="results" [hidden]="!results.hashQuickMatch && !results.addresses.length && !results.nodes.length && !results.channels.length">
|
||||
<ng-template [ngIf]="results.blockHeight">
|
||||
<div class="card-title">Bitcoin Block Height</div>
|
||||
<button (click)="clickItem(0)" [class.active]="0 === activeIdx" type="button" role="option" class="dropdown-item">
|
||||
Go to "{{ results.searchText }}"
|
||||
</button>
|
||||
</ng-template>
|
||||
<ng-template [ngIf]="results.txId">
|
||||
<div class="card-title">Bitcoin Transaction</div>
|
||||
<button (click)="clickItem(0)" [class.active]="0 === activeIdx" type="button" role="option" class="dropdown-item">
|
||||
Go to "{{ results.searchText | shortenString : 13 }}"
|
||||
</button>
|
||||
</ng-template>
|
||||
<ng-template [ngIf]="results.address">
|
||||
<div class="card-title">Bitcoin Address</div>
|
||||
<button (click)="clickItem(0)" [class.active]="0 === activeIdx" type="button" role="option" class="dropdown-item">
|
||||
Go to "{{ results.searchText | shortenString : isMobile ? 20 : 30 }}"
|
||||
</button>
|
||||
</ng-template>
|
||||
<ng-template [ngIf]="results.blockHash">
|
||||
<div class="card-title">Bitcoin Block</div>
|
||||
<button (click)="clickItem(0)" [class.active]="0 === activeIdx" type="button" role="option" class="dropdown-item">
|
||||
Go to "{{ results.searchText | shortenString : 13 }}"
|
||||
</button>
|
||||
</ng-template>
|
||||
<ng-template [ngIf]="results.addresses.length">
|
||||
<div class="card-title" *ngIf="stateService.env.LIGHTNING">Bitcoin Addresses</div>
|
||||
<div class="card-title">Bitcoin Addresses</div>
|
||||
<ng-template ngFor [ngForOf]="results.addresses" let-address let-i="index">
|
||||
<button (click)="clickItem(results.blockHeight.length + i)" [class.active]="(results.blockHeight.length + i) === activeIdx" type="button" role="option" class="dropdown-item">
|
||||
<button (click)="clickItem(results.hashQuickMatch + i)" [class.active]="(results.hashQuickMatch + i) === activeIdx" type="button" role="option" class="dropdown-item">
|
||||
<ngb-highlight [result]="address | shortenString : isMobile ? 25 : 36" [term]="results.searchText"></ngb-highlight>
|
||||
</button>
|
||||
</ng-template>
|
||||
@@ -16,7 +34,7 @@
|
||||
<ng-template [ngIf]="results.nodes.length">
|
||||
<div class="card-title">Lightning Nodes</div>
|
||||
<ng-template ngFor [ngForOf]="results.nodes" let-node let-i="index">
|
||||
<button (click)="clickItem(results.blockHeight.length + results.addresses.length + i)" [class.inactive]="node.status === 0" [class.active]="results.blockHeight.length + results.addresses.length + i === activeIdx" [routerLink]="['/lightning/node' | relativeUrl, node.public_key]" type="button" role="option" class="dropdown-item">
|
||||
<button (click)="clickItem(results.hashQuickMatch + results.addresses.length + i)" [class.inactive]="node.status === 0" [class.active]="results.hashQuickMatch + results.addresses.length + i === activeIdx" [routerLink]="['/lightning/node' | relativeUrl, node.public_key]" type="button" role="option" class="dropdown-item">
|
||||
<ngb-highlight [result]="node.alias" [term]="results.searchText"></ngb-highlight> <span class="symbol">{{ node.public_key | shortenString : 10 }}</span>
|
||||
</button>
|
||||
</ng-template>
|
||||
@@ -24,7 +42,7 @@
|
||||
<ng-template [ngIf]="results.channels.length">
|
||||
<div class="card-title">Lightning Channels</div>
|
||||
<ng-template ngFor [ngForOf]="results.channels" let-channel let-i="index">
|
||||
<button (click)="clickItem(results.blockHeight.length + results.addresses.length + results.nodes.length + i)" [class.inactive]="channel.status === 2" [class.active]="results.blockHeight.length + results.addresses.length + results.nodes.length + i === activeIdx" type="button" role="option" class="dropdown-item">
|
||||
<button (click)="clickItem(results.hashQuickMatch + results.addresses.length + results.nodes.length + i)" [class.inactive]="channel.status === 2" [class.active]="results.hashQuickMatch + results.addresses.length + results.nodes.length + i === activeIdx" type="button" role="option" class="dropdown-item">
|
||||
<ngb-highlight [result]="channel.short_id" [term]="results.searchText"></ngb-highlight> <span class="symbol">{{ channel.id }}</span>
|
||||
</button>
|
||||
</ng-template>
|
||||
|
||||
@@ -22,7 +22,7 @@ export class SearchResultsComponent implements OnChanges {
|
||||
ngOnChanges() {
|
||||
this.activeIdx = 0;
|
||||
if (this.results) {
|
||||
this.resultsFlattened = [...this.results.blockHeight, ...this.results.addresses, ...this.results.nodes, ...this.results.channels];
|
||||
this.resultsFlattened = [...(this.results.hashQuickMatch ? [this.results.searchText] : []), ...this.results.addresses, ...this.results.nodes, ...this.results.channels];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
<div *ngIf="countdown > 0" class="warning-label">{{ eventName }} in {{ countdown | number }} block{{ countdown === 1 ? '' : 's' }}!</div>
|
||||
|
||||
<div id="blockchain-container" dir="ltr" #blockchainContainer
|
||||
<div id="blockchain-container" [dir]="timeLtr ? 'rtl' : 'ltr'" #blockchainContainer
|
||||
(mousedown)="onMouseDown($event)"
|
||||
(dragstart)="onDragStart($event)"
|
||||
>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Component, ElementRef, HostListener, OnInit, ViewChild } from '@angular/core';
|
||||
import { Component, ElementRef, HostListener, OnInit, OnDestroy, ViewChild } from '@angular/core';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { specialBlocks } from '../../app.constants';
|
||||
|
||||
@@ -7,7 +8,7 @@ import { specialBlocks } from '../../app.constants';
|
||||
templateUrl: './start.component.html',
|
||||
styleUrls: ['./start.component.scss'],
|
||||
})
|
||||
export class StartComponent implements OnInit {
|
||||
export class StartComponent implements OnInit, OnDestroy {
|
||||
interval = 60;
|
||||
colors = ['#5E35B1', '#ffffff'];
|
||||
|
||||
@@ -16,6 +17,8 @@ export class StartComponent implements OnInit {
|
||||
eventName = '';
|
||||
mouseDragStartX: number;
|
||||
blockchainScrollLeftInit: number;
|
||||
timeLtrSubscription: Subscription;
|
||||
timeLtr: boolean = this.stateService.timeLtr.value;
|
||||
@ViewChild('blockchainContainer') blockchainContainer: ElementRef;
|
||||
|
||||
constructor(
|
||||
@@ -23,6 +26,9 @@ export class StartComponent implements OnInit {
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.timeLtrSubscription = this.stateService.timeLtr.subscribe((ltr) => {
|
||||
this.timeLtr = !!ltr;
|
||||
});
|
||||
this.stateService.blocks$
|
||||
.subscribe((blocks: any) => {
|
||||
if (this.stateService.network !== '') {
|
||||
@@ -72,4 +78,8 @@ export class StartComponent implements OnInit {
|
||||
this.mouseDragStartX = null;
|
||||
this.stateService.setBlockScrollingInProgress(false);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.timeLtrSubscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,11 +11,15 @@
|
||||
[showZoom]="false"
|
||||
></app-mempool-graph>
|
||||
</div>
|
||||
<div class="blockchain-wrapper">
|
||||
<div class="blockchain-wrapper" [dir]="timeLtr ? 'rtl' : 'ltr'" [class.time-ltr]="timeLtr">
|
||||
<div class="position-container">
|
||||
<app-mempool-blocks></app-mempool-blocks>
|
||||
<app-blockchain-blocks></app-blockchain-blocks>
|
||||
<div id="divider"></div>
|
||||
<span>
|
||||
<div class="blocks-wrapper">
|
||||
<app-mempool-blocks></app-mempool-blocks>
|
||||
<app-blockchain-blocks></app-blockchain-blocks>
|
||||
</div>
|
||||
<div id="divider"></div>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -31,8 +31,9 @@
|
||||
|
||||
.position-container {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
left: 0;
|
||||
bottom: 170px;
|
||||
transform: translateX(50vw);
|
||||
}
|
||||
|
||||
#divider {
|
||||
@@ -47,9 +48,33 @@
|
||||
top: -28px;
|
||||
}
|
||||
}
|
||||
|
||||
&.time-ltr {
|
||||
.blocks-wrapper {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:host-context(.ltr-layout) {
|
||||
.blockchain-wrapper.time-ltr .blocks-wrapper,
|
||||
.blockchain-wrapper .blocks-wrapper {
|
||||
direction: ltr;
|
||||
}
|
||||
}
|
||||
|
||||
:host-context(.rtl-layout) {
|
||||
.blockchain-wrapper.time-ltr .blocks-wrapper,
|
||||
.blockchain-wrapper .blocks-wrapper {
|
||||
direction: rtl;
|
||||
}
|
||||
}
|
||||
|
||||
.tv-container {
|
||||
display: flex;
|
||||
margin-top: 0px;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { WebsocketService } from '../../services/websocket.service';
|
||||
import { OptimizedMempoolStats } from '../../interfaces/node-api.interface';
|
||||
import { StateService } from '../../services/state.service';
|
||||
@@ -6,7 +6,7 @@ import { ApiService } from '../../services/api.service';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { map, scan, startWith, switchMap, tap } from 'rxjs/operators';
|
||||
import { interval, merge, Observable } from 'rxjs';
|
||||
import { interval, merge, Observable, Subscription } from 'rxjs';
|
||||
import { ChangeDetectionStrategy } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
@@ -15,11 +15,13 @@ import { ChangeDetectionStrategy } from '@angular/core';
|
||||
styleUrls: ['./television.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class TelevisionComponent implements OnInit {
|
||||
export class TelevisionComponent implements OnInit, OnDestroy {
|
||||
|
||||
mempoolStats: OptimizedMempoolStats[] = [];
|
||||
statsSubscription$: Observable<OptimizedMempoolStats[]>;
|
||||
fragment: string;
|
||||
timeLtrSubscription: Subscription;
|
||||
timeLtr: boolean = this.stateService.timeLtr.value;
|
||||
|
||||
constructor(
|
||||
private websocketService: WebsocketService,
|
||||
@@ -37,6 +39,10 @@ export class TelevisionComponent implements OnInit {
|
||||
this.seoService.setTitle($localize`:@@46ce8155c9ab953edeec97e8950b5a21e67d7c4e:TV view`);
|
||||
this.websocketService.want(['blocks', 'live-2h-chart', 'mempool-blocks']);
|
||||
|
||||
this.timeLtrSubscription = this.stateService.timeLtr.subscribe((ltr) => {
|
||||
this.timeLtr = !!ltr;
|
||||
});
|
||||
|
||||
this.statsSubscription$ = merge(
|
||||
this.stateService.live2Chart$.pipe(map(stats => [stats])),
|
||||
this.route.fragment
|
||||
@@ -70,4 +76,8 @@ export class TelevisionComponent implements OnInit {
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.timeLtrSubscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
</ng-template>
|
||||
</span>
|
||||
<span class="field col-sm-4 text-center"><ng-container *ngIf="transactionTime > 0">‎{{ transactionTime * 1000 | date:'yyyy-MM-dd HH:mm' }}</ng-container></span>
|
||||
<span class="field col-sm-4 text-right"><span class="label" i18n="transaction.fee|Transaction fee">Fee </span>{{ tx.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span></span>
|
||||
<span class="field col-sm-4 text-right"><span class="label" i18n="transaction.fee|Transaction fee">Fee</span> {{ tx.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span></span>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
@@ -190,12 +190,12 @@
|
||||
|
||||
<br>
|
||||
|
||||
<ng-container *ngIf="showFlow; else flowPlaceholder">
|
||||
<ng-container *ngIf="flowEnabled; else flowPlaceholder">
|
||||
<div class="title float-left">
|
||||
<h2 id="flow" i18n="transaction.flow|Transaction flow">Flow</h2>
|
||||
</div>
|
||||
|
||||
<button type="button" class="btn btn-outline-info flow-toggle btn-sm float-right" (click)="toggleGraph()" i18n="hide-flow-diagram">Hide flow diagram</button>
|
||||
<button type="button" class="btn btn-outline-info flow-toggle btn-sm float-right" (click)="toggleGraph()" i18n="hide-diagram">Hide diagram</button>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
@@ -208,7 +208,9 @@
|
||||
[lineLimit]="inOutLimit"
|
||||
[maxStrands]="graphExpanded ? maxInOut : 24"
|
||||
[network]="network"
|
||||
[tooltip]="true">
|
||||
[tooltip]="true"
|
||||
[inputIndex]="inputIndex" [outputIndex]="outputIndex"
|
||||
>
|
||||
</tx-bowtie-graph>
|
||||
</div>
|
||||
<div class="toggle-wrapper" *ngIf="maxInOut > 24">
|
||||
@@ -234,13 +236,13 @@
|
||||
</div>
|
||||
|
||||
<div class="title-buttons">
|
||||
<button *ngIf="!showFlow" type="button" class="btn btn-outline-info flow-toggle btn-sm" (click)="toggleGraph()" i18n="show">Show flow diagram</button>
|
||||
<button *ngIf="!flowEnabled" type="button" class="btn btn-outline-info flow-toggle btn-sm" (click)="toggleGraph()" i18n="show-diagram">Show diagram</button>
|
||||
<button type="button" class="btn btn-outline-info btn-sm" (click)="txList.toggleDetails()" i18n="transaction.details|Transaction Details">Details</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<app-transactions-list #txList [transactions]="[tx]" [errorUnblinded]="errorUnblinded" [outputIndex]="outputIndex" [transactionPage]="true"></app-transactions-list>
|
||||
<app-transactions-list #txList [transactions]="[tx]" [errorUnblinded]="errorUnblinded" [inputIndex]="inputIndex" [outputIndex]="outputIndex" [transactionPage]="true"></app-transactions-list>
|
||||
|
||||
<div class="title text-left">
|
||||
<h2 i18n="transaction.details">Details</h2>
|
||||
@@ -325,7 +327,7 @@
|
||||
|
||||
<br>
|
||||
|
||||
<ng-container *ngIf="showFlow">
|
||||
<ng-container *ngIf="flowEnabled">
|
||||
<div class="title">
|
||||
<h2 i18n="transaction.flow|Transaction flow">Flow</h2>
|
||||
</div>
|
||||
|
||||
@@ -7,34 +7,34 @@
|
||||
}
|
||||
|
||||
.title-block {
|
||||
flex-wrap: wrap;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
@media (min-width: 650px) {
|
||||
flex-direction: row;
|
||||
}
|
||||
h1 {
|
||||
margin: 0rem;
|
||||
margin-right: 15px;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
.tx-link {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
margin-bottom: 0px;
|
||||
margin-top: 8px;
|
||||
@media (min-width: 650px) {
|
||||
align-self: end;
|
||||
margin-left: 15px;
|
||||
margin-top: 0px;
|
||||
margin-bottom: -3px;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
flex-shrink: 0;
|
||||
@media (min-width: 651px) {
|
||||
display: flex;
|
||||
width: auto;
|
||||
flex-grow: 1;
|
||||
margin-bottom: 0px;
|
||||
top: 1px;
|
||||
position: relative;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
order: 3;
|
||||
}
|
||||
}
|
||||
@media (max-width: 650px) {
|
||||
order: 3;
|
||||
}
|
||||
}
|
||||
|
||||
.td-width {
|
||||
|
||||
@@ -18,6 +18,7 @@ import { ApiService } from '../../services/api.service';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { BlockExtended, CpfpInfo } from '../../interfaces/node-api.interface';
|
||||
import { LiquidUnblinding } from './liquid-ublinding';
|
||||
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
||||
|
||||
@Component({
|
||||
selector: 'app-transaction',
|
||||
@@ -40,6 +41,8 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
txReplacedSubscription: Subscription;
|
||||
blocksSubscription: Subscription;
|
||||
queryParamsSubscription: Subscription;
|
||||
urlFragmentSubscription: Subscription;
|
||||
fragmentParams: URLSearchParams;
|
||||
rbfTransaction: undefined | Transaction;
|
||||
cpfpInfo: CpfpInfo | null;
|
||||
showCpfpDetails = false;
|
||||
@@ -47,13 +50,17 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
now = new Date().getTime();
|
||||
timeAvg$: Observable<number>;
|
||||
liquidUnblinding = new LiquidUnblinding();
|
||||
inputIndex: number;
|
||||
outputIndex: number;
|
||||
showFlow: boolean = true;
|
||||
graphExpanded: boolean = false;
|
||||
graphWidth: number = 1000;
|
||||
graphHeight: number = 360;
|
||||
inOutLimit: number = 150;
|
||||
maxInOut: number = 0;
|
||||
flowPrefSubscription: Subscription;
|
||||
hideFlow: boolean = this.stateService.hideFlow.value;
|
||||
overrideFlowPreference: boolean = null;
|
||||
flowEnabled: boolean;
|
||||
|
||||
tooltipPosition: { x: number, y: number };
|
||||
|
||||
@@ -63,6 +70,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private relativeUrlPipe: RelativeUrlPipe,
|
||||
private electrsApiService: ElectrsApiService,
|
||||
private stateService: StateService,
|
||||
private websocketService: WebsocketService,
|
||||
@@ -77,12 +85,26 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
(network) => (this.network = network)
|
||||
);
|
||||
|
||||
this.setFlowEnabled();
|
||||
this.flowPrefSubscription = this.stateService.hideFlow.subscribe((hide) => {
|
||||
this.hideFlow = !!hide;
|
||||
this.setFlowEnabled();
|
||||
});
|
||||
|
||||
this.timeAvg$ = timer(0, 1000)
|
||||
.pipe(
|
||||
switchMap(() => this.stateService.difficultyAdjustment$),
|
||||
map((da) => da.timeAvg)
|
||||
);
|
||||
|
||||
this.urlFragmentSubscription = this.route.fragment.subscribe((fragment) => {
|
||||
this.fragmentParams = new URLSearchParams(fragment || '');
|
||||
const vin = parseInt(this.fragmentParams.get('vin'), 10);
|
||||
const vout = parseInt(this.fragmentParams.get('vout'), 10);
|
||||
this.inputIndex = (!isNaN(vin) && vin >= 0) ? vin : null;
|
||||
this.outputIndex = (!isNaN(vout) && vout >= 0) ? vout : null;
|
||||
});
|
||||
|
||||
this.fetchCpfpSubscription = this.fetchCpfp$
|
||||
.pipe(
|
||||
switchMap((txId) =>
|
||||
@@ -121,8 +143,31 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
.pipe(
|
||||
switchMap((params: ParamMap) => {
|
||||
const urlMatch = (params.get('id') || '').split(':');
|
||||
this.txId = urlMatch[0];
|
||||
this.outputIndex = urlMatch[1] === undefined ? null : parseInt(urlMatch[1], 10);
|
||||
if (urlMatch.length === 2 && urlMatch[1].length === 64) {
|
||||
const vin = parseInt(urlMatch[0], 10);
|
||||
this.txId = urlMatch[1];
|
||||
// rewrite legacy vin syntax
|
||||
if (!isNaN(vin)) {
|
||||
this.fragmentParams.set('vin', vin.toString());
|
||||
this.fragmentParams.delete('vout');
|
||||
}
|
||||
this.router.navigate([this.relativeUrlPipe.transform('/tx'), this.txId], {
|
||||
queryParamsHandling: 'merge',
|
||||
fragment: this.fragmentParams.toString(),
|
||||
});
|
||||
} else {
|
||||
this.txId = urlMatch[0];
|
||||
const vout = parseInt(urlMatch[1], 10);
|
||||
if (urlMatch.length > 1 && !isNaN(vout)) {
|
||||
// rewrite legacy vout syntax
|
||||
this.fragmentParams.set('vout', vout.toString());
|
||||
this.fragmentParams.delete('vin');
|
||||
this.router.navigate([this.relativeUrlPipe.transform('/tx'), this.txId], {
|
||||
queryParamsHandling: 'merge',
|
||||
fragment: this.fragmentParams.toString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
this.seoService.setTitle(
|
||||
$localize`:@@bisq.transaction.browser-title:Transaction: ${this.txId}:INTERPOLATION:`
|
||||
);
|
||||
@@ -205,6 +250,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.fetchCpfp$.next(this.tx.txid);
|
||||
}
|
||||
}
|
||||
setTimeout(() => { this.applyFragment(); }, 0);
|
||||
},
|
||||
(error) => {
|
||||
this.error = error;
|
||||
@@ -237,11 +283,14 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
this.queryParamsSubscription = this.route.queryParams.subscribe((params) => {
|
||||
if (params.showFlow === 'false') {
|
||||
this.showFlow = false;
|
||||
this.overrideFlowPreference = false;
|
||||
} else if (params.showFlow === 'true') {
|
||||
this.overrideFlowPreference = true;
|
||||
} else {
|
||||
this.showFlow = true;
|
||||
this.setGraphSize();
|
||||
this.overrideFlowPreference = null;
|
||||
}
|
||||
this.setFlowEnabled();
|
||||
this.setGraphSize();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -317,15 +366,20 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
|
||||
toggleGraph() {
|
||||
this.showFlow = !this.showFlow;
|
||||
const showFlow = !this.flowEnabled;
|
||||
this.stateService.hideFlow.next(!showFlow);
|
||||
this.router.navigate([], {
|
||||
relativeTo: this.route,
|
||||
queryParams: { showFlow: this.showFlow },
|
||||
queryParams: { showFlow: showFlow },
|
||||
queryParamsHandling: 'merge',
|
||||
fragment: 'flow'
|
||||
});
|
||||
}
|
||||
|
||||
setFlowEnabled() {
|
||||
this.flowEnabled = (this.overrideFlowPreference != null ? this.overrideFlowPreference : !this.hideFlow);
|
||||
}
|
||||
|
||||
expandGraph() {
|
||||
this.graphExpanded = true;
|
||||
}
|
||||
@@ -334,6 +388,17 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.graphExpanded = false;
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@HostListener('window:resize', ['$event'])
|
||||
setGraphSize(): void {
|
||||
if (this.graphContainer) {
|
||||
@@ -347,6 +412,8 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.txReplacedSubscription.unsubscribe();
|
||||
this.blocksSubscription.unsubscribe();
|
||||
this.queryParamsSubscription.unsubscribe();
|
||||
this.flowPrefSubscription.unsubscribe();
|
||||
this.urlFragmentSubscription.unsubscribe();
|
||||
this.leaveTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,9 +20,9 @@
|
||||
<div class="col">
|
||||
<table class="table table-borderless smaller-text table-sm table-tx-vin">
|
||||
<tbody>
|
||||
<ng-template ngFor let-vin let-vindex="index" [ngForOf]="tx['@vinLimit'] ? ((tx.vin.length > rowLimit) ? tx.vin.slice(0, rowLimit - 2) : tx.vin.slice(0, rowLimit)) : tx.vin" [ngForTrackBy]="trackByIndexFn">
|
||||
<ng-template ngFor let-vin let-vindex="index" [ngForOf]="tx['@vinLimit'] ? ((tx.vin.length > inputRowLimit) ? tx.vin.slice(0, inputRowLimit - 2) : tx.vin.slice(0, inputRowLimit)) : tx.vin" [ngForTrackBy]="trackByIndexFn">
|
||||
<tr [ngClass]="{
|
||||
'assetBox': assetsMinimal && vin.prevout && assetsMinimal[vin.prevout.asset] && !vin.is_coinbase && vin.prevout.scriptpubkey_address && tx._unblinded,
|
||||
'assetBox': (assetsMinimal && vin.prevout && assetsMinimal[vin.prevout.asset] && !vin.is_coinbase && vin.prevout.scriptpubkey_address && tx._unblinded) || inputIndex === vindex,
|
||||
'highlight': vin.prevout?.scriptpubkey_address === this.address && this.address !== ''
|
||||
}">
|
||||
<td class="arrow-td">
|
||||
@@ -43,7 +43,7 @@
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
<ng-template #defaultPrevout>
|
||||
<a [routerLink]="['/tx/' | relativeUrl, vin.txid + ':' + vin.vout]" class="red">
|
||||
<a [routerLink]="['/tx/' | relativeUrl, vin.txid]" [fragment]="'vout=' + vin.vout" class="red">
|
||||
<fa-icon [icon]="['fas', 'arrow-alt-circle-right']" [fixedWidth]="true"></fa-icon>
|
||||
</a>
|
||||
</ng-template>
|
||||
@@ -146,7 +146,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<tr *ngIf="tx.vin.length > rowLimit && tx['@vinLimit']">
|
||||
<tr *ngIf="tx.vin.length > inputRowLimit && tx['@vinLimit']">
|
||||
<td colspan="3" class="text-center">
|
||||
<button class="btn btn-sm btn-primary mt-2" (click)="loadMoreInputs(tx);"><span i18n="show-all">Show all</span> ({{ tx.vin.length }})</button>
|
||||
</td>
|
||||
@@ -158,7 +158,7 @@
|
||||
<div class="col mobile-bottomcol">
|
||||
<table class="table table-borderless smaller-text table-sm table-tx-vout">
|
||||
<tbody>
|
||||
<ng-template ngFor let-vout let-vindex="index" [ngForOf]="tx['@voutLimit'] && !outputIndex ? ((tx.vout.length > rowLimit) ? tx.vout.slice(0, rowLimit - 2) : tx.vout.slice(0, rowLimit)) : tx.vout" [ngForTrackBy]="trackByIndexFn">
|
||||
<ng-template ngFor let-vout let-vindex="index" [ngForOf]="tx['@voutLimit'] ? ((tx.vout.length > outputRowLimit) ? tx.vout.slice(0, outputRowLimit - 2) : tx.vout.slice(0, outputRowLimit)) : tx.vout" [ngForTrackBy]="trackByIndexFn">
|
||||
<tr [ngClass]="{
|
||||
'assetBox': assetsMinimal && assetsMinimal[vout.asset] && vout.scriptpubkey_address && tx.vin && !tx.vin[0].is_coinbase && tx._unblinded || outputIndex === vindex,
|
||||
'highlight': vout.scriptpubkey_address === this.address && this.address !== ''
|
||||
@@ -220,7 +220,7 @@
|
||||
<fa-icon [icon]="['fas', 'arrow-alt-circle-right']" [fixedWidth]="true"></fa-icon>
|
||||
</span>
|
||||
<ng-template #spent>
|
||||
<a *ngIf="tx._outspends[vindex].txid else outputNoTxId" [routerLink]="['/tx/' | relativeUrl, tx._outspends[vindex].txid]" class="red">
|
||||
<a *ngIf="tx._outspends[vindex].txid else outputNoTxId" [routerLink]="['/tx/' | relativeUrl, tx._outspends[vindex].txid]" [fragment]="'vin=' + tx._outspends[vindex].vin" class="red">
|
||||
<fa-icon [icon]="['fas', 'arrow-alt-circle-right']" [fixedWidth]="true"></fa-icon>
|
||||
</a>
|
||||
<ng-template #outputNoTxId>
|
||||
@@ -257,7 +257,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<tr *ngIf="tx.vout.length > rowLimit && tx['@voutLimit'] && !outputIndex">
|
||||
<tr *ngIf="tx.vout.length > outputRowLimit && tx['@voutLimit']">
|
||||
<td colspan="3" class="text-center">
|
||||
<button class="btn btn-sm btn-primary mt-2" (click)="tx['@voutLimit'] = false;"><span i18n="show-all">Show all</span> ({{ tx.vout.length }})</button>
|
||||
</td>
|
||||
|
||||
@@ -24,6 +24,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
||||
@Input() transactionPage = false;
|
||||
@Input() errorUnblinded = false;
|
||||
@Input() paginated = false;
|
||||
@Input() inputIndex: number;
|
||||
@Input() outputIndex: number;
|
||||
@Input() address: string = '';
|
||||
@Input() rowLimit = 12;
|
||||
@@ -37,6 +38,8 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
||||
showDetails$ = new BehaviorSubject<boolean>(false);
|
||||
assetsMinimal: any;
|
||||
transactionsLength: number = 0;
|
||||
inputRowLimit: number = 12;
|
||||
outputRowLimit: number = 12;
|
||||
|
||||
constructor(
|
||||
public stateService: StateService,
|
||||
@@ -97,50 +100,57 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
||||
).subscribe(() => this.ref.markForCheck());
|
||||
}
|
||||
|
||||
ngOnChanges(): void {
|
||||
if (!this.transactions || !this.transactions.length) {
|
||||
return;
|
||||
ngOnChanges(changes): void {
|
||||
if (changes.inputIndex || changes.outputIndex || changes.rowLimit) {
|
||||
this.inputRowLimit = Math.max(this.rowLimit, (this.inputIndex || 0) + 3);
|
||||
this.outputRowLimit = Math.max(this.rowLimit, (this.outputIndex || 0) + 3);
|
||||
if ((this.inputIndex || this.outputIndex) && !changes.transactions) {
|
||||
setTimeout(() => {
|
||||
const assetBoxElements = document.getElementsByClassName('assetBox');
|
||||
if (assetBoxElements && assetBoxElements[0]) {
|
||||
assetBoxElements[0].scrollIntoView({block: "center"});
|
||||
}
|
||||
}, 10);
|
||||
}
|
||||
}
|
||||
|
||||
this.transactionsLength = this.transactions.length;
|
||||
if (this.outputIndex) {
|
||||
setTimeout(() => {
|
||||
const assetBoxElements = document.getElementsByClassName('assetBox');
|
||||
if (assetBoxElements && assetBoxElements[0]) {
|
||||
assetBoxElements[0].scrollIntoView();
|
||||
}
|
||||
}, 10);
|
||||
}
|
||||
|
||||
this.transactions.forEach((tx) => {
|
||||
tx['@voutLimit'] = true;
|
||||
tx['@vinLimit'] = true;
|
||||
if (tx['addressValue'] !== undefined) {
|
||||
if (changes.transactions || changes.address) {
|
||||
if (!this.transactions || !this.transactions.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.address) {
|
||||
const addressIn = tx.vout
|
||||
.filter((v: Vout) => v.scriptpubkey_address === this.address)
|
||||
.map((v: Vout) => v.value || 0)
|
||||
.reduce((a: number, b: number) => a + b, 0);
|
||||
this.transactionsLength = this.transactions.length;
|
||||
|
||||
const addressOut = tx.vin
|
||||
.filter((v: Vin) => v.prevout && v.prevout.scriptpubkey_address === this.address)
|
||||
.map((v: Vin) => v.prevout.value || 0)
|
||||
.reduce((a: number, b: number) => a + b, 0);
|
||||
|
||||
tx['addressValue'] = addressIn - addressOut;
|
||||
}
|
||||
});
|
||||
const txIds = this.transactions.filter((tx) => !tx._outspends).map((tx) => tx.txid);
|
||||
if (txIds.length) {
|
||||
this.refreshOutspends$.next(txIds);
|
||||
}
|
||||
if (this.stateService.env.LIGHTNING) {
|
||||
const txIds = this.transactions.filter((tx) => !tx._channels).map((tx) => tx.txid);
|
||||
this.transactions.forEach((tx) => {
|
||||
tx['@voutLimit'] = true;
|
||||
tx['@vinLimit'] = true;
|
||||
if (tx['addressValue'] !== undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.address) {
|
||||
const addressIn = tx.vout
|
||||
.filter((v: Vout) => v.scriptpubkey_address === this.address)
|
||||
.map((v: Vout) => v.value || 0)
|
||||
.reduce((a: number, b: number) => a + b, 0);
|
||||
|
||||
const addressOut = tx.vin
|
||||
.filter((v: Vin) => v.prevout && v.prevout.scriptpubkey_address === this.address)
|
||||
.map((v: Vin) => v.prevout.value || 0)
|
||||
.reduce((a: number, b: number) => a + b, 0);
|
||||
|
||||
tx['addressValue'] = addressIn - addressOut;
|
||||
}
|
||||
});
|
||||
const txIds = this.transactions.filter((tx) => !tx._outspends).map((tx) => tx.txid);
|
||||
if (txIds.length) {
|
||||
this.refreshChannels$.next(txIds);
|
||||
this.refreshOutspends$.next(txIds);
|
||||
}
|
||||
if (this.stateService.env.LIGHTNING) {
|
||||
const txIds = this.transactions.filter((tx) => !tx._channels).map((tx) => tx.txid);
|
||||
if (txIds.length) {
|
||||
this.refreshChannels$.next(txIds);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,9 +42,9 @@
|
||||
<ng-container [ngSwitch]="line.type">
|
||||
<span *ngSwitchCase="'input'" i18n="transaction.input">Input</span>
|
||||
<span *ngSwitchCase="'output'" i18n="transaction.output">Output</span>
|
||||
<span *ngSwitchCase="'fee'" i18n="transaction.fee">Fee</span>
|
||||
<span *ngSwitchCase="'fee'" i18n="transaction.fee|Transaction fee">Fee</span>
|
||||
</ng-container>
|
||||
<span *ngIf="line.type !== 'fee'"> #{{ line.index }}</span>
|
||||
<span *ngIf="line.type !== 'fee'"> #{{ line.index + 1 }}</span>
|
||||
</p>
|
||||
<p *ngIf="line.value == null && line.confidential" i18n="shared.confidential">Confidential</p>
|
||||
<p *ngIf="line.value != null"><app-amount [satoshis]="line.value"></app-amount></p>
|
||||
|
||||
@@ -41,6 +41,18 @@
|
||||
<stop offset="98%" [attr.stop-color]="gradient[0]" />
|
||||
<stop offset="100%" [attr.stop-color]="gradient[0]" />
|
||||
</linearGradient>
|
||||
<linearGradient id="input-highlight-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" [attr.stop-color]="gradient[0]" />
|
||||
<stop offset="2%" [attr.stop-color]="gradient[0]" />
|
||||
<stop offset="30%" stop-color="#1bd8f4" />
|
||||
<stop offset="100%" [attr.stop-color]="gradient[1]" />
|
||||
</linearGradient>
|
||||
<linearGradient id="output-highlight-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" [attr.stop-color]="gradient[1]" />
|
||||
<stop offset="70%" stop-color="#1bd8f4" />
|
||||
<stop offset="98%" [attr.stop-color]="gradient[0]" />
|
||||
<stop offset="100%" [attr.stop-color]="gradient[0]" />
|
||||
</linearGradient>
|
||||
<linearGradient id="fee-hover-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" [attr.stop-color]="gradient[1]" />
|
||||
<stop offset="100%" stop-color="white" />
|
||||
@@ -56,20 +68,24 @@
|
||||
<path
|
||||
[attr.d]="input.path"
|
||||
class="line {{input.class}}"
|
||||
[class.highlight]="inputIndex != null && inputData[i].index === inputIndex"
|
||||
[style]="input.style"
|
||||
attr.marker-start="url(#{{input.class}}-arrow)"
|
||||
(pointerover)="onHover($event, 'input', i);"
|
||||
(pointerout)="onBlur($event, 'input', i);"
|
||||
(click)="onClick($event, 'input', inputData[i].index);"
|
||||
/>
|
||||
</ng-container>
|
||||
<ng-container *ngFor="let output of outputs; let i = index">
|
||||
<path
|
||||
[attr.d]="output.path"
|
||||
class="line {{output.class}}"
|
||||
[class.highlight]="outputIndex != null && outputData[i].index === outputIndex"
|
||||
[style]="output.style"
|
||||
attr.marker-start="url(#{{output.class}}-arrow)"
|
||||
(pointerover)="onHover($event, 'output', i);"
|
||||
(pointerout)="onBlur($event, 'output', i);"
|
||||
(click)="onClick($event, 'output', outputData[i].index);"
|
||||
/>
|
||||
</ng-container>
|
||||
</svg>
|
||||
|
||||
@@ -12,6 +12,17 @@
|
||||
stroke: url(#fee-gradient);
|
||||
}
|
||||
|
||||
&.highlight {
|
||||
z-index: 8;
|
||||
cursor: pointer;
|
||||
&.input {
|
||||
stroke: url(#input-highlight-gradient);
|
||||
}
|
||||
&.output {
|
||||
stroke: url(#output-highlight-gradient);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
z-index: 10;
|
||||
cursor: pointer;
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { Component, OnInit, Input, OnChanges, HostListener } from '@angular/core';
|
||||
import { Transaction } from '../../interfaces/electrs.interface';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { Outspend, Transaction } from '../../interfaces/electrs.interface';
|
||||
import { Router } from '@angular/router';
|
||||
import { ReplaySubject, merge, Subscription } from 'rxjs';
|
||||
import { tap, switchMap } from 'rxjs/operators';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
||||
|
||||
interface SvgLine {
|
||||
path: string;
|
||||
@@ -34,6 +40,8 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
|
||||
@Input() minWeight = 2; //
|
||||
@Input() maxStrands = 24; // number of inputs/outputs to keep fully on-screen.
|
||||
@Input() tooltip = false;
|
||||
@Input() inputIndex: number;
|
||||
@Input() outputIndex: number;
|
||||
|
||||
inputData: Xput[];
|
||||
outputData: Xput[];
|
||||
@@ -45,6 +53,10 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
|
||||
isLiquid: boolean = false;
|
||||
hoverLine: Xput | void = null;
|
||||
tooltipPosition = { x: 0, y: 0 };
|
||||
outspends: Outspend[] = [];
|
||||
|
||||
outspendsSubscription: Subscription;
|
||||
refreshOutspends$: ReplaySubject<string> = new ReplaySubject();
|
||||
|
||||
gradientColors = {
|
||||
'': ['#9339f4', '#105fb0'],
|
||||
@@ -61,12 +73,45 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
|
||||
|
||||
gradient: string[] = ['#105fb0', '#105fb0'];
|
||||
|
||||
constructor(
|
||||
private router: Router,
|
||||
private relativeUrlPipe: RelativeUrlPipe,
|
||||
private stateService: StateService,
|
||||
private apiService: ApiService,
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.initGraph();
|
||||
|
||||
this.outspendsSubscription = merge(
|
||||
this.refreshOutspends$
|
||||
.pipe(
|
||||
switchMap((txid) => this.apiService.getOutspendsBatched$([txid])),
|
||||
tap((outspends: Outspend[][]) => {
|
||||
if (!this.tx || !outspends || !outspends.length) {
|
||||
return;
|
||||
}
|
||||
this.outspends = outspends[0];
|
||||
}),
|
||||
),
|
||||
this.stateService.utxoSpent$
|
||||
.pipe(
|
||||
tap((utxoSpent) => {
|
||||
for (const i in utxoSpent) {
|
||||
this.outspends[i] = {
|
||||
spent: true,
|
||||
txid: utxoSpent[i].txid,
|
||||
vin: utxoSpent[i].vin,
|
||||
};
|
||||
}
|
||||
}),
|
||||
),
|
||||
).subscribe(() => {});
|
||||
}
|
||||
|
||||
ngOnChanges(): void {
|
||||
this.initGraph();
|
||||
this.refreshOutspends$.next(this.tx.txid);
|
||||
}
|
||||
|
||||
initGraph(): void {
|
||||
@@ -76,11 +121,12 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
|
||||
this.combinedWeight = Math.min(this.maxCombinedWeight, Math.floor((this.width - (2 * this.midWidth)) / 6));
|
||||
|
||||
const totalValue = this.calcTotalValue(this.tx);
|
||||
let voutWithFee = this.tx.vout.map(v => {
|
||||
let voutWithFee = this.tx.vout.map((v, i) => {
|
||||
return {
|
||||
type: v.scriptpubkey_type === 'fee' ? 'fee' : 'output',
|
||||
value: v?.value,
|
||||
address: v?.scriptpubkey_address || v?.scriptpubkey_type?.toUpperCase(),
|
||||
index: i,
|
||||
pegout: v?.pegout?.scriptpubkey_address,
|
||||
confidential: (this.isLiquid && v?.value === undefined),
|
||||
} as Xput;
|
||||
@@ -91,11 +137,12 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
|
||||
}
|
||||
const outputCount = voutWithFee.length;
|
||||
|
||||
let truncatedInputs = this.tx.vin.map(v => {
|
||||
let truncatedInputs = this.tx.vin.map((v, i) => {
|
||||
return {
|
||||
type: 'input',
|
||||
value: v?.prevout?.value,
|
||||
address: v?.prevout?.scriptpubkey_address || v?.prevout?.scriptpubkey_type?.toUpperCase(),
|
||||
index: i,
|
||||
coinbase: v?.is_coinbase,
|
||||
pegin: v?.is_pegin,
|
||||
confidential: (this.isLiquid && v?.prevout?.value === undefined),
|
||||
@@ -306,8 +353,7 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
|
||||
};
|
||||
} else {
|
||||
this.hoverLine = {
|
||||
...this.outputData[index],
|
||||
index
|
||||
...this.outputData[index]
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -315,4 +361,47 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
|
||||
onBlur(event, side, index): void {
|
||||
this.hoverLine = null;
|
||||
}
|
||||
|
||||
onClick(event, side, index): void {
|
||||
if (side === 'input') {
|
||||
const input = this.tx.vin[index];
|
||||
if (input && !input.is_coinbase && !input.is_pegin && input.txid && input.vout != null) {
|
||||
this.router.navigate([this.relativeUrlPipe.transform('/tx'), input.txid], {
|
||||
queryParamsHandling: 'merge',
|
||||
fragment: (new URLSearchParams({
|
||||
flow: '',
|
||||
vout: input.vout.toString(),
|
||||
})).toString(),
|
||||
});
|
||||
} else if (index != null) {
|
||||
this.router.navigate([this.relativeUrlPipe.transform('/tx'), this.tx.txid], {
|
||||
queryParamsHandling: 'merge',
|
||||
fragment: (new URLSearchParams({
|
||||
flow: '',
|
||||
vin: index.toString(),
|
||||
})).toString(),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const output = this.tx.vout[index];
|
||||
const outspend = this.outspends[index];
|
||||
if (output && outspend && outspend.spent && outspend.txid) {
|
||||
this.router.navigate([this.relativeUrlPipe.transform('/tx'), outspend.txid], {
|
||||
queryParamsHandling: 'merge',
|
||||
fragment: (new URLSearchParams({
|
||||
flow: '',
|
||||
vin: outspend.vin.toString(),
|
||||
})).toString(),
|
||||
});
|
||||
} else if (index != null) {
|
||||
this.router.navigate([this.relativeUrlPipe.transform('/tx'), this.tx.txid], {
|
||||
queryParamsHandling: 'merge',
|
||||
fragment: (new URLSearchParams({
|
||||
flow: '',
|
||||
vout: index.toString(),
|
||||
})).toString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,10 @@
|
||||
<ng-template #notFullyTaproot>
|
||||
<span *ngIf="segwitGains.realizedTaprootGains && segwitGains.potentialTaprootGains; else noTaproot" class="badge badge-warning mr-1" i18n-ngbTooltip="Tooltip about fees that saved and could be saved with taproot" ngbTooltip="This transaction uses Taproot and already saved at least {{ segwitGains.realizedTaprootGains * 100 | number: '1.0-0' }}% on fees, but could save an additional {{ segwitGains.potentialTaprootGains * 100 | number: '1.0-0' }}% by fully using Taproot" placement="bottom" i18n="tx-features.tag.taproot|Taproot">Taproot</span>
|
||||
<ng-template #noTaproot>
|
||||
<span *ngIf="segwitGains.potentialTaprootGains; else taprootButNoGains" class="badge badge-danger mr-1" i18n-ngbTooltip="Tooltip about fees that could be saved with taproot" ngbTooltip="This transaction could save {{ segwitGains.potentialTaprootGains * 100 | number: '1.0-0' }}% on fees by using Taproot" placement="bottom"><del i18n="tx-features.tag.taproot|Taproot">Taproot</del></span>
|
||||
<span *ngIf="segwitGains.potentialTaprootGains && segwitGains.potentialTaprootGains > 0; else negativeTaprootGains" class="badge badge-danger mr-1" i18n-ngbTooltip="Tooltip about fees that could be saved with taproot" ngbTooltip="This transaction could save {{ segwitGains.potentialTaprootGains * 100 | number: '1.0-0' }}% on fees by using Taproot" placement="bottom"><del i18n="tx-features.tag.taproot|Taproot">Taproot</del></span>
|
||||
<ng-template #negativeTaprootGains>
|
||||
<span *ngIf="!isTaproot; else taprootButNoGains" class="badge badge-danger mr-1" i18n-ngbTooltip="Tooltip about using taproot" ngbTooltip="This transaction does not use Taproot" placement="bottom"><del i18n="tx-features.tag.taproot|Taproot">Taproot</del></span>
|
||||
</ng-template>
|
||||
<ng-template #taprootButNoGains>
|
||||
<span *ngIf="isTaproot" class="badge badge-success mr-1" i18n-ngbTooltip="Tooltip about taproot" ngbTooltip="This transaction uses Taproot" placement="bottom" i18n="tx-features.tag.taproot|Taproot">Taproot</span>
|
||||
</ng-template>
|
||||
|
||||
@@ -123,7 +123,7 @@
|
||||
<th class="table-cell-txid" i18n="dashboard.latest-transactions.txid">TXID</th>
|
||||
<th class="table-cell-satoshis" i18n="dashboard.latest-transactions.amount">Amount</th>
|
||||
<th class="table-cell-fiat" *ngIf="(network$ | async) === ''" i18n="dashboard.latest-transactions.USD">USD</th>
|
||||
<th class="table-cell-fees" i18n="dashboard.latest-transactions.fee">Fee</th>
|
||||
<th class="table-cell-fees" i18n="transaction.fee|Transaction fee">Fee</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let transaction of transactions$ | async; let i = index;">
|
||||
|
||||
@@ -106,6 +106,20 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="electrs" *ngIf="whichTab === 'electrs'">
|
||||
<div class="doc-content no-sidebar">
|
||||
<div class="doc-item-container">
|
||||
<p class='subtitle'>Hostname</p>
|
||||
<p>{{plainHostname}}</p>
|
||||
<p class="subtitle">Port</p>
|
||||
<p>{{electrsPort}}</p>
|
||||
<p class="subtitle">SSL</p>
|
||||
<p>Enabled</p>
|
||||
<p class="note" *ngIf="network.val !== 'signet'">Electrum RPC interface for Bitcoin Signet is <a href="/signet/docs/api/electrs">publicly available</a>. Electrum RPC interface for all other networks is available to <a href='/enterprise'>sponsors</a> only—whitelisting is required.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
|
||||
@@ -1,7 +1,21 @@
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.note {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.text-small {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.container-xl {
|
||||
display: flex;
|
||||
min-height: 75vh;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
code {
|
||||
background-color: #1d1f31;
|
||||
font-family: Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New;
|
||||
@@ -116,6 +130,10 @@ li.nav-item {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.doc-content.no-sidebar {
|
||||
width: 100%
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 2rem 0 0 0;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ import { FaqTemplateDirective } from '../faq-template/faq-template.component';
|
||||
styleUrls: ['./api-docs.component.scss']
|
||||
})
|
||||
export class ApiDocsComponent implements OnInit, AfterViewInit {
|
||||
plainHostname = document.location.hostname;
|
||||
electrsPort = 0;
|
||||
hostname = document.location.hostname;
|
||||
network$: Observable<string>;
|
||||
active = 0;
|
||||
@@ -82,6 +84,20 @@ export class ApiDocsComponent implements OnInit, AfterViewInit {
|
||||
|
||||
this.network$.subscribe((network) => {
|
||||
this.active = (network === 'liquid' || network === 'liquidtestnet') ? 2 : 0;
|
||||
switch( network ) {
|
||||
case "":
|
||||
this.electrsPort = 50002; break;
|
||||
case "mainnet":
|
||||
this.electrsPort = 50002; break;
|
||||
case "testnet":
|
||||
this.electrsPort = 60002; break;
|
||||
case "signet":
|
||||
this.electrsPort = 60602; break;
|
||||
case "liquid":
|
||||
this.electrsPort = 51002; break;
|
||||
case "liquidtestnet":
|
||||
this.electrsPort = 51302; break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ if (browserWindowEnv.BASE_MODULE && (browserWindowEnv.BASE_MODULE === 'bisq' ||
|
||||
},
|
||||
{
|
||||
path: 'faq',
|
||||
data: { networks: ['bitcoin'] },
|
||||
component: DocsComponent
|
||||
},
|
||||
{
|
||||
|
||||
@@ -32,6 +32,15 @@
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
<li [ngbNavItem]="3" *ngIf="showElectrsTab" role="presentation">
|
||||
<a ngbNavLink [routerLink]="['/docs/api/electrs' | relativeUrl]" role="tab">API - Electrum RPC</a>
|
||||
<ng-template ngbNavContent>
|
||||
|
||||
<app-api-docs [whichTab]="'electrs'"></app-api-docs>
|
||||
|
||||
</ng-template>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
<div id="main-tab-content" [ngbNavOutlet]="nav"></div>
|
||||
|
||||
@@ -15,6 +15,7 @@ export class DocsComponent implements OnInit {
|
||||
env: Env;
|
||||
showWebSocketTab = true;
|
||||
showFaqTab = true;
|
||||
showElectrsTab = true;
|
||||
|
||||
@HostBinding('attr.dir') dir = 'ltr';
|
||||
|
||||
@@ -34,14 +35,18 @@ export class DocsComponent implements OnInit {
|
||||
} else if( url[1].path === "rest" ) {
|
||||
this.activeTab = 1;
|
||||
this.seoService.setTitle($localize`:@@e351b40b3869a5c7d19c3d4918cb1ac7aaab95c4:API`);
|
||||
} else {
|
||||
} else if( url[1].path === "websocket" ) {
|
||||
this.activeTab = 2;
|
||||
this.seoService.setTitle($localize`:@@e351b40b3869a5c7d19c3d4918cb1ac7aaab95c4:API`);
|
||||
} else {
|
||||
this.activeTab = 3;
|
||||
this.seoService.setTitle($localize`:@@e351b40b3869a5c7d19c3d4918cb1ac7aaab95c4:API`);
|
||||
}
|
||||
|
||||
this.env = this.stateService.env;
|
||||
this.showWebSocketTab = ( ! ( ( this.stateService.network === "bisq" ) || ( this.stateService.network === "liquidtestnet" ) ) );
|
||||
this.showFaqTab = ( this.env.BASE_MODULE === 'mempool' ) ? true : false;
|
||||
this.showElectrsTab = this.stateService.env.OFFICIAL_MEMPOOL_SPACE && ( this.stateService.network !== "bisq" );
|
||||
|
||||
document.querySelector<HTMLElement>( "html" ).style.scrollBehavior = "smooth";
|
||||
}
|
||||
|
||||
@@ -37,10 +37,12 @@ const routes: Routes = [
|
||||
children: [
|
||||
{
|
||||
path: 'mining/pool/:slug',
|
||||
data: { networks: ['bitcoin'] },
|
||||
component: PoolComponent,
|
||||
},
|
||||
{
|
||||
path: 'mining',
|
||||
data: { networks: ['bitcoin'] },
|
||||
component: StartComponent,
|
||||
children: [
|
||||
{
|
||||
@@ -51,6 +53,7 @@ const routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: 'mempool-block/:id',
|
||||
data: { networks: ['bitcoin', 'liquid'] },
|
||||
component: StartComponent,
|
||||
children: [
|
||||
{
|
||||
@@ -61,62 +64,77 @@ const routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: 'graphs',
|
||||
data: { networks: ['bitcoin', 'liquid'] },
|
||||
component: GraphsComponent,
|
||||
children: [
|
||||
{
|
||||
path: 'mempool',
|
||||
data: { networks: ['bitcoin', 'liquid'] },
|
||||
component: StatisticsComponent,
|
||||
},
|
||||
{
|
||||
path: 'mining/hashrate-difficulty',
|
||||
data: { networks: ['bitcoin'] },
|
||||
component: HashrateChartComponent,
|
||||
},
|
||||
{
|
||||
path: 'mining/pools-dominance',
|
||||
data: { networks: ['bitcoin'] },
|
||||
component: HashrateChartPoolsComponent,
|
||||
},
|
||||
{
|
||||
path: 'mining/pools',
|
||||
data: { networks: ['bitcoin'] },
|
||||
component: PoolRankingComponent,
|
||||
},
|
||||
{
|
||||
path: 'mining/block-fees',
|
||||
data: { networks: ['bitcoin'] },
|
||||
component: BlockFeesGraphComponent,
|
||||
},
|
||||
{
|
||||
path: 'mining/block-rewards',
|
||||
data: { networks: ['bitcoin'] },
|
||||
component: BlockRewardsGraphComponent,
|
||||
},
|
||||
{
|
||||
path: 'mining/block-fee-rates',
|
||||
data: { networks: ['bitcoin'] },
|
||||
component: BlockFeeRatesGraphComponent,
|
||||
},
|
||||
{
|
||||
path: 'mining/block-sizes-weights',
|
||||
data: { networks: ['bitcoin'] },
|
||||
component: BlockSizesWeightsGraphComponent,
|
||||
},
|
||||
{
|
||||
path: 'lightning/nodes-networks',
|
||||
data: { networks: ['bitcoin'] },
|
||||
component: NodesNetworksChartComponent,
|
||||
},
|
||||
{
|
||||
path: 'lightning/capacity',
|
||||
data: { networks: ['bitcoin'] },
|
||||
component: LightningStatisticsChartComponent,
|
||||
},
|
||||
{
|
||||
path: 'lightning/nodes-per-isp',
|
||||
data: { networks: ['bitcoin'] },
|
||||
component: NodesPerISPChartComponent,
|
||||
},
|
||||
{
|
||||
path: 'lightning/nodes-per-country',
|
||||
data: { networks: ['bitcoin'] },
|
||||
component: NodesPerCountryChartComponent,
|
||||
},
|
||||
{
|
||||
path: 'lightning/nodes-map',
|
||||
data: { networks: ['bitcoin'] },
|
||||
component: NodesMap,
|
||||
},
|
||||
{
|
||||
path: 'lightning/nodes-channels-map',
|
||||
data: { networks: ['bitcoin'] },
|
||||
component: NodesChannelsMap,
|
||||
},
|
||||
{
|
||||
@@ -125,6 +143,7 @@ const routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: 'mining/block-prediction',
|
||||
data: { networks: ['bitcoin'] },
|
||||
component: BlockPredictionGraphComponent,
|
||||
},
|
||||
]
|
||||
@@ -141,6 +160,7 @@ const routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: 'tv',
|
||||
data: { networks: ['bitcoin', 'liquid'] },
|
||||
component: TelevisionComponent
|
||||
},
|
||||
];
|
||||
|
||||
@@ -141,7 +141,7 @@ export interface TransactionStripped {
|
||||
fee: number;
|
||||
vsize: number;
|
||||
value: number;
|
||||
status?: 'found' | 'missing' | 'added';
|
||||
status?: 'found' | 'missing' | 'added' | 'censored' | 'selected';
|
||||
}
|
||||
|
||||
export interface RewardStats {
|
||||
|
||||
@@ -70,7 +70,7 @@ export interface TransactionStripped {
|
||||
fee: number;
|
||||
vsize: number;
|
||||
value: number;
|
||||
status?: 'found' | 'missing' | 'added';
|
||||
status?: 'found' | 'missing' | 'added' | 'censored' | 'selected';
|
||||
}
|
||||
|
||||
export interface IBackendInfo {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<app-clipboard [text]="channel.public_key"></app-clipboard>
|
||||
</div>
|
||||
<div class="box-right">
|
||||
<div class="second-line">{{ channel.channels }} channels</div>
|
||||
<div class="second-line"><ng-container *ngTemplateOutlet="xChannels; context: {$implicit: channel.channels }"></ng-container></div>
|
||||
<div class="second-line"><app-amount [satoshis]="channel.capacity" digitsInfo="1.2-2"></app-amount></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -16,7 +16,7 @@
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td i18n="lightning.fee-rate">Fee rate</td>
|
||||
<td i18n="transaction.fee-rate|Transaction fee rate">Fee rate</td>
|
||||
<td>
|
||||
<span class="d-inline-block d-md-none">
|
||||
{{ channel.fee_rate !== null ? (channel.fee_rate | amountShortener : 2 : undefined : true) : '-' }} <span class="symbol">ppm {{ channel.fee_rate !== null ? '(' + (channel.fee_rate / 10000 | amountShortener : 2 : undefined : true) + '%)' : '' }}</span>
|
||||
@@ -33,19 +33,24 @@
|
||||
<span>
|
||||
<span *ngIf="channel.base_fee_mtokens !== null">
|
||||
{{ channel.base_fee_mtokens | amountShortener : 0 }}
|
||||
<span class="symbol">msats</span>
|
||||
<span class="symbol" i18n="shared.m-sats">mSats</span>
|
||||
</span>
|
||||
<span *ngIf="channel.base_fee_mtokens === null">
|
||||
-
|
||||
</span>
|
||||
</span>
|
||||
<span *ngIf="channel.base_fee_mtokens !== null" class="badge" [class]="channel.base_fee_mtokens === 0 ? 'badge-success' : 'badge-danger'"
|
||||
i18n-ngbTooltip="lightning.zero-base-fee"
|
||||
[ngbTooltip]="channel.base_fee_mtokens === 0 ? 'This channel supports zero base fee routing' :
|
||||
'This channel does not support zero base fee routing'"
|
||||
placement="bottom" i18n="lightning.zerobasefee">
|
||||
{{ channel.base_fee_mtokens === 0 ? 'Zero base fee' : 'Non-zero base fee' }}
|
||||
</span>
|
||||
<ng-template [ngIf]="channel.base_fee_mtokens !== null">
|
||||
<span class="badge badge-success" *ngIf="channel.base_fee_mtokens === 0; else nonZeroBaseFee"
|
||||
i18n-ngbTooltip="lightning.zero-base-fee-tooltip"
|
||||
ngbTooltip="This channel supports zero base fee routing"
|
||||
placement="bottom" i18n="lightning.zero-base-fee">Zero base fee</span>
|
||||
</ng-template>
|
||||
<ng-template #nonZeroBaseFee>
|
||||
<span class="badge badge-danger"
|
||||
i18n-ngbTooltip="lightning.non-zero-base-fee-tooltip"
|
||||
ngbTooltip="This channel does not support zero base fee routing"
|
||||
placement="bottom" i18n="lightning.non-zero-base-fee">Non-zero base fee</span>
|
||||
</ng-template>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -62,7 +67,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="lightning.timelock-detla">Timelock delta</td>
|
||||
<td i18n="lightning.timelock-delta">Timelock delta</td>
|
||||
<td>
|
||||
<ng-container *ngTemplateOutlet="blocksPlural; context: {$implicit: channel.cltv_delta ?? '-' }"></ng-container>
|
||||
</td>
|
||||
@@ -72,3 +77,4 @@
|
||||
</div>
|
||||
|
||||
<ng-template #blocksPlural let-i i18n="shared.blocks">{{ i }} <span class="shared-block">blocks</span></ng-template>
|
||||
<ng-template #xChannels let-i i18n="lightning.x-channels">{{ i }} channels</ng-template>
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
<h1 class="title">{{ channel.short_id }}</h1>
|
||||
</div>
|
||||
<div class="badges mb-2">
|
||||
<span class="badge rounded-pill badge-secondary" *ngIf="channel.status === 0">Inactive</span>
|
||||
<span class="badge rounded-pill badge-success" *ngIf="channel.status === 1">Active</span>
|
||||
<span class="badge rounded-pill badge-danger" *ngIf="channel.status === 2">Closed</span>
|
||||
<span class="badge rounded-pill badge-secondary" *ngIf="channel.status === 0" i18n="status.inactive">Inactive</span>
|
||||
<span class="badge rounded-pill badge-success" *ngIf="channel.status === 1" i18n="status.active">Active</span>
|
||||
<span class="badge rounded-pill badge-danger" *ngIf="channel.status === 2" i18n="status.closed">Closed</span>
|
||||
|
||||
<app-closing-type [type]="channel.closing_reason" *ngIf="channel.status === 2"></app-closing-type>
|
||||
</div>
|
||||
@@ -20,20 +20,20 @@
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td i18n="channel.created">Created</td>
|
||||
<td i18n="lightning.created">Created</td>
|
||||
<td>{{ channel.created | date:'yyyy-MM-dd HH:mm' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="channel.capacity">Capacity</td>
|
||||
<td i18n="lightning.capacity">Capacity</td>
|
||||
<td><app-amount [satoshis]="channel.capacity" [noFiat]="true"></app-amount></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="channel.fee-rate">Fee rate</td>
|
||||
<td i18n="transaction.fee-rate|Transaction fee rate">Fee rate</td>
|
||||
<td>
|
||||
<div class="dual-cell">
|
||||
<span>{{ channel.node_left.fee_rate }} <span class="symbol">ppm</span></span>
|
||||
<span>{{ channel.node_left.fee_rate }} <span class="symbol" i18n="lightning.ppm">ppm</span></span>
|
||||
<fa-icon class="between-arrow" [icon]="['fas', 'arrow-right-arrow-left']" [fixedWidth]="true"></fa-icon>
|
||||
<span>{{ channel.node_right.fee_rate }} <span class="symbol">ppm</span></span>
|
||||
<span>{{ channel.node_right.fee_rate }} <span class="symbol" i18n="lightning.ppm">ppm</span></span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -8,9 +8,9 @@
|
||||
</span>
|
||||
</div>
|
||||
<div class="badges mb-2">
|
||||
<span class="badge rounded-pill badge-secondary" *ngIf="channel.status === 0">Inactive</span>
|
||||
<span class="badge rounded-pill badge-success" *ngIf="channel.status === 1">Active</span>
|
||||
<span class="badge rounded-pill badge-danger" *ngIf="channel.status === 2">Closed</span>
|
||||
<span class="badge rounded-pill badge-secondary" *ngIf="channel.status === 0" i18n="status.inactive">Inactive</span>
|
||||
<span class="badge rounded-pill badge-success" *ngIf="channel.status === 1" i18n="status.active">Active</span>
|
||||
<span class="badge rounded-pill badge-danger" *ngIf="channel.status === 2" i18n="status.closed">Closed</span>
|
||||
<app-closing-type *ngIf="channel.closing_reason" [type]="channel.closing_reason"></app-closing-type>
|
||||
</div>
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td i18n="address.total-received">Capacity</td>
|
||||
<td i18n="lightning.capacity">Capacity</td>
|
||||
<td><app-sats [satoshis]="channel.capacity"></app-sats><app-fiat [value]="channel.capacity" digitsInfo="1.0-0"></app-fiat></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -70,7 +70,7 @@
|
||||
<ng-container *ngIf="transactions$ | async as transactions">
|
||||
<ng-template [ngIf]="transactions[0]">
|
||||
<div class="d-flex">
|
||||
<h3>Opening transaction</h3>
|
||||
<h3 i18n="lightning.opening-transaction">Opening transaction</h3>
|
||||
<button type="button" class="btn btn-outline-info details-button btn-sm" (click)="txList1.toggleDetails()"
|
||||
i18n="transaction.details|Transaction Details">Details</button>
|
||||
</div>
|
||||
@@ -79,7 +79,7 @@
|
||||
</ng-template>
|
||||
<ng-template [ngIf]="transactions[1]">
|
||||
<div class="closing-header d-flex">
|
||||
<h3 style="margin: 0;">Closing transaction</h3> <app-closing-type [type]="channel.closing_reason">
|
||||
<h3 style="margin: 0;" i18n="lightning.closing-transaction">Closing transaction</h3> <app-closing-type [type]="channel.closing_reason">
|
||||
</app-closing-type>
|
||||
<button type="button" class="btn btn-outline-info details-button btn-sm" (click)="txList2.toggleDetails()"
|
||||
i18n="transaction.details|Transaction Details">Details</button>
|
||||
|
||||
@@ -34,7 +34,7 @@ export class ChannelComponent implements OnInit {
|
||||
return this.lightningApiService.getChannel$(params.get('short_id'))
|
||||
.pipe(
|
||||
tap((value) => {
|
||||
this.seoService.setTitle(`Channel: ${value.short_id}`);
|
||||
this.seoService.setTitle($localize`Channel: ${value.short_id}`);
|
||||
}),
|
||||
catchError((err) => {
|
||||
this.error = err;
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
<form [formGroup]="channelStatusForm" class="formRadioGroup">
|
||||
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="status">
|
||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
||||
<input ngbButton type="radio" [value]="'open'" fragment="open" i18n="open">Open
|
||||
<input ngbButton type="radio" [value]="'open'" fragment="open"><span i18n="open">Open</span>
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
||||
<input ngbButton type="radio" [value]="'closed'" fragment="closed" i18n="closed">Closed
|
||||
<input ngbButton type="radio" [value]="'closed'" fragment="closed"><span i18n="closed">Closed</span>
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
@@ -32,12 +32,12 @@
|
||||
|
||||
<ng-template #tableHeader>
|
||||
<thead>
|
||||
<th class="alias text-left" i18n="nodes.alias">Node Alias</th>
|
||||
<th class="alias text-left d-none d-md-table-cell" i18n="channels.transaction"> </th>
|
||||
<th class="alias text-left" i18n="lightning.alias">Alias</th>
|
||||
<th class="alias text-left d-none d-md-table-cell"> </th>
|
||||
<th class="alias text-left d-none d-md-table-cell" i18n="status">Status</th>
|
||||
<th *ngIf="status !== 'closed'" class="channels text-left d-none d-md-table-cell" i18n="channels.rate">Fee Rate</th>
|
||||
<th *ngIf="status !== 'closed'" class="channels text-left d-none d-md-table-cell" i18n="transaction.fee-rate|Transaction fee rate">Fee rate</th>
|
||||
<th *ngIf="status === 'closed'" class="channels text-left d-none d-md-table-cell" i18n="channels.closing_date">Closing date</th>
|
||||
<th class="capacity text-right d-none d-md-table-cell" i18n="nodes.capacity">Capacity</th>
|
||||
<th class="capacity text-right d-none d-md-table-cell" i18n="lightning.capacity">Capacity</th>
|
||||
<th class="capacity text-right" i18n="channels.id">Channel ID</th>
|
||||
</thead>
|
||||
</ng-template>
|
||||
@@ -53,7 +53,7 @@
|
||||
</div>
|
||||
</td>
|
||||
<td class="alias text-left d-none d-md-table-cell">
|
||||
<div class="second-line">{{ node.channels }} channels</div>
|
||||
<div class="second-line"><ng-container *ngTemplateOutlet="xChannels; context: {$implicit: node.channels }"></ng-container></div>
|
||||
<div class="second-line">
|
||||
<app-amount *ngIf="node.capacity > 100000000; else smallnode" [satoshis]="node.capacity" [digitsInfo]="'1.2-2'" [noFiat]="true"></app-amount>
|
||||
<ng-template #smallnode>
|
||||
@@ -63,10 +63,10 @@
|
||||
</div>
|
||||
</td>
|
||||
<td class="d-none d-md-table-cell">
|
||||
<span class="badge rounded-pill badge-secondary" *ngIf="channel.status === 0" i18n="lightning.inactive">Inactive</span>
|
||||
<span class="badge rounded-pill badge-success" *ngIf="channel.status === 1" i18n="lightning.active">Active</span>
|
||||
<span class="badge rounded-pill badge-secondary" *ngIf="channel.status === 0" i18n="status.inactive">Inactive</span>
|
||||
<span class="badge rounded-pill badge-success" *ngIf="channel.status === 1" i18n="status.active">Active</span>
|
||||
<ng-template [ngIf]="channel.status === 2">
|
||||
<span class="badge rounded-pill badge-secondary" *ngIf="!channel.closing_reason; else closingReason" i18n="lightning.closed">Closed</span>
|
||||
<span class="badge rounded-pill badge-secondary" *ngIf="!channel.closing_reason; else closingReason" i18n="status.closed">Closed</span>
|
||||
<ng-template #closingReason>
|
||||
<app-closing-type [type]="channel.closing_reason"></app-closing-type>
|
||||
</ng-template>
|
||||
@@ -117,3 +117,5 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #xChannels let-i i18n="lightning.x-channels">{{ i }} channels</ng-template>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<div class="widget-toggler">
|
||||
<a href="javascript:;" (click)="switchMode('avg')" class="toggler-option"
|
||||
[ngClass]="{'inactive': mode !== 'avg'}"><small>avg</small></a>
|
||||
<a href="" (click)="switchMode('avg')" class="toggler-option"
|
||||
[ngClass]="{'inactive': mode === 'avg'}"><small>avg</small></a>
|
||||
<span style="color: #ffffff66; font-size: 8px"> | </span>
|
||||
<a href="javascript:;" (click)="switchMode('med')" class="toggler-option"
|
||||
[ngClass]="{'inactive': mode !== 'med'}"><small>med</small></a>
|
||||
<a href="" (click)="switchMode('med')" class="toggler-option"
|
||||
[ngClass]="{'inactive': mode === 'med'}"><small>med</small></a>
|
||||
</div>
|
||||
|
||||
<div class="fee-estimation-wrapper" *ngIf="statistics$ | async as statistics; else loadingReward">
|
||||
@@ -14,7 +14,7 @@
|
||||
<div class="card-text">
|
||||
<div class="fee-text" [class]="!statistics.previous ? 'no-border' : ''">
|
||||
{{ statistics.latest?.avg_capacity || 0 | number: '1.0-0' }}
|
||||
<span i18n="shared.sat-vbyte|sat/vB">sats</span>
|
||||
<span i18n="shared.sats">sats</span>
|
||||
</div>
|
||||
<span class="fiat" *ngIf="statistics.previous">
|
||||
<app-change [current]="statistics.latest?.avg_capacity" [previous]="statistics.previous?.avg_capacity"></app-change>
|
||||
@@ -29,7 +29,7 @@
|
||||
placement="bottom">
|
||||
<div class="fee-text" [class]="!statistics.previous ? 'no-border' : ''">
|
||||
{{ statistics.latest?.avg_fee_rate || 0 | number: '1.0-0' }}
|
||||
<span i18n="shared.sat-vbyte|sat/vB">ppm</span>
|
||||
<span i18n="lightning.ppm">ppm</span>
|
||||
</div>
|
||||
<span class="fiat" *ngIf="statistics.previous">
|
||||
<app-change [current]="statistics.latest?.avg_fee_rate" [previous]="statistics.previous?.avg_fee_rate"></app-change>
|
||||
@@ -44,7 +44,7 @@
|
||||
<div class="card-text">
|
||||
<div class="fee-text" [class]="!statistics.previous ? 'no-border' : ''">
|
||||
{{ statistics.latest?.avg_base_fee_mtokens || 0 | number: '1.0-0' }}
|
||||
<span i18n="shared.sat-vbyte|sat/vB">msats</span>
|
||||
<span i18n="shared.m-sats">mSats</span>
|
||||
</div>
|
||||
<span class="fiat" *ngIf="statistics.previous">
|
||||
<app-change [current]="statistics.latest?.avg_base_fee_mtokens" [previous]="statistics.previous?.avg_base_fee_mtokens"></app-change>
|
||||
@@ -60,7 +60,7 @@
|
||||
<div class="card-text">
|
||||
<div class="fee-text" [class]="!statistics.previous ? 'no-border' : ''">
|
||||
{{ statistics.latest?.med_capacity || 0 | number: '1.0-0' }}
|
||||
<span i18n="shared.sat-vbyte|sat/vB">sats</span>
|
||||
<span i18n="shared.sats">sats</span>
|
||||
</div>
|
||||
<span class="fiat" *ngIf="statistics.previous">
|
||||
<app-change [current]="statistics.latest?.med_capacity" [previous]="statistics.previous?.med_capacity"></app-change>
|
||||
@@ -75,7 +75,7 @@
|
||||
placement="bottom">
|
||||
<div class="fee-text" [class]="!statistics.previous ? 'no-border' : ''">
|
||||
{{ statistics.latest?.med_fee_rate || 0 | number: '1.0-0' }}
|
||||
<span i18n="shared.sat-vbyte|sat/vB">ppm</span>
|
||||
<span i18n="lightning.ppm">ppm</span>
|
||||
</div>
|
||||
<span class="fiat" *ngIf="statistics.previous">
|
||||
<app-change [current]="statistics.latest?.med_fee_rate" [previous]="statistics.previous?.med_fee_rate"></app-change>
|
||||
@@ -90,7 +90,7 @@
|
||||
<div class="card-text">
|
||||
<div class="fee-text" [class]="!statistics.previous ? 'no-border' : ''">
|
||||
{{ statistics.latest?.med_base_fee_mtokens || 0 | number: '1.0-0' }}
|
||||
<span i18n="shared.sat-vbyte|sat/vB">msats</span>
|
||||
<span i18n="shared.m-sats">mSats</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="fiat" *ngIf="statistics.previous">
|
||||
|
||||
@@ -18,5 +18,6 @@ export class ChannelsStatisticsComponent implements OnInit {
|
||||
|
||||
switchMode(mode: 'avg' | 'med') {
|
||||
this.mode = mode;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div class="box preview-box" *ngIf="nodes$ | async as nodes">
|
||||
<app-preview-title>
|
||||
<span i18n="lightning.node">Lightning node group</span>
|
||||
<span i18n="lightning.node-group">Lightning node group</span>
|
||||
</app-preview-title>
|
||||
<div class="row d-flex justify-content-between full-width-row">
|
||||
<div class="logo-wrapper">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div class="container-xl full-height" style="min-height: 335px">
|
||||
<h5 class="mb-1" style="color: #ffffff66" i18n="lightning.node">Lightning node group</h5>
|
||||
<h5 class="mb-1" style="color: #ffffff66" i18n="lightning.node-group">Lightning node group</h5>
|
||||
|
||||
<div class="header">
|
||||
<div class="logo-container">
|
||||
|
||||
@@ -53,6 +53,10 @@ export class LightningApiService {
|
||||
return this.httpClient.get<any>(this.apiBasePath + '/api/v1/lightning/nodes/' + publicKey + '/statistics');
|
||||
}
|
||||
|
||||
getNodeFeeHistogram$(publicKey: string): Observable<any> {
|
||||
return this.httpClient.get<any>(this.apiBasePath + '/api/v1/lightning/nodes/' + publicKey + '/fees/histogram');
|
||||
}
|
||||
|
||||
getNodesRanking$(): Observable<INodesRanking> {
|
||||
return this.httpClient.get<INodesRanking>(this.apiBasePath + '/api/v1/lightning/nodes/rankings');
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<!-- Network capacity/channels/nodes -->
|
||||
<div class="col">
|
||||
<div class="main-title">
|
||||
<span i18n="lightning.statistics-title">Network Statistics</span>
|
||||
<span i18n="lightning.network-statistics-title">Network Statistics</span>
|
||||
</div>
|
||||
<div class="card-wrapper">
|
||||
<div class="card" style="height: 123px">
|
||||
@@ -21,7 +21,7 @@
|
||||
<!-- Channels stats -->
|
||||
<div class="col">
|
||||
<div class="main-title">
|
||||
<span i18n="lightning.statistics-title">Channels Statistics</span>
|
||||
<span i18n="lightning.channel-statistics-title">Channels Statistics</span>
|
||||
</div>
|
||||
<div class="card-wrapper">
|
||||
<div class="card" style="height: 123px">
|
||||
@@ -46,7 +46,7 @@
|
||||
<div class="col">
|
||||
<div class="card graph-card">
|
||||
<div class="card-body pl-2 pr-2 pt-1">
|
||||
<h5 class="card-title mt-3" i18n="lightning.network-history">Lightning network history</h5>
|
||||
<h5 class="card-title mt-3" i18n="lightning.network-history">Lightning Network History</h5>
|
||||
<app-lightning-statistics-chart [widget]=true></app-lightning-statistics-chart>
|
||||
<app-nodes-networks-chart [widget]=true></app-nodes-networks-chart>
|
||||
<div><a [routerLink]="['/graphs/lightning/nodes-networks' | relativeUrl]" i18n="dashboard.view-more">View more »</a></div>
|
||||
@@ -59,7 +59,7 @@
|
||||
<div class="card" style="height: 409px">
|
||||
<div class="card-body">
|
||||
<a class="title-link" href="" [routerLink]="['/lightning/nodes/rankings/liquidity' | relativeUrl]">
|
||||
<h5 class="card-title d-inline" i18n="lightning.liquidity-ranking">Liquidity ranking</h5>
|
||||
<h5 class="card-title d-inline" i18n="lightning.liquidity-ranking">Liquidity Ranking</h5>
|
||||
<span> </span>
|
||||
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: 'text-top'; font-size: 13px; color: '#4a68b9'"></fa-icon>
|
||||
</a>
|
||||
@@ -73,7 +73,7 @@
|
||||
<div class="card" style="height: 409px">
|
||||
<div class="card-body">
|
||||
<a class="title-link" href="" [routerLink]="['/lightning/nodes/rankings/connectivity' | relativeUrl]">
|
||||
<h5 class="card-title d-inline" i18n="lightning.connectivity-ranking">Connectivity ranking</h5>
|
||||
<h5 class="card-title d-inline" i18n="lightning.connectivity-ranking">Connectivity Ranking</h5>
|
||||
<span> </span>
|
||||
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: 'text-top'; font-size: 13px; color: '#4a68b9'"></fa-icon>
|
||||
</a>
|
||||
|
||||
@@ -24,7 +24,7 @@ export class LightningDashboardComponent implements OnInit {
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.seoService.setTitle($localize`Lightning Network`);
|
||||
this.seoService.setTitle($localize`:@@142e923d3b04186ac6ba23387265d22a2fa404e0:Lightning Explorer`);
|
||||
|
||||
this.nodesRanking$ = this.lightningApiService.getNodesRanking$().pipe(share());
|
||||
this.statistics$ = this.lightningApiService.getLatestStatistics$().pipe(share());
|
||||
|
||||
@@ -15,6 +15,7 @@ import { ChannelBoxComponent } from './channel/channel-box/channel-box.component
|
||||
import { ClosingTypeComponent } from './channel/closing-type/closing-type.component';
|
||||
import { LightningStatisticsChartComponent } from './statistics-chart/lightning-statistics-chart.component';
|
||||
import { NodeStatisticsChartComponent } from './node-statistics-chart/node-statistics-chart.component';
|
||||
import { NodeFeeChartComponent } from './node-fee-chart/node-fee-chart.component';
|
||||
import { GraphsModule } from '../graphs/graphs.module';
|
||||
import { NodesNetworksChartComponent } from './nodes-networks-chart/nodes-networks-chart.component';
|
||||
import { ChannelsStatisticsComponent } from './channels-statistics/channels-statistics.component';
|
||||
@@ -38,6 +39,7 @@ import { GroupComponent } from './group/group.component';
|
||||
NodesListComponent,
|
||||
NodeStatisticsComponent,
|
||||
NodeStatisticsChartComponent,
|
||||
NodeFeeChartComponent,
|
||||
NodeComponent,
|
||||
ChannelsListComponent,
|
||||
ChannelComponent,
|
||||
@@ -73,6 +75,7 @@ import { GroupComponent } from './group/group.component';
|
||||
NodesListComponent,
|
||||
NodeStatisticsComponent,
|
||||
NodeStatisticsChartComponent,
|
||||
NodeFeeChartComponent,
|
||||
NodeComponent,
|
||||
ChannelsListComponent,
|
||||
ChannelComponent,
|
||||
|
||||
@@ -21,10 +21,12 @@ const routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: 'node/:public_key',
|
||||
data: { networkSpecific: true },
|
||||
component: NodeComponent,
|
||||
},
|
||||
{
|
||||
path: 'channel/:short_id',
|
||||
data: { networkSpecific: true },
|
||||
component: ChannelComponent,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<div class="full-container">
|
||||
<h2 i18n="lightning.node-fee-distribution">Fee distribution</h2>
|
||||
<div class="chart" echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartInit)="onChartInit($event)"></div>
|
||||
<div class="text-center loadingGraphs" *ngIf="isLoading">
|
||||
<div class="spinner-border text-light"></div>d
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,5 @@
|
||||
.full-container {
|
||||
margin-top: 25px;
|
||||
margin-bottom: 25px;
|
||||
min-height: 100%;
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
import { Component, Inject, Input, LOCALE_ID, OnInit, HostBinding } from '@angular/core';
|
||||
import { EChartsOption } from 'echarts';
|
||||
import { switchMap } from 'rxjs/operators';
|
||||
import { download } from '../../shared/graphs.utils';
|
||||
import { LightningApiService } from '../lightning-api.service';
|
||||
import { ActivatedRoute, ParamMap } from '@angular/router';
|
||||
import { AmountShortenerPipe } from '../../shared/pipes/amount-shortener.pipe';
|
||||
|
||||
@Component({
|
||||
selector: 'app-node-fee-chart',
|
||||
templateUrl: './node-fee-chart.component.html',
|
||||
styleUrls: ['./node-fee-chart.component.scss'],
|
||||
styles: [`
|
||||
.loadingGraphs {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: calc(50% - 15px);
|
||||
z-index: 100;
|
||||
}
|
||||
`],
|
||||
})
|
||||
export class NodeFeeChartComponent implements OnInit {
|
||||
chartOptions: EChartsOption = {};
|
||||
chartInitOptions = {
|
||||
renderer: 'svg',
|
||||
};
|
||||
|
||||
@HostBinding('attr.dir') dir = 'ltr';
|
||||
|
||||
isLoading = true;
|
||||
chartInstance: any = undefined;
|
||||
|
||||
constructor(
|
||||
@Inject(LOCALE_ID) public locale: string,
|
||||
private lightningApiService: LightningApiService,
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private amountShortenerPipe: AmountShortenerPipe,
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
||||
this.activatedRoute.paramMap
|
||||
.pipe(
|
||||
switchMap((params: ParamMap) => {
|
||||
this.isLoading = true;
|
||||
return this.lightningApiService.getNodeFeeHistogram$(params.get('public_key'));
|
||||
}),
|
||||
).subscribe((data) => {
|
||||
if (data && data.incoming && data.outgoing) {
|
||||
const outgoingHistogram = this.bucketsToHistogram(data.outgoing);
|
||||
const incomingHistogram = this.bucketsToHistogram(data.incoming);
|
||||
this.prepareChartOptions(outgoingHistogram, incomingHistogram);
|
||||
}
|
||||
this.isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
bucketsToHistogram(buckets): { label: string, count: number, capacity: number}[] {
|
||||
const histogram = [];
|
||||
let increment = 1;
|
||||
let lower = -increment;
|
||||
let upper = 0;
|
||||
|
||||
let nullBucket;
|
||||
if (buckets.length && buckets[0] && buckets[0].bucket == null) {
|
||||
nullBucket = buckets.shift();
|
||||
}
|
||||
|
||||
while (upper <= 5000) {
|
||||
let bucket;
|
||||
if (buckets.length && buckets[0] && upper >= Number(buckets[0].bucket)) {
|
||||
bucket = buckets.shift();
|
||||
}
|
||||
histogram.push({
|
||||
label: upper === 0 ? '0 ppm' : `${lower} - ${upper} ppm`,
|
||||
count: Number(bucket?.count || 0) + (upper === 0 ? Number(nullBucket?.count || 0) : 0),
|
||||
capacity: Number(bucket?.capacity || 0) + (upper === 0 ? Number(nullBucket?.capacity || 0) : 0),
|
||||
});
|
||||
|
||||
if (upper >= increment * 10) {
|
||||
increment *= 10;
|
||||
lower = increment;
|
||||
upper = increment + increment;
|
||||
} else {
|
||||
lower += increment;
|
||||
upper += increment;
|
||||
}
|
||||
}
|
||||
const rest = buckets.reduce((acc, bucket) => {
|
||||
acc.count += Number(bucket.count);
|
||||
acc.capacity += Number(bucket.capacity);
|
||||
return acc;
|
||||
}, { count: 0, capacity: 0 });
|
||||
histogram.push({
|
||||
label: `5000+ ppm`,
|
||||
count: rest.count,
|
||||
capacity: rest.capacity,
|
||||
});
|
||||
return histogram;
|
||||
}
|
||||
|
||||
prepareChartOptions(outgoingData, incomingData): void {
|
||||
let title: object;
|
||||
if (outgoingData.length === 0) {
|
||||
title = {
|
||||
textStyle: {
|
||||
color: 'grey',
|
||||
fontSize: 15
|
||||
},
|
||||
text: $localize`No data to display yet. Try again later.`,
|
||||
left: 'center',
|
||||
top: 'center'
|
||||
};
|
||||
}
|
||||
|
||||
this.chartOptions = {
|
||||
title: outgoingData.length === 0 ? title : undefined,
|
||||
animation: false,
|
||||
grid: {
|
||||
top: 30,
|
||||
bottom: 20,
|
||||
right: 20,
|
||||
left: 65,
|
||||
},
|
||||
tooltip: {
|
||||
show: !this.isMobile(),
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'line'
|
||||
},
|
||||
backgroundColor: 'rgba(17, 19, 31, 1)',
|
||||
borderRadius: 4,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.5)',
|
||||
textStyle: {
|
||||
color: '#b1b1b1',
|
||||
align: 'left',
|
||||
},
|
||||
borderColor: '#000',
|
||||
formatter: (ticks): string => {
|
||||
return `
|
||||
<b style="color: white; margin-left: 2px">${ticks[0].data.label}</b><br>
|
||||
<br>
|
||||
<b style="color: white; margin-left: 2px">${ticks[0].marker} Outgoing</b><br>
|
||||
<span>Capacity: ${this.amountShortenerPipe.transform(ticks[0].data.capacity, 2, undefined, true)} sats</span><br>
|
||||
<span>Channels: ${ticks[0].data.count}</span><br>
|
||||
<br>
|
||||
<b style="color: white; margin-left: 2px">${ticks[1].marker} Incoming</b><br>
|
||||
<span>Capacity: ${this.amountShortenerPipe.transform(ticks[1].data.capacity, 2, undefined, true)} sats</span><br>
|
||||
<span>Channels: ${ticks[1].data.count}</span><br>
|
||||
`;
|
||||
}
|
||||
},
|
||||
xAxis: outgoingData.length === 0 ? undefined : {
|
||||
type: 'category',
|
||||
axisLine: { onZero: true },
|
||||
axisLabel: {
|
||||
align: 'center',
|
||||
fontSize: 11,
|
||||
lineHeight: 12,
|
||||
hideOverlap: true,
|
||||
padding: [0, 5],
|
||||
},
|
||||
data: outgoingData.map(bucket => bucket.label)
|
||||
},
|
||||
legend: outgoingData.length === 0 ? undefined : {
|
||||
padding: 10,
|
||||
data: [
|
||||
{
|
||||
name: 'Outgoing Fees',
|
||||
inactiveColor: 'rgb(110, 112, 121)',
|
||||
textStyle: {
|
||||
color: 'white',
|
||||
},
|
||||
icon: 'roundRect',
|
||||
},
|
||||
{
|
||||
name: 'Incoming Fees',
|
||||
inactiveColor: 'rgb(110, 112, 121)',
|
||||
textStyle: {
|
||||
color: 'white',
|
||||
},
|
||||
icon: 'roundRect',
|
||||
},
|
||||
],
|
||||
},
|
||||
yAxis: outgoingData.length === 0 ? undefined : [
|
||||
{
|
||||
type: 'value',
|
||||
axisLabel: {
|
||||
color: 'rgb(110, 112, 121)',
|
||||
formatter: (val) => {
|
||||
return `${this.amountShortenerPipe.transform(Math.abs(val), 2, undefined, true)} sats`;
|
||||
}
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
type: 'dotted',
|
||||
color: '#ffffff66',
|
||||
opacity: 0.25,
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
series: outgoingData.length === 0 ? undefined : [
|
||||
{
|
||||
zlevel: 0,
|
||||
name: 'Outgoing Fees',
|
||||
data: outgoingData.map(bucket => ({
|
||||
value: bucket.capacity,
|
||||
label: bucket.label,
|
||||
capacity: bucket.capacity,
|
||||
count: bucket.count,
|
||||
})),
|
||||
type: 'bar',
|
||||
barWidth: '90%',
|
||||
barMaxWidth: 50,
|
||||
stack: 'fees',
|
||||
},
|
||||
{
|
||||
zlevel: 0,
|
||||
name: 'Incoming Fees',
|
||||
data: incomingData.map(bucket => ({
|
||||
value: -bucket.capacity,
|
||||
label: bucket.label,
|
||||
capacity: bucket.capacity,
|
||||
count: bucket.count,
|
||||
})),
|
||||
type: 'bar',
|
||||
barWidth: '90%',
|
||||
barMaxWidth: 50,
|
||||
stack: 'fees',
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
onChartInit(ec) {
|
||||
if (this.chartInstance !== undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.chartInstance = ec;
|
||||
}
|
||||
|
||||
isMobile() {
|
||||
return (window.innerWidth <= 767.98);
|
||||
}
|
||||
|
||||
onSaveChart() {
|
||||
// @ts-ignore
|
||||
const prevBottom = this.chartOptions.grid.bottom;
|
||||
// @ts-ignore
|
||||
this.chartOptions.grid.bottom = 40;
|
||||
this.chartOptions.backgroundColor = '#11131f';
|
||||
this.chartInstance.setOption(this.chartOptions);
|
||||
download(this.chartInstance.getDataURL({
|
||||
pixelRatio: 2,
|
||||
}), `node-fee-chart.svg`);
|
||||
// @ts-ignore
|
||||
this.chartOptions.grid.bottom = prevBottom;
|
||||
this.chartOptions.backgroundColor = 'none';
|
||||
this.chartInstance.setOption(this.chartOptions);
|
||||
}
|
||||
}
|
||||
@@ -37,8 +37,9 @@
|
||||
}
|
||||
&.more-padding {
|
||||
padding-top: 10px;
|
||||
}
|
||||
&:first-child{
|
||||
}
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
display: none;
|
||||
@media (min-width: 485px) {
|
||||
display: block;
|
||||
@@ -48,10 +49,7 @@
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
.card-text span {
|
||||
color: #ffffff66;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div class="box preview-box" *ngIf="(node$ | async) as node">
|
||||
<app-preview-title>
|
||||
<span i18n="lightning.node">lightning node</span>
|
||||
<span i18n="lightning.node">Lightning node</span>
|
||||
</app-preview-title>
|
||||
<div class="row d-flex justify-content-between full-width-row">
|
||||
<h1 class="title"></h1>
|
||||
@@ -29,13 +29,13 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="lightning.active-channels-avg">Average size</td>
|
||||
<td i18n="lightning.avg-size">Average size</td>
|
||||
<td>
|
||||
<app-amount [satoshis]="node.avgCapacity" [noFiat]="true"></app-amount>
|
||||
</td>
|
||||
</tr>
|
||||
<tr *ngIf="node.city">
|
||||
<td i18n="location">Location</td>
|
||||
<td i18n="lightning.location">Location</td>
|
||||
<td>
|
||||
<span>{{ node.city.en }}</span>
|
||||
</td>
|
||||
@@ -47,7 +47,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr *ngIf="!node.city && !node.country">
|
||||
<td i18n="location">Location</td>
|
||||
<td i18n="lightning.location">Location</td>
|
||||
<td>
|
||||
<span>unknown</span>
|
||||
</td>
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
|
||||
<div *ngIf="error" class="d-flex flex-column justify-content-around align-items-center mt-5 w-100" style="min-height: 100px">
|
||||
<span i18n="lightning.node-not-found">No node found for public key "{{ node.public_key | shortenString : 12}}"</span>
|
||||
<a [routerLink]="['/lightning' | relativeUrl]" i18n="lightning.back-to-lightning-dashboard">Back to the lightning dashboard</a>
|
||||
</div>
|
||||
|
||||
<div class="box" *ngIf="!error">
|
||||
@@ -45,7 +44,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="location" class="text-truncate">Location</td>
|
||||
<td i18n="lightning.location" class="text-truncate">Location</td>
|
||||
<td *ngIf="node.geolocation">
|
||||
<app-geolocation [data]="node.geolocation" [type]="'node'"></app-geolocation>
|
||||
</td>
|
||||
@@ -61,19 +60,19 @@
|
||||
<table class="table table-borderless table-striped table-fixed">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td i18n="address.total-received" class="text-truncate label">First seen</td>
|
||||
<td i18n="transaction.first-seen|Transaction first seen" class="text-truncate label">First seen</td>
|
||||
<td>
|
||||
<app-timestamp [unixTime]="node.first_seen"></app-timestamp>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="address.total-sent" class="text-truncate label">Last update</td>
|
||||
<td class="text-truncate label" i18n="lightning.last_update">Last update</td>
|
||||
<td>
|
||||
<app-timestamp [unixTime]="node.updated_at"></app-timestamp>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="address.balance" class="text-truncate label">Color</td>
|
||||
<td i18n="lightning.color" class="text-truncate label">Color</td>
|
||||
<td>
|
||||
<div [ngStyle]="{'color': node.color}">{{ node.color }}</div>
|
||||
</td>
|
||||
@@ -141,6 +140,8 @@
|
||||
|
||||
<app-node-channels style="display:block;margin-bottom: 40px" [publicKey]="node.public_key"></app-node-channels>
|
||||
|
||||
<app-node-fee-chart style="display:block;margin-bottom: 40px"></app-node-fee-chart>
|
||||
|
||||
<div class="d-flex">
|
||||
<h2 *ngIf="channelsListStatus === 'open'">
|
||||
<span i18n="lightning.open-channels">Open channels</span>
|
||||
|
||||
@@ -39,7 +39,7 @@ export class NodeComponent implements OnInit {
|
||||
return this.lightningApiService.getNode$(params.get('public_key'));
|
||||
}),
|
||||
map((node) => {
|
||||
this.seoService.setTitle(`Node: ${node.alias}`);
|
||||
this.seoService.setTitle($localize`Node: ${node.alias}`);
|
||||
|
||||
const socketsObject = [];
|
||||
for (const socket of node.sockets.split(',')) {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div *ngIf="chartOptions" [class]="'full-container ' + style + (fitContainer ? ' fit-container' : '')">
|
||||
<div *ngIf="style === 'graph'" class="card-header">
|
||||
<div class="d-flex d-md-block align-items-baseline" style="margin-bottom: -5px">
|
||||
<span i18n="lightning.nodes-channels-world-map">Lightning nodes channels world map</span>
|
||||
<span i18n="lightning.nodes-channels-world-map">Lightning Nodes Channels World Map</span>
|
||||
</div>
|
||||
<small style="color: #ffffff66" i18n="lightning.tor-nodes-excluded">(Tor nodes excluded)</small>
|
||||
</div>
|
||||
|
||||
@@ -66,7 +66,7 @@ export class NodesChannelsMap implements OnInit {
|
||||
}
|
||||
|
||||
if (this.style === 'graph') {
|
||||
this.seoService.setTitle($localize`Lightning nodes channels world map`);
|
||||
this.seoService.setTitle($localize`Lightning Nodes Channels World Map`);
|
||||
}
|
||||
|
||||
if (['nodepage', 'channelpage'].includes(this.style)) {
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
<table class="table table-borderless">
|
||||
<thead>
|
||||
<th class="alias text-left" i18n="nodes.alias">Alias</th>
|
||||
<th class="capacity text-right" [class]="show" i18n="node.capacity">Capacity</th>
|
||||
<th class="channels text-right" [class]="show" i18n="node.channels">Channels</th>
|
||||
<th class="capacity text-right" [class]="show" i18n="lightning.capacity">Capacity</th>
|
||||
<th class="channels text-right" [class]="show" i18n="lightning.channels">Channels</th>
|
||||
</thead>
|
||||
<tbody *ngIf="nodes$ | async as nodes; else skeleton">
|
||||
<tr *ngFor="let node of nodes; let i = index;">
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<div *ngIf="!widget" class="card-header">
|
||||
<div class="d-flex d-md-block align-items-baseline" style="margin-bottom: -5px">
|
||||
<span i18n="lightning.nodes-world-map">Lightning nodes world map</span>
|
||||
<span i18n="lightning.nodes-world-map">Lightning Nodes World Map</span>
|
||||
</div>
|
||||
<small style="color: #ffffff66" i18n="lightning.tor-nodes-excluded">(Tor nodes excluded)</small>
|
||||
</div>
|
||||
|
||||
@@ -47,7 +47,7 @@ export class NodesMap implements OnInit, OnChanges {
|
||||
|
||||
ngOnInit(): void {
|
||||
if (!this.widget) {
|
||||
this.seoService.setTitle($localize`Lightning nodes world map`);
|
||||
this.seoService.setTitle($localize`:@@af8560ca50882114be16c951650f83bca73161a7:Lightning Nodes World Map`);
|
||||
}
|
||||
|
||||
if (!this.inputNodes$) {
|
||||
@@ -141,7 +141,7 @@ export class NodesMap implements OnInit, OnChanges {
|
||||
color: 'grey',
|
||||
fontSize: 15
|
||||
},
|
||||
text: $localize`No data to display yet`,
|
||||
text: $localize`No data to display yet. Try again later.`,
|
||||
left: 'center',
|
||||
top: 'center'
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<div class="card-header mb-0 mb-md-4" [style]="widget ? 'display:none' : ''">
|
||||
<div class="d-flex d-md-block align-items-baseline">
|
||||
<span i18n="lightning.nodes-networks">Lightning nodes per network</span>
|
||||
<span i18n="lightning.nodes-networks">Lightning Nodes Per Network</span>
|
||||
<button class="btn p-0 pl-2" style="margin: 0 0 4px 0px" (click)="onSaveChart()">
|
||||
<fa-icon [icon]="['fas', 'download']" [fixedWidth]="true"></fa-icon>
|
||||
</button>
|
||||
|
||||
@@ -64,7 +64,7 @@ export class NodesNetworksChartComponent implements OnInit {
|
||||
if (this.widget) {
|
||||
this.miningWindowPreference = '3y';
|
||||
} else {
|
||||
this.seoService.setTitle($localize`Lightning nodes per network`);
|
||||
this.seoService.setTitle($localize`:@@b420668a91f8ebaf6e6409c4ba87f1d45961d2bd:Lightning Nodes Per Network`);
|
||||
this.miningWindowPreference = this.miningService.getDefaultTimespan('all');
|
||||
}
|
||||
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
|
||||
@@ -128,7 +128,7 @@ export class NodesNetworksChartComponent implements OnInit {
|
||||
color: 'grey',
|
||||
fontSize: 11
|
||||
},
|
||||
text: $localize`Nodes per network`,
|
||||
text: $localize`:@@b420668a91f8ebaf6e6409c4ba87f1d45961d2bd:Lightning Nodes Per Network`,
|
||||
left: 'center',
|
||||
top: 11,
|
||||
zlevel: 10,
|
||||
@@ -139,7 +139,7 @@ export class NodesNetworksChartComponent implements OnInit {
|
||||
{
|
||||
zlevel: 1,
|
||||
yAxisIndex: 0,
|
||||
name: $localize`Unknown`,
|
||||
name: $localize`:@@e5d8bb389c702588877f039d72178f219453a72d:Unknown`,
|
||||
showSymbol: false,
|
||||
symbol: 'none',
|
||||
data: data.unannounced_nodes,
|
||||
@@ -308,7 +308,7 @@ export class NodesNetworksChartComponent implements OnInit {
|
||||
icon: 'roundRect',
|
||||
},
|
||||
{
|
||||
name: $localize`Unknown`,
|
||||
name: $localize`:@@e5d8bb389c702588877f039d72178f219453a72d:Unknown`,
|
||||
inactiveColor: 'rgb(110, 112, 121)',
|
||||
textStyle: {
|
||||
color: 'white',
|
||||
@@ -320,7 +320,7 @@ export class NodesNetworksChartComponent implements OnInit {
|
||||
'$localize`Reachable on Darknet Only`': true,
|
||||
'$localize`Reachable on Clearnet Only`': true,
|
||||
'$localize`Reachable on Clearnet and Darknet`': true,
|
||||
'$localize`Unknown`': true,
|
||||
'$localize`:@@e5d8bb389c702588877f039d72178f219453a72d:Unknown`': true,
|
||||
}
|
||||
},
|
||||
yAxis: data.tor_nodes.length === 0 ? undefined : [
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<div class="card-header">
|
||||
<div class="d-flex d-md-block align-items-baseline" style="margin-bottom: -5px">
|
||||
<span i18n="lightning.nodes-per-country">Lightning nodes per country</span>
|
||||
<span i18n="lightning.nodes-per-country">Lightning Nodes Per Country</span>
|
||||
<button class="btn p-0 pl-2" style="margin: 0 0 4px 0px" (click)="onSaveChart()">
|
||||
<fa-icon [icon]="['fas', 'download']" [fixedWidth]="true"></fa-icon>
|
||||
</button>
|
||||
|
||||
@@ -43,7 +43,7 @@ export class NodesPerCountryChartComponent implements OnInit {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.seoService.setTitle($localize`Lightning nodes per country`);
|
||||
this.seoService.setTitle($localize`:@@9d3ad4c6623870d96b65fb7a708fed6ce7c20044:Lightning Nodes Per Country`);
|
||||
|
||||
this.nodesPerCountryObservable$ = this.apiService.getNodesPerCountry$()
|
||||
.pipe(
|
||||
@@ -100,7 +100,7 @@ export class NodesPerCountryChartComponent implements OnInit {
|
||||
borderColor: '#000',
|
||||
formatter: () => {
|
||||
return `<b style="color: white">${country.name.en} (${country.share}%)</b><br>` +
|
||||
$localize`${country.count.toString()} nodes<br>` +
|
||||
$localize`${country.count.toString()} nodes` + `<br>` +
|
||||
$localize`${this.amountShortenerPipe.transform(country.capacity / 100000000, 2)} BTC capacity`
|
||||
;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div class="container-xl full-height" style="min-height: 335px">
|
||||
<h1 i18n="lightning.nodes-in-country">
|
||||
<span>Lightning nodes in {{ country?.name }}</span>
|
||||
<h1>
|
||||
<span i18n="lightning.nodes-in-country">Lightning nodes in {{ country?.name }}</span>
|
||||
<span style="font-size: 50px; vertical-align:sub;"> {{ country?.flag }}</span>
|
||||
</h1>
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
|
||||
<thead>
|
||||
<th class="alias text-left" i18n="lightning.alias">Alias</th>
|
||||
<th class="timestamp-first text-left" i18n="lightning.first_seen">First seen</th>
|
||||
<th class="timestamp-first text-left" i18n="transaction.first-seen|Transaction first seen">First seen</th>
|
||||
<th class="timestamp-update text-left" i18n="lightning.last_update">Last update</th>
|
||||
<th class="capacity text-right" i18n="lightning.capacity">Capacity</th>
|
||||
<th class="channels text-right" i18n="lightning.channels">Channels</th>
|
||||
|
||||
@@ -3,21 +3,21 @@
|
||||
<div *ngIf="widget">
|
||||
<div class="pool-distribution" *ngIf="(nodesPerAsObservable$ | async) as stats; else loadingReward">
|
||||
<div class="item">
|
||||
<h5 class="card-title d-inline-block" i18n="lightning.clearnet-capacity">Clearnet capacity</h5>
|
||||
<h5 class="card-title d-inline-block" i18n="lightning.clearnet-capacity">Clearnet Capacity</h5>
|
||||
<p class="card-text" i18n-ngbTooltip="lightning.clearnet-capacity-desc"
|
||||
ngbTooltip="How much liquidity is running on nodes advertising at least one clearnet IP address" placement="bottom">
|
||||
<app-amount [satoshis]="stats.clearnetCapacity" [digitsInfo]="'1.2-2'" [noFiat]="true"></app-amount>
|
||||
</p>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title d-inline-block" i18n="lightning.unknown-capacity">Unknown capacity</h5>
|
||||
<h5 class="card-title d-inline-block" i18n="lightning.unknown-capacity">Unknown Capacity</h5>
|
||||
<p class="card-text" i18n-ngbTooltip="lightning.unknown-capacity-desc"
|
||||
ngbTooltip="How much liquidity is running on nodes which ISP was not identifiable" placement="bottom">
|
||||
<app-amount [satoshis]="stats.unknownCapacity" [digitsInfo]="'1.2-2'" [noFiat]="true"></app-amount>
|
||||
</p>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title d-inline-block" i18n="lightning.tor-capacity">Tor capacity</h5>
|
||||
<h5 class="card-title d-inline-block" i18n="lightning.tor-capacity">Tor Capacity</h5>
|
||||
<p class="card-text" i18n-ngbTooltip="lightning.tor-capacity-desc"
|
||||
ngbTooltip="How much liquidity is running on nodes advertising only Tor addresses" placement="bottom">
|
||||
<app-amount [satoshis]="stats.torCapacity" [digitsInfo]="'1.2-2'" [noFiat]="true"></app-amount>
|
||||
@@ -33,8 +33,8 @@
|
||||
<fa-icon [icon]="['fas', 'download']" [fixedWidth]="true"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
<small class="d-block" style="color: #ffffff66; min-height: 25px" i18n="lightning.tor-nodes-excluded">
|
||||
<span>(Tor nodes excluded)</span>
|
||||
<small class="d-block" style="color: #ffffff66; min-height: 25px" >
|
||||
<span i18n="lightning.tor-nodes-excluded">(Tor nodes excluded)</span>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
@@ -80,19 +80,19 @@
|
||||
<ng-template #loadingReward>
|
||||
<div class="pool-distribution">
|
||||
<div class="item">
|
||||
<h5 class="card-title d-inline-block" i18n="lightning.clearnet-capacity">Clearnet capacity</h5>
|
||||
<h5 class="card-title d-inline-block" i18n="lightning.clearnet-capacity">Clearnet Capacity</h5>
|
||||
<p class="card-text">
|
||||
<span class="skeleton-loader skeleton-loader-big"></span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title d-inline-block" i18n="lightning.unknown-capacity">Unknown capacity</h5>
|
||||
<h5 class="card-title d-inline-block" i18n="lightning.unknown-capacity">Unknown Capacity</h5>
|
||||
<p class="card-text">
|
||||
<span class="skeleton-loader skeleton-loader-big"></span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title d-inline-block" i18n="lightning.tor-capacity">Tor capacity</h5>
|
||||
<h5 class="card-title d-inline-block" i18n="lightning.tor-capacity">Tor Capacity</h5>
|
||||
<p class="card-text">
|
||||
<span class="skeleton-loader skeleton-loader-big"></span>
|
||||
</p>
|
||||
|
||||
@@ -48,7 +48,7 @@ export class NodesPerISPChartComponent implements OnInit {
|
||||
|
||||
ngOnInit(): void {
|
||||
if (!this.widget) {
|
||||
this.seoService.setTitle($localize`Lightning nodes per ISP`);
|
||||
this.seoService.setTitle($localize`:@@8573a1576789bd2c4faeaed23037c4917812c6cf:Lightning Nodes Per ISP`);
|
||||
}
|
||||
|
||||
this.nodesPerAsObservable$ = combineLatest([
|
||||
@@ -154,7 +154,7 @@ export class NodesPerISPChartComponent implements OnInit {
|
||||
borderColor: '#000',
|
||||
formatter: () => {
|
||||
return `<b style="color: white">${isp[1]} (${this.sortBy === 'capacity' ? isp[7] : isp[6]}%)</b><br>` +
|
||||
$localize`${isp[4].toString()} nodes<br>` +
|
||||
$localize`${isp[4].toString()} nodes` + `<br>` +
|
||||
$localize`${this.amountShortenerPipe.transform(isp[2] / 100000000, 2)} BTC`
|
||||
;
|
||||
}
|
||||
@@ -186,7 +186,7 @@ export class NodesPerISPChartComponent implements OnInit {
|
||||
borderColor: '#000',
|
||||
formatter: () => {
|
||||
return `<b style="color: white">Other (${totalShareOther.toFixed(2)}%)</b><br>` +
|
||||
$localize`${nodeCountOther.toString()} nodes<br>` +
|
||||
$localize`${nodeCountOther.toString()} nodes` + `<br>` +
|
||||
$localize`${this.amountShortenerPipe.transform(capacityOther / 100000000, 2)} BTC`;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div class="box preview-box" *ngIf="(nodes$ | async) as ispNodes">
|
||||
<app-preview-title>
|
||||
<span i18n="lightning.node">lightning ISP</span>
|
||||
<span i18n="lightning.node-isp">Lightning ISP</span>
|
||||
</app-preview-title>
|
||||
<div class="row d-flex justify-content-between full-width-row">
|
||||
<div class="title-wrapper">
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
|
||||
<thead>
|
||||
<th class="alias text-left" i18n="lightning.alias">Alias</th>
|
||||
<th class="timestamp-first text-left" i18n="lightning.first_seen">First seen</th>
|
||||
<th class="timestamp-first text-left" i18n="transaction.first-seen|Transaction first seen">First seen</th>
|
||||
<th class="timestamp-update text-left" i18n="lightning.last_update">Last update</th>
|
||||
<th class="capacity text-right" i18n="lightning.capacity">Capacity</th>
|
||||
<th class="channels text-right" i18n="lightning.channels">Channels</th>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div [class]="!widget ? 'container-xl full-height' : ''">
|
||||
<h1 *ngIf="!widget" class="float-left" i18n="lightning.top-100-oldest-nodes">
|
||||
<span>Top 100 oldest lightning nodes</span>
|
||||
<h1 *ngIf="!widget" class="float-left">
|
||||
<span i18n="lightning.top-100-oldest-nodes">Top 100 oldest lightning nodes</span>
|
||||
</h1>
|
||||
|
||||
<div [class]="widget ? 'widget' : 'full'">
|
||||
@@ -8,7 +8,7 @@
|
||||
<thead>
|
||||
<th class="rank"></th>
|
||||
<th class="alias text-left" i18n="nodes.alias">Alias</th>
|
||||
<th class="timestamp-first text-right" i18n="lightning.first_seen">First seen</th>
|
||||
<th class="timestamp-first text-right" i18n="transaction.first-seen|Transaction first seen">First seen</th>
|
||||
<th *ngIf="!widget" class="capacity text-right" i18n="node.liquidity">Liquidity</th>
|
||||
<th *ngIf="!widget" class="channels text-right" i18n="lightning.channels">Channels</th>
|
||||
<th *ngIf="!widget" class="timestamp-update text-left" i18n="lightning.last_update">Last update</th>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<th class="alias text-left" i18n="nodes.alias">Alias</th>
|
||||
<th class="capacity text-right" i18n="node.liquidity">Liquidity</th>
|
||||
<th *ngIf="!widget" class="channels text-right" i18n="lightning.channels">Channels</th>
|
||||
<th *ngIf="!widget" class="timestamp-first text-left" i18n="lightning.first_seen">First seen</th>
|
||||
<th *ngIf="!widget" class="timestamp-first text-left" i18n="transaction.first-seen|Transaction first seen">First seen</th>
|
||||
<th *ngIf="!widget" class="timestamp-update text-left" i18n="lightning.last_update">Last update</th>
|
||||
<th *ngIf="!widget" class="location text-right" i18n="lightning.location">Location</th>
|
||||
</thead>
|
||||
|
||||
@@ -26,7 +26,7 @@ export class TopNodesPerCapacity implements OnInit {
|
||||
|
||||
ngOnInit(): void {
|
||||
if (!this.widget) {
|
||||
this.seoService.setTitle($localize`Liquidity Ranking`);
|
||||
this.seoService.setTitle($localize`:@@2d9883d230a47fbbb2ec969e32a186597ea27405:Liquidity Ranking`);
|
||||
}
|
||||
|
||||
for (let i = 1; i <= (this.widget ? (isMobile() ? 8 : 7) : 100); ++i) {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<th class="alias text-left" i18n="nodes.alias">Alias</th>
|
||||
<th class="channels text-right" i18n="node.channels">Channels</th>
|
||||
<th *ngIf="!widget" class="capacity text-right" i18n="lightning.liquidity">Liquidity</th>
|
||||
<th *ngIf="!widget" class="timestamp-first text-left" i18n="lightning.first_seen">First seen</th>
|
||||
<th *ngIf="!widget" class="timestamp-first text-left" i18n="transaction.first-seen|Transaction first seen">First seen</th>
|
||||
<th *ngIf="!widget" class="timestamp-update text-left" i18n="lightning.last_update">Last update</th>
|
||||
<th *ngIf="!widget" class="location text-right" i18n="lightning.location">Location</th>
|
||||
</thead>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
|
||||
import { map, Observable } from 'rxjs';
|
||||
import { INodesRanking, ITopNodesPerChannels } from '../../../interfaces/node-api.interface';
|
||||
import { SeoService } from '../../../services/seo.service';
|
||||
import { isMobile } from '../../../shared/common.utils';
|
||||
import { GeolocationData } from '../../../shared/components/geolocation/geolocation.component';
|
||||
import { LightningApiService } from '../../lightning-api.service';
|
||||
@@ -21,14 +20,9 @@ export class TopNodesPerChannels implements OnInit {
|
||||
|
||||
constructor(
|
||||
private apiService: LightningApiService,
|
||||
private seoService: SeoService
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (!this.widget) {
|
||||
this.seoService.setTitle($localize`Connectivity Ranking`);
|
||||
}
|
||||
|
||||
for (let i = 1; i <= (this.widget ? (isMobile() ? 8 : 7) : 100); ++i) {
|
||||
this.skeletonRows.push(i);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<a class="title-link" href="" [routerLink]="['/lightning/nodes/rankings/liquidity' | relativeUrl]">
|
||||
<h5 class="card-title d-inline" i18n="lightning.liquidity-ranking">Liquidity ranking</h5>
|
||||
<h5 class="card-title d-inline" i18n="lightning.liquidity-ranking">Liquidity Ranking</h5>
|
||||
<span> </span>
|
||||
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true"
|
||||
style="vertical-align: 'text-top'; font-size: 13px; color: '#4a68b9'"></fa-icon>
|
||||
@@ -19,7 +19,7 @@
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<a class="title-link" href="" [routerLink]="['/lightning/nodes/rankings/connectivity' | relativeUrl]">
|
||||
<h5 class="card-title d-inline" i18n="lightning.connectivity-ranking">Connectivity ranking</h5>
|
||||
<h5 class="card-title d-inline" i18n="lightning.connectivity-ranking">Connectivity Ranking</h5>
|
||||
<span> </span>
|
||||
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true"
|
||||
style="vertical-align: 'text-top'; font-size: 13px; color: '#4a68b9'"></fa-icon>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<div class="card-header mb-0 mb-md-4" [style]="widget ? 'display:none' : ''">
|
||||
<div class="d-flex d-md-block align-items-baseline">
|
||||
<span i18n="mining.channels-and-capacity">Channels & Capacity</span>
|
||||
<span i18n="lightning.network-capacity">Lightning Network Capacity</span>
|
||||
<button class="btn p-0 pl-2" style="margin: 0 0 4px 0px" (click)="onSaveChart()">
|
||||
<fa-icon [icon]="['fas', 'download']" [fixedWidth]="true"></fa-icon>
|
||||
</button>
|
||||
@@ -49,9 +49,7 @@
|
||||
</div>
|
||||
|
||||
<div *ngIf="widget && (capacityObservable$ | async) as stats">
|
||||
<div *ngIf="stats.days === 0" class="indexing-message d-flex" i18n="lightning.indexing-in-progress">
|
||||
Indexing in progress
|
||||
</div>
|
||||
<div *ngIf="stats.days === 0" class="indexing-message d-flex" i18n="lightning.indexing-in-progress">Indexing in progress</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -63,7 +63,7 @@ export class LightningStatisticsChartComponent implements OnInit {
|
||||
if (this.widget) {
|
||||
this.miningWindowPreference = '3y';
|
||||
} else {
|
||||
this.seoService.setTitle($localize`Channels and Capacity`);
|
||||
this.seoService.setTitle($localize`:@@ea8db27e6db64f8b940711948c001a1100e5fe9f:Lightning Network Capacity`);
|
||||
this.miningWindowPreference = this.miningService.getDefaultTimespan('all');
|
||||
}
|
||||
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
|
||||
@@ -119,7 +119,7 @@ export class LightningStatisticsChartComponent implements OnInit {
|
||||
color: 'grey',
|
||||
fontSize: 11
|
||||
},
|
||||
text: $localize`Channels & Capacity`,
|
||||
text: $localize`:@@ea8db27e6db64f8b940711948c001a1100e5fe9f:Lightning Network Capacity`,
|
||||
left: 'center',
|
||||
top: 11,
|
||||
zlevel: 10,
|
||||
@@ -191,7 +191,7 @@ export class LightningStatisticsChartComponent implements OnInit {
|
||||
padding: 10,
|
||||
data: [
|
||||
{
|
||||
name: 'Channels',
|
||||
name: $localize`:@@807cf11e6ac1cde912496f764c176bdfdd6b7e19:Channels`,
|
||||
inactiveColor: 'rgb(110, 112, 121)',
|
||||
textStyle: {
|
||||
color: 'white',
|
||||
@@ -199,7 +199,7 @@ export class LightningStatisticsChartComponent implements OnInit {
|
||||
icon: 'roundRect',
|
||||
},
|
||||
{
|
||||
name: 'Capacity',
|
||||
name: $localize`:@@ce9dfdc6dccb28dc75a78c704e09dc18fb02dcfa:Capacity`,
|
||||
inactiveColor: 'rgb(110, 112, 121)',
|
||||
textStyle: {
|
||||
color: 'white',
|
||||
@@ -279,7 +279,7 @@ export class LightningStatisticsChartComponent implements OnInit {
|
||||
{
|
||||
zlevel: 0,
|
||||
yAxisIndex: 1,
|
||||
name: $localize`Capacity`,
|
||||
name: $localize`:@@ce9dfdc6dccb28dc75a78c704e09dc18fb02dcfa:Capacity`,
|
||||
showSymbol: false,
|
||||
symbol: 'none',
|
||||
stack: 'Total',
|
||||
@@ -341,7 +341,7 @@ export class LightningStatisticsChartComponent implements OnInit {
|
||||
this.chartInstance.setOption(this.chartOptions);
|
||||
download(this.chartInstance.getDataURL({
|
||||
pixelRatio: 2,
|
||||
}), `block-sizes-weights-${this.timespan}-${Math.round(now.getTime() / 1000)}.svg`);
|
||||
}), `lightning-network-capacity-${this.timespan}-${Math.round(now.getTime() / 1000)}.svg`);
|
||||
// @ts-ignore
|
||||
this.chartOptions.grid.bottom = prevBottom;
|
||||
this.chartOptions.backgroundColor = 'none';
|
||||
|
||||
90
frontend/src/app/services/navigation.service.ts
Normal file
90
frontend/src/app/services/navigation.service.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Router, ActivatedRoute, NavigationEnd, ActivatedRouteSnapshot } from '@angular/router';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { filter, map } from 'rxjs/operators';
|
||||
import { StateService } from './state.service';
|
||||
|
||||
const networkModules = {
|
||||
bitcoin: {
|
||||
subnets: [
|
||||
{ name: 'mainnet', path: '' },
|
||||
{ name: 'testnet', path: '/testnet' },
|
||||
{ name: 'signet', path: '/signet' },
|
||||
],
|
||||
},
|
||||
liquid: {
|
||||
subnets: [
|
||||
{ name: 'liquid', path: '' },
|
||||
{ name: 'liquidtestnet', path: '/testnet' },
|
||||
],
|
||||
},
|
||||
bisq: {
|
||||
subnets: [
|
||||
{ name: 'bisq', path: '' },
|
||||
],
|
||||
},
|
||||
};
|
||||
const networks = Object.keys(networkModules);
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class NavigationService {
|
||||
subnetPaths = new BehaviorSubject<Record<string,string>>({});
|
||||
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
private router: Router,
|
||||
) {
|
||||
this.router.events.pipe(
|
||||
filter(event => event instanceof NavigationEnd),
|
||||
map(() => this.router.routerState.snapshot.root),
|
||||
).subscribe((state) => {
|
||||
this.updateSubnetPaths(state);
|
||||
});
|
||||
}
|
||||
|
||||
// For each network (bitcoin/liquid/bisq), find and save the longest url path compatible with the current route
|
||||
updateSubnetPaths(root: ActivatedRouteSnapshot): void {
|
||||
let path = '';
|
||||
const networkPaths = {};
|
||||
let route = root;
|
||||
// traverse the router state tree until all network paths are set, or we reach the end of the tree
|
||||
while (!networks.reduce((acc, network) => acc && !!networkPaths[network], true) && route) {
|
||||
// 'networkSpecific' paths may correspond to valid routes on other networks, but aren't directly compatible
|
||||
// (e.g. we shouldn't link a mainnet transaction page to the same txid on testnet or liquid)
|
||||
if (route.data?.networkSpecific) {
|
||||
networks.forEach(network => {
|
||||
if (networkPaths[network] == null) {
|
||||
networkPaths[network] = path;
|
||||
}
|
||||
});
|
||||
}
|
||||
// null or empty networks list is shorthand for "compatible with every network"
|
||||
if (route.data?.networks?.length) {
|
||||
// if the list is non-empty, only those networks are compatible
|
||||
networks.forEach(network => {
|
||||
if (!route.data.networks.includes(network)) {
|
||||
if (networkPaths[network] == null) {
|
||||
networkPaths[network] = path;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
if (route.url?.length) {
|
||||
path = [path, ...route.url.map(segment => segment.path).filter(path => {
|
||||
return path.length && !['testnet', 'signet'].includes(path);
|
||||
})].join('/');
|
||||
}
|
||||
route = route.firstChild;
|
||||
}
|
||||
|
||||
const subnetPaths = {};
|
||||
Object.entries(networkModules).forEach(([key, network]) => {
|
||||
network.subnets.forEach(subnet => {
|
||||
subnetPaths[subnet.name] = subnet.path + (networkPaths[key] != null ? networkPaths[key] : path);
|
||||
});
|
||||
});
|
||||
this.subnetPaths.next(subnetPaths);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Inject, Injectable, PLATFORM_ID } from '@angular/core';
|
||||
import { Inject, Injectable, PLATFORM_ID, LOCALE_ID } from '@angular/core';
|
||||
import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable } from 'rxjs';
|
||||
import { Transaction } from '../interfaces/electrs.interface';
|
||||
import { IBackendInfo, MempoolBlock, MempoolBlockWithTransactions, MempoolBlockDelta, MempoolInfo, Recommendedfees, ReplacedTransaction, TransactionStripped } from '../interfaces/websocket.interface';
|
||||
@@ -110,9 +110,11 @@ export class StateService {
|
||||
|
||||
blockScrolling$: Subject<boolean> = new Subject<boolean>();
|
||||
timeLtr: BehaviorSubject<boolean>;
|
||||
hideFlow: BehaviorSubject<boolean>;
|
||||
|
||||
constructor(
|
||||
@Inject(PLATFORM_ID) private platformId: any,
|
||||
@Inject(LOCALE_ID) private locale: string,
|
||||
private router: Router,
|
||||
private storageService: StorageService,
|
||||
) {
|
||||
@@ -151,10 +153,23 @@ export class StateService {
|
||||
|
||||
this.blockVSize = this.env.BLOCK_WEIGHT_UNITS / 4;
|
||||
|
||||
this.timeLtr = new BehaviorSubject<boolean>(this.storageService.getValue('time-preference-ltr') === 'true');
|
||||
const savedTimePreference = this.storageService.getValue('time-preference-ltr');
|
||||
const rtlLanguage = (this.locale.startsWith('ar') || this.locale.startsWith('fa') || this.locale.startsWith('he'));
|
||||
// default time direction is right-to-left, unless locale is a RTL language
|
||||
this.timeLtr = new BehaviorSubject<boolean>(savedTimePreference === 'true' || (savedTimePreference == null && rtlLanguage));
|
||||
this.timeLtr.subscribe((ltr) => {
|
||||
this.storageService.setValue('time-preference-ltr', ltr ? 'true' : 'false');
|
||||
});
|
||||
|
||||
const savedFlowPreference = this.storageService.getValue('flow-preference');
|
||||
this.hideFlow = new BehaviorSubject<boolean>(savedFlowPreference === 'hide');
|
||||
this.hideFlow.subscribe((hide) => {
|
||||
if (hide) {
|
||||
this.storageService.setValue('flow-preference', hide ? 'hide' : 'show');
|
||||
} else {
|
||||
this.storageService.removeItem('flow-preference');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setNetworkBasedonUrl(url: string) {
|
||||
@@ -166,7 +181,8 @@ export class StateService {
|
||||
// (?:[a-z]{2}(?:-[A-Z]{2})?\/)? optional locale prefix (non-capturing)
|
||||
// (?:preview\/)? optional "preview" prefix (non-capturing)
|
||||
// (bisq|testnet|liquidtestnet|liquid|signet)/ network string (captured as networkMatches[1])
|
||||
const networkMatches = url.match(/^\/(?:[a-z]{2}(?:-[A-Z]{2})?\/)?(?:preview\/)?(bisq|testnet|liquidtestnet|liquid|signet)/);
|
||||
// ($|\/) network string must end or end with a slash
|
||||
const networkMatches = url.match(/^\/(?:[a-z]{2}(?:-[A-Z]{2})?\/)?(?:preview\/)?(bisq|testnet|liquidtestnet|liquid|signet)($|\/)/);
|
||||
switch (networkMatches && networkMatches[1]) {
|
||||
case 'liquid':
|
||||
if (this.network !== 'liquid') {
|
||||
|
||||
@@ -46,4 +46,12 @@ export class StorageService {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
removeItem(key: string): void {
|
||||
try {
|
||||
localStorage.removeItem(key);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user