Merge branch 'master' into dependabot/npm_and_yarn/backend/bitcoinjs-lib-6.0.2

This commit is contained in:
wiz 2022-08-14 13:51:53 +09:00 committed by GitHub
commit 3684ee1e6c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 587 additions and 416 deletions

View File

@ -24,8 +24,8 @@ __MEMPOOL_USER_AGENT__=${MEMPOOL_USER_AGENT:=mempool}
__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__=${MEMPOOL_STDOUT_LOG_MIN_PRIORITY:=info} __MEMPOOL_STDOUT_LOG_MIN_PRIORITY__=${MEMPOOL_STDOUT_LOG_MIN_PRIORITY:=info}
__MEMPOOL_INDEXING_BLOCKS_AMOUNT__=${MEMPOOL_INDEXING_BLOCKS_AMOUNT:=false} __MEMPOOL_INDEXING_BLOCKS_AMOUNT__=${MEMPOOL_INDEXING_BLOCKS_AMOUNT:=false}
__MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__=${MEMPOOL_AUTOMATIC_BLOCK_REINDEXING:=false} __MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__=${MEMPOOL_AUTOMATIC_BLOCK_REINDEXING:=false}
__MEMPOOL_POOLS_JSON_URL__=${MEMPOOL_POOLS_JSON_URL:=false} __MEMPOOL_POOLS_JSON_URL__=${MEMPOOL_POOLS_JSON_URL:=https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json}
__MEMPOOL_POOLS_JSON_TREE_URL__=${MEMPOOL_POOLS_JSON_TREE_URL:=false} __MEMPOOL_POOLS_JSON_TREE_URL__=${MEMPOOL_POOLS_JSON_TREE_URL:=https://api.github.com/repos/mempool/mining-pools/git/trees/master}
# CORE_RPC # CORE_RPC
__CORE_RPC_HOST__=${CORE_RPC_HOST:=127.0.0.1} __CORE_RPC_HOST__=${CORE_RPC_HOST:=127.0.0.1}
@ -116,8 +116,8 @@ sed -i "s!__MEMPOOL_USER_AGENT__!${__MEMPOOL_USER_AGENT__}!g" mempool-config.jso
sed -i "s/__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__/${__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__}/g" mempool-config.json sed -i "s/__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__/${__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__}/g" mempool-config.json
sed -i "s/__MEMPOOL_INDEXING_BLOCKS_AMOUNT__/${__MEMPOOL_INDEXING_BLOCKS_AMOUNT__}/g" mempool-config.json sed -i "s/__MEMPOOL_INDEXING_BLOCKS_AMOUNT__/${__MEMPOOL_INDEXING_BLOCKS_AMOUNT__}/g" mempool-config.json
sed -i "s/__MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__/${__MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__}/g" mempool-config.json sed -i "s/__MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__/${__MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__}/g" mempool-config.json
sed -i "s/__MEMPOOL_POOLS_JSON_URL__/${__MEMPOOL_POOLS_JSON_URL__}/g" mempool-config.json sed -i "s!__MEMPOOL_POOLS_JSON_URL__!${__MEMPOOL_POOLS_JSON_URL__}/g" mempool-config.json
sed -i "s/__MEMPOOL_POOLS_JSON_TREE_URL__/${__MEMPOOL_POOLS_JSON_TREE_URL__}/g" mempool-config.json sed -i "s!__MEMPOOL_POOLS_JSON_TREE_URL__!${__MEMPOOL_POOLS_JSON_TREE_URL__}/g" mempool-config.json
sed -i "s/__CORE_RPC_HOST__/${__CORE_RPC_HOST__}/g" mempool-config.json sed -i "s/__CORE_RPC_HOST__/${__CORE_RPC_HOST__}/g" mempool-config.json
sed -i "s/__CORE_RPC_PORT__/${__CORE_RPC_PORT__}/g" mempool-config.json sed -i "s/__CORE_RPC_PORT__/${__CORE_RPC_PORT__}/g" mempool-config.json

View File

@ -15,7 +15,6 @@ import { TermsOfServiceComponent } from './components/terms-of-service/terms-of-
import { PrivacyPolicyComponent } from './components/privacy-policy/privacy-policy.component'; import { PrivacyPolicyComponent } from './components/privacy-policy/privacy-policy.component';
import { TrademarkPolicyComponent } from './components/trademark-policy/trademark-policy.component'; import { TrademarkPolicyComponent } from './components/trademark-policy/trademark-policy.component';
import { BisqMasterPageComponent } from './components/bisq-master-page/bisq-master-page.component'; import { BisqMasterPageComponent } from './components/bisq-master-page/bisq-master-page.component';
import { SponsorComponent } from './components/sponsor/sponsor.component';
import { PushTransactionComponent } from './components/push-transaction/push-transaction.component'; import { PushTransactionComponent } from './components/push-transaction/push-transaction.component';
import { BlocksList } from './components/blocks-list/blocks-list.component'; import { BlocksList } from './components/blocks-list/blocks-list.component';
import { LiquidMasterPageComponent } from './components/liquid-master-page/liquid-master-page.component'; import { LiquidMasterPageComponent } from './components/liquid-master-page/liquid-master-page.component';
@ -367,16 +366,24 @@ let routes: Routes = [
children: [], children: [],
component: AddressPreviewComponent component: AddressPreviewComponent
}, },
{
path: 'lightning',
loadChildren: () => import('./lightning/lightning-previews.module').then(m => m.LightningPreviewsModule)
},
{
path: 'testnet/lightning',
loadChildren: () => import('./lightning/lightning-previews.module').then(m => m.LightningPreviewsModule)
},
{
path: 'signet/lightning',
loadChildren: () => import('./lightning/lightning-previews.module').then(m => m.LightningPreviewsModule)
},
], ],
}, },
{ {
path: 'status', path: 'status',
component: StatusViewComponent component: StatusViewComponent
}, },
{
path: 'sponsor',
component: SponsorComponent,
},
{ {
path: '', path: '',
loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule) loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule)
@ -642,10 +649,6 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
path: 'status', path: 'status',
component: StatusViewComponent component: StatusViewComponent
}, },
{
path: 'sponsor',
component: SponsorComponent,
},
{ {
path: '', path: '',
loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule) loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule)

View File

@ -25,12 +25,6 @@
</a> </a>
</div> </div>
<br><br>
<div class="sponsor-button">
<button [hidden]="showNavigateToSponsor" type="button" class="btn btn-primary" (click)="sponsor()" i18n="about.become-a-sponsor">Become a sponsor ❤️</button>
<ng-container *ngIf="showNavigateToSponsor" i18n="about.navigate-to-sponsor">Navigate to <a href="https://mempool.space/sponsor" target="_blank">https://mempool.space/sponsor</a> to sponsor</ng-container>
</div>
<div class="enterprise-sponsor"> <div class="enterprise-sponsor">
<h3 i18n="about.sponsors.enterprise.withRocket">Enterprise Sponsors 🚀</h3> <h3 i18n="about.sponsors.enterprise.withRocket">Enterprise Sponsors 🚀</h3>
<div class="wrapper"> <div class="wrapper">

View File

@ -65,6 +65,10 @@
height: 45px; height: 45px;
width: 100%; width: 100%;
} }
img {
width: 67px;
height: 67px;
}
} }
.alliances { .alliances {

View File

@ -61,9 +61,9 @@ export class AboutComponent implements OnInit {
); );
} }
sponsor() { sponsor(): void {
if (this.officialMempoolSpace && this.stateService.env.BASE_MODULE === 'mempool') { if (this.officialMempoolSpace && this.stateService.env.BASE_MODULE === 'mempool') {
this.router.navigateByUrl('/sponsor'); this.router.navigateByUrl('/enterprise');
} else { } else {
this.showNavigateToSponsor = true; this.showNavigateToSponsor = true;
} }

View File

@ -7,12 +7,12 @@
</span> </span>
<div [ngSwitch]="network.val"> <div [ngSwitch]="network.val">
<span *ngSwitchCase="'signet'" class="network signet"><img src="/resources/signet-logo.png" style="width: 45px;" class="signet mr-1" alt="logo"> Signet</span> <span *ngSwitchCase="'signet'" class="network signet"><img src="/resources/signet-logo.png" style="width: 45px;" class="signet mr-1" alt="logo"> Signet <ng-template [ngIf]="(lightning$ | async)">Lightning</ng-template></span>
<span *ngSwitchCase="'testnet'" class="network testnet"><img src="/resources/testnet-logo.png" style="width: 45px;" class="mr-1" alt="testnet logo"> Testnet</span> <span *ngSwitchCase="'testnet'" class="network testnet"><img src="/resources/testnet-logo.png" style="width: 45px;" class="mr-1" alt="testnet logo"> Testnet <ng-template [ngIf]="(lightning$ | async)">Lightning</ng-template></span>
<span *ngSwitchCase="'bisq'" class="network bisq"><img src="/resources/bisq-logo.png" style="width: 45px;" class="mr-1" alt="bisq logo"> Bisq</span> <span *ngSwitchCase="'bisq'" class="network bisq"><img src="/resources/bisq-logo.png" style="width: 45px;" class="mr-1" alt="bisq logo"> Bisq</span>
<span *ngSwitchCase="'liquid'" class="network liquid"><img src="/resources/liquid-logo.png" style="width: 45px;" class="mr-1" alt="liquid mainnet logo"> Liquid</span> <span *ngSwitchCase="'liquid'" class="network liquid"><img src="/resources/liquid-logo.png" style="width: 45px;" class="mr-1" alt="liquid mainnet logo"> Liquid</span>
<span *ngSwitchCase="'liquidtestnet'" class="network liquidtestnet"><img src="/resources/liquidtestnet-logo.png" style="width: 45px;" class="mr-1" alt="liquid testnet logo"> Liquid Testnet</span> <span *ngSwitchCase="'liquidtestnet'" class="network liquidtestnet"><img src="/resources/liquidtestnet-logo.png" style="width: 45px;" class="mr-1" alt="liquid testnet logo"> Liquid Testnet</span>
<span *ngSwitchDefault class="network mainnet"><img src="/resources/bitcoin-logo.png" style="width: 45px;" class="mainnet mr-1" alt="bitcoin logo"> Mainnet</span> <span *ngSwitchDefault class="network mainnet"><img src="/resources/bitcoin-logo.png" style="width: 45px;" class="mainnet mr-1" alt="bitcoin logo"> Mainnet <ng-template [ngIf]="(lightning$ | async)">Lightning</ng-template></span>
</div> </div>
</header> </header>
<router-outlet></router-outlet> <router-outlet></router-outlet>

View File

@ -10,6 +10,7 @@ import { LanguageService } from 'src/app/services/language.service';
}) })
export class MasterPagePreviewComponent implements OnInit { export class MasterPagePreviewComponent implements OnInit {
network$: Observable<string>; network$: Observable<string>;
lightning$: Observable<boolean>;
officialMempoolSpace = this.stateService.env.OFFICIAL_MEMPOOL_SPACE; officialMempoolSpace = this.stateService.env.OFFICIAL_MEMPOOL_SPACE;
urlLanguage: string; urlLanguage: string;
@ -20,6 +21,7 @@ export class MasterPagePreviewComponent implements OnInit {
ngOnInit() { ngOnInit() {
this.network$ = merge(of(''), this.stateService.networkChanged$); this.network$ = merge(of(''), this.stateService.networkChanged$);
this.lightning$ = this.stateService.lightningChanged$;
this.urlLanguage = this.languageService.getLanguageForUrl(); this.urlLanguage = this.languageService.getLanguageForUrl();
} }
} }

View File

@ -1,157 +0,0 @@
<div class="container-xl">
<app-svg-images name="officialMempoolSpace" class="logo" style="width: 140px; height: 35px; margin: 30px;" width="500" height="126" viewBox="0 0 500 126"></app-svg-images>
<div class="sponsor-page">
<h1 i18n="sponsor.title">Sponsor</h1>
<p *ngIf="!sponsorsEnabled; else sponsorForm">
<ng-container i18n="about.navigate-to-sponsor">Navigate to <a href="https://mempool.space/sponsor" target="_blank">https://mempool.space/sponsor</a> to sponsor</ng-container>
</p>
<ng-template #sponsorForm>
<div [hidden]="donationStatus !== 1">
<form [formGroup]="donationForm" (submit)="submitDonation()" class="form">
<div class="flex-container">
<div class="card" (click)="setSelection(0.001)" [class.shiny-border]="donationForm.get('selection').value === 0.001">
<div class="card-header">Support the Project</div>
<div class="card-body">
<h5 class="card-title">100K <span class="symbol">sats</span></h5>
<p class="card-text">Make a donation and support the project</p>
</div>
</div>
<div class="card middle-card" (click)="setSelection(0.01)" [class.shiny-border]="donationForm.get('selection').value === 0.01">
<div class="card-header">Community Sponsor</div>
<div class="card-body">
<h5 class="card-title">1M <span class="symbol">sats</span></h5>
<p class="card-text">Display your Twitter profile photo on our About page</p>
</div>
</div>
<div class="card" (click)="setSelection(1)" [class.shiny-border]="donationForm.get('selection').value === 1">
<div class="card-header">Enterprise Sponsor</div>
<div class="card-body">
<h5 class="card-title">1 <span class="symbol">BTC</span></h5>
<p class="card-text">Backlink to your organization's website</p>
</div>
</div>
</div>
<div class="donation-form">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">@</span>
</div>
<input formControlName="handle" class="form-control" type="text" placeholder="Twitter handle (Optional)">
</div>
<div class="input-group">
<button class="btn btn-primary" type="submit" [disabled]="donationForm.invalid" i18n="about.sponsor.request-invoice">Request invoice</button>
</div>
</div>
</form>
</div>
</ng-template>
<div *ngIf="donationStatus === 2">
<form [formGroup]="paymentForm">
<div class="btn-group btn-group-toggle" ngbRadioGroup formControlName="method">
<label ngbButtonLabel class="btn-primary">
<input ngbButton type="radio" value="chain"> <fa-icon [icon]="['fas', 'link']" [fixedWidth]="true" title="Onchain"></fa-icon>
</label>
<label ngbButtonLabel class="btn-primary" *ngIf="donationObj.addresses.BTC_LightningLike">
<input ngbButton type="radio" value="lightning"> <fa-icon [icon]="['fas', 'bolt']" [fixedWidth]="true" title="Lightning"></fa-icon>
</label>
<label ngbButtonLabel class="btn-primary" *ngIf="donationObj.addresses.LBTC">
<input ngbButton type="radio" value="lbtc"> <fa-icon [icon]="['fas', 'tint']" [fixedWidth]="true" title="Liquid Bitcoin"></fa-icon>
</label>
</div>
</form>
<ng-template [ngIf]="paymentForm.get('method').value === 'chain'">
<div class="qr-wrapper">
<a [href]="bypassSecurityTrustUrl('bitcoin:' + donationObj.addresses.BTC + '?amount=' + donationObj.amount)" target="_blank">
<app-qrcode imageUrl="/resources/bitcoin-logo.png" [size]="200" [data]="'bitcoin:' + donationObj.addresses.BTC + '?amount=' + donationObj.amount"></app-qrcode>
</a>
</div>
<div class="input-group input-group-sm info-group">
<input type="text" class="form-control" readonly [value]="donationObj.addresses.BTC">
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="button" ><app-clipboard [text]="donationObj.addresses.BTC"></app-clipboard></button>
</div>
</div>
<p>{{ donationObj.amount }} <span class="symbol">BTC</span></p>
</ng-template>
<ng-template [ngIf]="paymentForm.get('method').value === 'lightning'">
<div class="qr-wrapper">
<a [href]="bypassSecurityTrustUrl('lightning:' + donationObj.addresses.BTC_LightningLike)" target="_blank">
<app-qrcode imageUrl="/resources/bitcoin-logo.png" [size]="200" [data]="donationObj.addresses.BTC_LightningLike.toUpperCase()"></app-qrcode>
</a>
</div>
<div class="input-group input-group-sm info-group">
<input type="text" class="form-control" readonly [value]="donationObj.addresses.BTC_LightningLike">
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="button"><app-clipboard [text]="donationObj.addresses.BTC_LightningLike"></app-clipboard></button>
</div>
</div>
<div class="input-group input-group-sm info-group">
<input type="text" class="form-control" readonly value="036f7fad4938521ddc6fc87ab7d6c6a091cef23cad87564a1f55adb806c017575e@103.99.170.198:9735">
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="button"><app-clipboard [text]="'036f7fad4938521ddc6fc87ab7d6c6a091cef23cad87564a1f55adb806c017575e@103.99.170.198:9735'"></app-clipboard></button>
</div>
</div>
<p>{{ donationObj.amount }} <span class="symbol">BTC</span></p>
</ng-template>
<ng-template [ngIf]="paymentForm.get('method').value === 'lbtc' || paymentForm.get('method').value === 'tlbtc'">
<div class="qr-wrapper">
<a [href]="bypassSecurityTrustUrl('liquidnetwork:' + donationObj.addresses.LBTC + '?amount=' + donationObj.amount + '&assetid=6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d')" target="_blank">
<app-qrcode imageUrl="/resources/liquid-bitcoin.png" [size]="200" [data]="'liquidnetwork:' + donationObj.addresses.LBTC + '?amount=' + donationObj.amount + '&assetid=6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d'"></app-qrcode>
</a>
</div>
<br>
<div class="input-group input-group-sm info-group">
<input type="text" class="form-control" readonly [value]="donationObj.addresses.LBTC" />
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="button" ><app-clipboard [text]="donationObj.addresses.LBTC"></app-clipboard></button>
</div>
</div>
<p>{{ donationObj.amount }} <span class="symbol">BTC</span></p>
</ng-template>
<p i18n="about.sponsor.waiting-for-transaction">Waiting for transaction... </p>
<div class="spinner-border text-light"></div>
</div>
<div *ngIf="donationStatus === 3" class="donation-confirmed">
<h2>
<span i18n="about.sponsor.donation-confirmed">Donation confirmed!</span>
<span i18n="about.sponsor.thank-you">Thank you!</span>
</h2>
<div class="order-details">
Order ID <span>{{ donationObj.id }}</span>
</div>
</div>
</div>
</div>

View File

@ -1,133 +0,0 @@
.sponsor-page {
text-align: center;
}
.qr-wrapper {
background-color: #FFF;
padding: 10px;
display: inline-block;
padding-bottom: 5px;
margin: 20px auto 0px;
}
.info-group {
max-width: 400px;
}
.card {
width: 240px;
height: 220px;
background-color: #1d1f31;
border: 2px solid #1d1f31;
cursor: pointer;
position: relative;
transition: 100ms all;
margin: 30px 30px 20px 30px;
@media(min-width: 476px) {
margin: 30px 100px 20px 100px;
}
@media(min-width: 851px) {
margin: 60px 20px 40px 20px;
}
.card-title {
font-weight: bold;
span {
font-weight: 100;
}
}
&.bigger {
height: 220px;
width: 240px;
margin-top: 40px;
}
&:hover {
background-color: #5058926b;
border: 2px solid #505892;
transform: scale(1.1) translateY(-10px);
margin-top: 70px;
.card-header {
background-color: #505892;
}
}
}
.donation-form {
max-width: 280px;
margin: auto;
button {
width: 100%;
}
}
.card-header {
background-color: #171929;
}
.flex-container {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
}
.middle-card {
width: 280px;
height: 260px;
margin-top: 40px;
&:hover {
margin-top: 50px;
}
}
.shiny-border {
background-color: #5058926b;
border: 2px solid #505892;
transform: scale(1.1) translateY(-10px);
margin-top: 70px;
box-shadow: 0px 0px 100px #9858ff52;
.card-header {
background-color: #505892;
}
&.middle-card {
margin-top: 50px;
}
}
.input-group {
margin: 20px auto;
}
.donation-confirmed {
h2 {
margin-top: 50px;
span {
display: block;
&:last-child {
color: #9858ff;
font-weight: bold;
font-size: 2rem;
}
}
}
.order-details {
margin-top: 50px;
span {
color: #d81b60;
margin-left: 10px;
}
}
}
.card-body {
align-items: center;
display: flex;
justify-content: center;
flex-direction: column;
height: 100%;
}

View File

@ -1,88 +0,0 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
import { Subscription } from 'rxjs';
import { delay, retryWhen, switchMap, tap } from 'rxjs/operators';
import { ApiService } from 'src/app/services/api.service';
import { SeoService } from 'src/app/services/seo.service';
import { StateService } from 'src/app/services/state.service';
import { WebsocketService } from 'src/app/services/websocket.service';
@Component({
selector: 'app-sponsor',
templateUrl: './sponsor.component.html',
styleUrls: ['./sponsor.component.scss']
})
export class SponsorComponent implements OnInit, OnDestroy {
sponsorsEnabled = this.stateService.env.OFFICIAL_MEMPOOL_SPACE;
donationForm: FormGroup;
paymentForm: FormGroup;
requestSubscription: Subscription | undefined;
donationObj: any;
donationStatus = 1;
constructor(
private formBuilder: FormBuilder,
private apiService: ApiService,
private sanitizer: DomSanitizer,
private stateService: StateService,
private websocketService: WebsocketService,
private seoService: SeoService,
) { }
ngOnInit(): void {
this.seoService.setTitle($localize`:@@dfd99c62b5b308fc5b1ad7adbbf9d526d2b31516:Sponsor`);
this.websocketService.want(['blocks']);
this.paymentForm = this.formBuilder.group({
'method': 'chain'
});
this.donationForm = this.formBuilder.group({
selection: [0.01],
handle: [''],
});
}
submitDonation() {
if (this.donationForm.invalid) {
return;
}
this.requestSubscription = this.apiService.requestDonation$(
this.donationForm.get('selection').value,
this.donationForm.get('handle').value
)
.pipe(
tap((response) => {
this.donationObj = response;
this.donationStatus = 2;
}),
switchMap(() => this.apiService.checkDonation$(this.donationObj.id)
.pipe(
retryWhen((errors) => errors.pipe(delay(2000)))
)
)
).subscribe(() => {
this.donationStatus = 3;
/*
if (this.donationForm.get('handle').value) {
this.sponsors.unshift({ handle: this.donationForm.get('handle').value });
}
*/
});
}
setSelection(amount: number): void {
this.donationForm.get('selection').setValue(amount);
}
bypassSecurityTrustUrl(text: string): SafeUrl {
return this.sanitizer.bypassSecurityTrustUrl(text);
}
ngOnDestroy() {
if (this.requestSubscription) {
this.requestSubscription.unsubscribe();
}
}
}

View File

@ -0,0 +1,72 @@
<div class="box preview-box" *ngIf="(channel$ | async) as channel">
<div class="row d-flex justify-content-between full-width-row">
<h1 class="title">
<span i18n="lightning.channel">Channel</span>:
<a [routerLink]="['/lightning/channel' | relativeUrl, channel.id]"> {{ channel.short_id }}</a>
</h1>
<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>
<app-closing-type [type]="channel.closing_reason" *ngIf="channel.status === 2"></app-closing-type>
</div>
</div>
<div class="row d-flex justify-content-between full-width-row nodes">
<span class="node left">
{{ channel.node_left.alias || '?' }}
</span>
<fa-icon class="between-arrow" [icon]="['fas', 'arrow-right-arrow-left']" [fixedWidth]="true" title="channel between"></fa-icon>
<span class="node right">
{{ channel.node_right.alias || '?' }}
</span>
</div>
<div class="row">
<div class="col-md">
<table class="table table-borderless table-striped">
<tbody>
<tr></tr>
<tr>
<td i18n="channel.created">Created</td>
<td>{{ channel.created | date:'yyyy-MM-dd HH:mm' }}</td>
</tr>
<tr>
<td i18n="channel.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>
<div class="dual-cell">
<span>{{ channel.node_left.fee_rate }} <span class="symbol">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>
</div>
</td>
</tr>
<tr>
<td i18n="channel.base-fee">Base fee</td>
<td>
<div class="dual-cell">
<app-sats [satoshis]="channel.node_left.base_fee_mtokens / 1000" digitsInfo="1.0-2"></app-sats>
<fa-icon class="between-arrow" [icon]="['fas', 'arrow-right-arrow-left']" [fixedWidth]="true"></fa-icon>
<app-sats [satoshis]="channel.node_right.base_fee_mtokens / 1000" digitsInfo="1.0-2"></app-sats>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div class="col-md map-col">
<app-nodes-channels-map *ngIf="!error" [style]="'channelpage'" [channel]="channelGeo" [fitContainer]="true" (readyEvent)="onMapReady()"></app-nodes-channels-map>
</div>
</div>
</div>
<ng-template [ngIf]="error">
<div class="text-center">
<span i18n="error.general-loading-data">Error loading data.</span>
<br><br>
<i>{{ error.status }}: {{ error.error }}</i>
</div>
</ng-template>

View File

@ -0,0 +1,76 @@
.title {
font-size: 52px;
margin: 0;
}
.table {
font-size: 32px;
margin-top: 36px;
}
.badges {
font-size: 28px;
::ng-deep .badge {
margin-left: 0.5em;
}
}
.row {
margin-right: 0;
}
.full-width-row {
padding-left: 15px;
padding-right: 15px;
&:nth-child(even) {
background: #181b2d;
margin: 15px 0;
}
}
.nodes {
font-size: 36px;
align-items: center;
}
.between-arrow {
font-size: 24px;
}
.map-col {
flex-grow: 0;
flex-shrink: 0;
width: 470px;
min-width: 470px;
padding: 0;
background: #181b2d;
max-height: 470px;
overflow: hidden;
}
::ng-deep .symbol {
font-size: 24px;
}
.dual-cell {
width: 100%;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: baseline;
& > * {
width: 0;
flex-grow: 1;
&:nth-child(2) {
text-align: center;
max-width: 1.5em;
}
&:nth-child(3) {
text-align: right;
}
}
}

View File

@ -0,0 +1,67 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { ActivatedRoute, ParamMap } from '@angular/router';
import { Observable, of } from 'rxjs';
import { catchError, switchMap, tap } from 'rxjs/operators';
import { SeoService } from 'src/app/services/seo.service';
import { OpenGraphService } from 'src/app/services/opengraph.service';
import { LightningApiService } from '../lightning-api.service';
@Component({
selector: 'app-channel-preview',
templateUrl: './channel-preview.component.html',
styleUrls: ['./channel-preview.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ChannelPreviewComponent implements OnInit {
channel$: Observable<any>;
error: any = null;
channelGeo: number[] = [];
constructor(
private lightningApiService: LightningApiService,
private activatedRoute: ActivatedRoute,
private seoService: SeoService,
private openGraphService: OpenGraphService,
) { }
ngOnInit(): void {
this.channel$ = this.activatedRoute.paramMap
.pipe(
switchMap((params: ParamMap) => {
this.openGraphService.waitFor('channel-map');
this.openGraphService.waitFor('channel-data');
this.error = null;
this.seoService.setTitle(`Channel: ${params.get('short_id')}`);
return this.lightningApiService.getChannel$(params.get('short_id'))
.pipe(
tap((data) => {
if (!data.node_left.longitude || !data.node_left.latitude ||
!data.node_right.longitude || !data.node_right.latitude) {
this.channelGeo = [];
} else {
this.channelGeo = [
data.node_left.public_key,
data.node_left.alias,
data.node_left.longitude, data.node_left.latitude,
data.node_right.public_key,
data.node_right.alias,
data.node_right.longitude, data.node_right.latitude,
];
}
this.openGraphService.waitOver('channel-data');
}),
catchError((err) => {
this.error = err;
this.openGraphService.fail('channel-map');
this.openGraphService.fail('channel-data');
return of(null);
})
);
})
);
}
onMapReady() {
this.openGraphService.waitOver('channel-map');
}
}

View File

@ -0,0 +1,28 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SharedModule } from '../shared/shared.module';
import { RouterModule } from '@angular/router';
import { GraphsModule } from '../graphs/graphs.module';
import { LightningModule } from './lightning.module';
import { LightningApiService } from './lightning-api.service';
import { NodePreviewComponent } from './node/node-preview.component';
import { LightningPreviewsRoutingModule } from './lightning-previews.routing.module';
import { ChannelPreviewComponent } from './channel/channel-preview.component';
@NgModule({
declarations: [
NodePreviewComponent,
ChannelPreviewComponent,
],
imports: [
CommonModule,
SharedModule,
RouterModule,
GraphsModule,
LightningPreviewsRoutingModule,
LightningModule,
],
providers: [
LightningApiService,
]
})
export class LightningPreviewsModule { }

View File

@ -0,0 +1,25 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { NodePreviewComponent } from './node/node-preview.component';
import { ChannelPreviewComponent } from './channel/channel-preview.component';
const routes: Routes = [
{
path: 'node/:public_key',
component: NodePreviewComponent,
},
{
path: 'channel/:short_id',
component: ChannelPreviewComponent,
},
{
path: '**',
redirectTo: ''
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class LightningPreviewsRoutingModule { }

View File

@ -53,6 +53,27 @@ import { NodesChannelsMap } from '../lightning/nodes-channels-map/nodes-channels
LightningRoutingModule, LightningRoutingModule,
GraphsModule, GraphsModule,
], ],
exports: [
LightningDashboardComponent,
NodesListComponent,
NodeStatisticsComponent,
NodeStatisticsChartComponent,
NodeComponent,
ChannelsListComponent,
ChannelComponent,
LightningWrapperComponent,
ChannelBoxComponent,
ClosingTypeComponent,
LightningStatisticsChartComponent,
NodesNetworksChartComponent,
ChannelsStatisticsComponent,
NodesPerISPChartComponent,
NodesPerCountry,
NodesPerISP,
NodesPerCountryChartComponent,
NodesMap,
NodesChannelsMap,
],
providers: [ providers: [
LightningApiService, LightningApiService,
] ]

View File

@ -0,0 +1,64 @@
<div class="box preview-box" *ngIf="(node$ | async) as node">
<div class="row d-flex justify-content-between full-width-row">
<h1 class="title">
<span i18n="lightning.node">Node</span>:
<a [routerLink]="['/lightning/node' | relativeUrl, node.id]"> {{ node.alias }}</a>
</h1>
<div class="badges mb-2">
<span class="badge rounded-pill badge-success" *ngFor="let socketType of socketTypes">{{ socketType }}</span>
</div>
</div>
<div class="row">
<div class="col-md">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td i18n="lightning.active-capacity">Active capacity</td>
<td>
<app-amount [satoshis]="node.capacity" [noFiat]="true"></app-amount>
</td>
</tr>
<tr>
<td i18n="lightning.active-channels">Active channels</td>
<td>
{{ node.active_channel_count }}
</td>
</tr>
<tr>
<td i18n="lightning.active-channels-avg">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>
<span>{{ node.city.en }}</span>
</td>
</tr>
<tr *ngIf="node.country">
<td i18n="country">Country</td>
<td>
{{ node.country.en }} {{ node.flag }}
</td>
</tr>
<tr *ngIf="!node.city && !node.country">
<td i18n="location">Location</td>
<td>
<span>unknown</span>
</td>
</tr>
</tbody>
</table>
</div>
<div class="col-md map-col">
<app-nodes-channels-map *ngIf="!error" [style]="'nodepage'" [publicKey]="node.public_key" [fitContainer]="true" (readyEvent)="onMapReady()"></app-nodes-channels-map>
</div>
</div>
</div>
<ng-template [ngIf]="error">
<div class="text-center">
<span i18n="error.general-loading-data">Error loading data.</span>
</div>
</ng-template>

View File

@ -0,0 +1,43 @@
.title {
font-size: 52px;
margin-bottom: 0;
}
.table {
margin-top: 48px;
font-size: 32px;
}
.badges {
font-size: 28px;
::ng-deep .badge {
margin-left: 0.5em;
}
}
.map-col {
flex-grow: 0;
flex-shrink: 0;
width: 470px;
height: 390px;
min-width: 470px;
min-height: 390px;
max-height: 390px;
padding: 0;
background: #181b2d;
overflow: hidden;
margin-top: 18px;
}
.row {
margin-right: 0;
}
.full-width-row {
padding-left: 15px;
}
::ng-deep .symbol {
font-size: 24px;
}

View File

@ -0,0 +1,105 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { ActivatedRoute, ParamMap } from '@angular/router';
import { Observable } from 'rxjs';
import { catchError, map, switchMap } from 'rxjs/operators';
import { SeoService } from 'src/app/services/seo.service';
import { OpenGraphService } from 'src/app/services/opengraph.service';
import { getFlagEmoji } from 'src/app/shared/graphs.utils';
import { LightningApiService } from '../lightning-api.service';
import { isMobile } from '../../shared/common.utils';
@Component({
selector: 'app-node-preview',
templateUrl: './node-preview.component.html',
styleUrls: ['./node-preview.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class NodePreviewComponent implements OnInit {
node$: Observable<any>;
statistics$: Observable<any>;
publicKey$: Observable<string>;
selectedSocketIndex = 0;
qrCodeVisible = false;
channelsListStatus: string;
error: Error;
publicKey: string;
socketTypes: string[];
publicKeySize = 99;
constructor(
private lightningApiService: LightningApiService,
private activatedRoute: ActivatedRoute,
private seoService: SeoService,
private openGraphService: OpenGraphService,
) {
if (isMobile()) {
this.publicKeySize = 12;
}
}
ngOnInit(): void {
this.node$ = this.activatedRoute.paramMap
.pipe(
switchMap((params: ParamMap) => {
this.openGraphService.waitFor('node-map');
this.openGraphService.waitFor('node-data');
this.publicKey = params.get('public_key');
return this.lightningApiService.getNode$(params.get('public_key'));
}),
map((node) => {
this.seoService.setTitle(`Node: ${node.alias}`);
const socketsObject = [];
const socketTypesMap = {};
for (const socket of node.sockets.split(',')) {
if (socket === '') {
continue;
}
let label = '';
if (socket.match(/(?:[0-9]{1,3}\.){3}[0-9]{1,3}/)) {
label = 'IPv4';
} else if (socket.indexOf('[') > -1) {
label = 'IPv6';
} else if (socket.indexOf('onion') > -1) {
label = 'Tor';
}
node.flag = getFlagEmoji(node.iso_code);
socketsObject.push({
label: label,
socket: node.public_key + '@' + socket,
});
socketTypesMap[label] = true
}
node.socketsObject = socketsObject;
this.socketTypes = Object.keys(socketTypesMap);
node.avgCapacity = node.capacity / Math.max(1, node.active_channel_count);
this.openGraphService.waitOver('node-data');
return node;
}),
catchError(err => {
this.error = err;
this.openGraphService.fail('node-map');
this.openGraphService.fail('node-data');
return [{
alias: this.publicKey,
public_key: this.publicKey,
}];
})
);
}
changeSocket(index: number) {
this.selectedSocketIndex = index;
}
onChannelsListStatusChanged(e) {
this.channelsListStatus = e;
}
onMapReady() {
this.openGraphService.waitOver('node-map');
}
}

View File

@ -1,4 +1,4 @@
<div [class]="'full-container ' + style"> <div [class]="'full-container ' + style + (fitContainer ? ' fit-container' : '')">
<div *ngIf="style === 'graph'" class="card-header"> <div *ngIf="style === 'graph'" class="card-header">
<div class="d-flex d-md-block align-items-baseline" style="margin-bottom: -5px"> <div class="d-flex d-md-block align-items-baseline" style="margin-bottom: -5px">
@ -8,7 +8,7 @@
</div> </div>
<div *ngIf="observable$ | async" class="chart" echarts [initOpts]="chartInitOptions" [options]="chartOptions" <div *ngIf="observable$ | async" class="chart" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
(chartInit)="onChartInit($event)"> (chartInit)="onChartInit($event)" (chartFinished)="onChartFinished($event)">
</div> </div>
</div> </div>

View File

@ -29,6 +29,18 @@
min-height: 250px; min-height: 250px;
} }
.full-container.fit-container {
margin: 0;
padding: 0;
height: 100%;
min-height: 100px;
.chart {
padding: 0;
min-height: 100px;
}
}
.widget { .widget {
width: 90vw; width: 90vw;
margin-left: auto; margin-left: auto;

View File

@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, Component, HostListener, Input, NgZone, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { ChangeDetectionStrategy, Component, HostListener, Input, Output, EventEmitter, NgZone, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { SeoService } from 'src/app/services/seo.service'; import { SeoService } from 'src/app/services/seo.service';
import { ApiService } from 'src/app/services/api.service'; import { ApiService } from 'src/app/services/api.service';
import { Observable, switchMap, tap, zip } from 'rxjs'; import { Observable, switchMap, tap, zip } from 'rxjs';
@ -20,6 +20,8 @@ export class NodesChannelsMap implements OnInit, OnDestroy {
@Input() style: 'graph' | 'nodepage' | 'widget' | 'channelpage' = 'graph'; @Input() style: 'graph' | 'nodepage' | 'widget' | 'channelpage' = 'graph';
@Input() publicKey: string | undefined; @Input() publicKey: string | undefined;
@Input() channel: any[] = []; @Input() channel: any[] = [];
@Input() fitContainer = false;
@Output() readyEvent = new EventEmitter();
observable$: Observable<any>; observable$: Observable<any>;
@ -313,4 +315,8 @@ export class NodesChannelsMap implements OnInit, OnDestroy {
this.chartInstance.setOption(chartOptions); this.chartInstance.setOption(chartOptions);
}); });
} }
onChartFinished(e) {
this.readyEvent.emit();
}
} }

View File

@ -71,11 +71,13 @@ const defaultEnv: Env = {
export class StateService { export class StateService {
isBrowser: boolean = isPlatformBrowser(this.platformId); isBrowser: boolean = isPlatformBrowser(this.platformId);
network = ''; network = '';
lightning = false;
blockVSize: number; blockVSize: number;
env: Env; env: Env;
latestBlockHeight = -1; latestBlockHeight = -1;
networkChanged$ = new ReplaySubject<string>(1); networkChanged$ = new ReplaySubject<string>(1);
lightningChanged$ = new ReplaySubject<boolean>(1);
blocks$: ReplaySubject<[BlockExtended, boolean]>; blocks$: ReplaySubject<[BlockExtended, boolean]>;
transactions$ = new ReplaySubject<TransactionStripped>(6); transactions$ = new ReplaySubject<TransactionStripped>(6);
conversions$ = new ReplaySubject<any>(1); conversions$ = new ReplaySubject<any>(1);
@ -122,15 +124,18 @@ export class StateService {
if (this.isBrowser) { if (this.isBrowser) {
this.setNetworkBasedonUrl(window.location.pathname); this.setNetworkBasedonUrl(window.location.pathname);
this.setLightningBasedonUrl(window.location.pathname);
this.isTabHidden$ = fromEvent(document, 'visibilitychange').pipe(map(() => this.isHidden()), shareReplay()); this.isTabHidden$ = fromEvent(document, 'visibilitychange').pipe(map(() => this.isHidden()), shareReplay());
} else { } else {
this.setNetworkBasedonUrl('/'); this.setNetworkBasedonUrl('/');
this.setLightningBasedonUrl('/');
this.isTabHidden$ = new BehaviorSubject(false); this.isTabHidden$ = new BehaviorSubject(false);
} }
this.router.events.subscribe((event) => { this.router.events.subscribe((event) => {
if (event instanceof NavigationStart) { if (event instanceof NavigationStart) {
this.setNetworkBasedonUrl(event.url); this.setNetworkBasedonUrl(event.url);
this.setLightningBasedonUrl(event.url);
} }
}); });
@ -198,6 +203,15 @@ export class StateService {
} }
} }
setLightningBasedonUrl(url: string) {
if (this.env.BASE_MODULE !== 'mempool') {
return;
}
const networkMatches = url.match(/\/lightning\//);
this.lightning = !!networkMatches;
this.lightningChanged$.next(this.lightning);
}
getHiddenProp(){ getHiddenProp(){
const prefixes = ['webkit', 'moz', 'ms', 'o']; const prefixes = ['webkit', 'moz', 'ms', 'o'];
if ('hidden' in document) { return 'hidden'; } if ('hidden' in document) { return 'hidden'; }

View File

@ -4,7 +4,7 @@ import { NgbCollapse, NgbCollapseModule, NgbRadioGroup, NgbTypeaheadModule } fro
import { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontawesome'; import { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontawesome';
import { faFilter, faAngleDown, faAngleUp, faAngleRight, faAngleLeft, faBolt, faChartArea, faCogs, faCubes, faHammer, faDatabase, faExchangeAlt, faInfoCircle, import { faFilter, faAngleDown, faAngleUp, faAngleRight, faAngleLeft, faBolt, faChartArea, faCogs, faCubes, faHammer, faDatabase, faExchangeAlt, faInfoCircle,
faLink, faList, faSearch, faCaretUp, faCaretDown, faTachometerAlt, faThList, faTint, faTv, faAngleDoubleDown, faSortUp, faAngleDoubleUp, faChevronDown, faLink, faList, faSearch, faCaretUp, faCaretDown, faTachometerAlt, faThList, faTint, faTv, faAngleDoubleDown, faSortUp, faAngleDoubleUp, faChevronDown,
faFileAlt, faRedoAlt, faArrowAltCircleRight, faExternalLinkAlt, faBook, faListUl, faDownload, faQrcode } from '@fortawesome/free-solid-svg-icons'; faFileAlt, faRedoAlt, faArrowAltCircleRight, faExternalLinkAlt, faBook, faListUl, faDownload, faQrcode, faArrowRightArrowLeft } from '@fortawesome/free-solid-svg-icons';
import { InfiniteScrollModule } from 'ngx-infinite-scroll'; import { InfiniteScrollModule } from 'ngx-infinite-scroll';
import { MasterPageComponent } from '../components/master-page/master-page.component'; import { MasterPageComponent } from '../components/master-page/master-page.component';
import { MasterPagePreviewComponent } from '../components/master-page-preview/master-page-preview.component'; import { MasterPagePreviewComponent } from '../components/master-page-preview/master-page-preview.component';
@ -64,7 +64,6 @@ import { DifficultyComponent } from '../components/difficulty/difficulty.compone
import { TermsOfServiceComponent } from '../components/terms-of-service/terms-of-service.component'; import { TermsOfServiceComponent } from '../components/terms-of-service/terms-of-service.component';
import { PrivacyPolicyComponent } from '../components/privacy-policy/privacy-policy.component'; import { PrivacyPolicyComponent } from '../components/privacy-policy/privacy-policy.component';
import { TrademarkPolicyComponent } from '../components/trademark-policy/trademark-policy.component'; import { TrademarkPolicyComponent } from '../components/trademark-policy/trademark-policy.component';
import { SponsorComponent } from '../components/sponsor/sponsor.component';
import { PushTransactionComponent } from '../components/push-transaction/push-transaction.component'; import { PushTransactionComponent } from '../components/push-transaction/push-transaction.component';
import { AssetsFeaturedComponent } from '../components/assets/assets-featured/assets-featured.component'; import { AssetsFeaturedComponent } from '../components/assets/assets-featured/assets-featured.component';
import { AssetGroupComponent } from '../components/assets/asset-group/asset-group.component'; import { AssetGroupComponent } from '../components/assets/asset-group/asset-group.component';
@ -139,7 +138,6 @@ import { ToggleComponent } from './components/toggle/toggle.component';
TermsOfServiceComponent, TermsOfServiceComponent,
PrivacyPolicyComponent, PrivacyPolicyComponent,
TrademarkPolicyComponent, TrademarkPolicyComponent,
SponsorComponent,
PushTransactionComponent, PushTransactionComponent,
AssetsNavComponent, AssetsNavComponent,
AssetsFeaturedComponent, AssetsFeaturedComponent,
@ -242,7 +240,6 @@ import { ToggleComponent } from './components/toggle/toggle.component';
TermsOfServiceComponent, TermsOfServiceComponent,
PrivacyPolicyComponent, PrivacyPolicyComponent,
TrademarkPolicyComponent, TrademarkPolicyComponent,
SponsorComponent,
PushTransactionComponent, PushTransactionComponent,
AssetsNavComponent, AssetsNavComponent,
AssetsFeaturedComponent, AssetsFeaturedComponent,
@ -300,5 +297,6 @@ export class SharedModule {
library.addIcons(faListUl); library.addIcons(faListUl);
library.addIcons(faDownload); library.addIcons(faDownload);
library.addIcons(faQrcode); library.addIcons(faQrcode);
library.addIcons(faArrowRightArrowLeft);
} }
} }

View File

@ -150,12 +150,27 @@ class Server {
} }
// handle supported preview routes // handle supported preview routes
if (parts[0] === 'block') { switch (parts[0]) {
ogTitle = `Block: ${parts[1]}`; case 'block':
} else if (parts[0] === 'address') { ogTitle = `Block: ${parts[1]}`;
ogTitle = `Address: ${parts[1]}`; break;
} else { case 'address':
previewSupported = false; ogTitle = `Address: ${parts[1]}`;
break;
case 'lightning':
switch (parts[1]) {
case 'node':
ogTitle = `Lightning Node: ${parts[2]}`;
break;
case 'channel':
ogTitle = `Lightning Channel: ${parts[2]}`;
break;
default:
previewSupported = false;
}
break;
default:
previewSupported = false;
} }
if (previewSupported) { if (previewSupported) {