Merge branch 'master' into better-block-unfurls
This commit is contained in:
@@ -5,9 +5,9 @@ const P2SH_P2WSH_COST = 35 * 4; // the WU cost for the non-witness part of P2SH
|
||||
|
||||
export function calcSegwitFeeGains(tx: Transaction) {
|
||||
// calculated in weight units
|
||||
let realizedBech32Gains = 0;
|
||||
let potentialBech32Gains = 0;
|
||||
let potentialP2shGains = 0;
|
||||
let realizedSegwitGains = 0;
|
||||
let potentialSegwitGains = 0;
|
||||
let potentialP2shSegwitGains = 0;
|
||||
let potentialTaprootGains = 0;
|
||||
let realizedTaprootGains = 0;
|
||||
|
||||
@@ -24,31 +24,33 @@ export function calcSegwitFeeGains(tx: Transaction) {
|
||||
const isP2tr = vin.prevout.scriptpubkey_type === 'v1_p2tr';
|
||||
|
||||
const op = vin.scriptsig ? vin.scriptsig_asm.split(' ')[0] : null;
|
||||
const isP2sh2Wpkh = isP2sh && !!vin.witness && op === 'OP_PUSHBYTES_22';
|
||||
const isP2sh2Wsh = isP2sh && !!vin.witness && op === 'OP_PUSHBYTES_34';
|
||||
const isP2shP2Wpkh = isP2sh && !!vin.witness && op === 'OP_PUSHBYTES_22';
|
||||
const isP2shP2Wsh = isP2sh && !!vin.witness && op === 'OP_PUSHBYTES_34';
|
||||
|
||||
switch (true) {
|
||||
// Native Segwit - P2WPKH/P2WSH (Bech32)
|
||||
// Native Segwit - P2WPKH/P2WSH/P2TR
|
||||
case isP2wpkh:
|
||||
case isP2wsh:
|
||||
case isP2tr:
|
||||
// maximal gains: the scriptSig is moved entirely to the witness part
|
||||
realizedBech32Gains += witnessSize(vin) * 3;
|
||||
// if taproot is used savings are 42 WU higher because it produces smaller signatures and doesn't require a pubkey in the witness
|
||||
// this number is explained above `realizedTaprootGains += 42;`
|
||||
realizedSegwitGains += (witnessSize(vin) + (isP2tr ? 42 : 0)) * 3;
|
||||
// XXX P2WSH output creation is more expensive, should we take this into consideration?
|
||||
break;
|
||||
|
||||
// Backward compatible Segwit - P2SH-P2WPKH
|
||||
case isP2sh2Wpkh:
|
||||
// the scriptSig is moved to the witness, but we have extra 21 extra non-witness bytes (48 WU)
|
||||
realizedBech32Gains += witnessSize(vin) * 3 - P2SH_P2WPKH_COST;
|
||||
potentialBech32Gains += P2SH_P2WPKH_COST;
|
||||
case isP2shP2Wpkh:
|
||||
// the scriptSig is moved to the witness, but we have extra 21 extra non-witness bytes (84 WU)
|
||||
realizedSegwitGains += witnessSize(vin) * 3 - P2SH_P2WPKH_COST;
|
||||
potentialSegwitGains += P2SH_P2WPKH_COST;
|
||||
break;
|
||||
|
||||
// Backward compatible Segwit - P2SH-P2WSH
|
||||
case isP2sh2Wsh:
|
||||
// the scriptSig is moved to the witness, but we have extra 35 extra non-witness bytes
|
||||
realizedBech32Gains += witnessSize(vin) * 3 - P2SH_P2WSH_COST;
|
||||
potentialBech32Gains += P2SH_P2WSH_COST;
|
||||
case isP2shP2Wsh:
|
||||
// the scriptSig is moved to the witness, but we have extra 35 extra non-witness bytes (140 WU)
|
||||
realizedSegwitGains += witnessSize(vin) * 3 - P2SH_P2WSH_COST;
|
||||
potentialSegwitGains += P2SH_P2WSH_COST;
|
||||
break;
|
||||
|
||||
// Non-segwit P2PKH/P2SH/P2PK/bare multisig
|
||||
@@ -56,9 +58,13 @@ export function calcSegwitFeeGains(tx: Transaction) {
|
||||
case isP2sh:
|
||||
case isP2pk:
|
||||
case isBareMultisig: {
|
||||
const fullGains = scriptSigSize(vin) * 3;
|
||||
potentialBech32Gains += fullGains;
|
||||
potentialP2shGains += fullGains - (isP2pkh ? P2SH_P2WPKH_COST : P2SH_P2WSH_COST);
|
||||
let fullGains = scriptSigSize(vin) * 3;
|
||||
if (isBareMultisig) {
|
||||
// a _bare_ multisig has the keys in the output script, but P2SH and P2WSH require them to be in the scriptSig/scriptWitness
|
||||
fullGains -= vin.prevout.scriptpubkey.length / 2;
|
||||
}
|
||||
potentialSegwitGains += fullGains;
|
||||
potentialP2shSegwitGains += fullGains - (isP2pkh ? P2SH_P2WPKH_COST : P2SH_P2WSH_COST);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -79,11 +85,11 @@ export function calcSegwitFeeGains(tx: Transaction) {
|
||||
// TODO maybe add some complex scripts that are specified somewhere, so that size is known, such as lightning scripts
|
||||
}
|
||||
} else {
|
||||
const script = isP2sh2Wsh || isP2wsh ? vin.inner_witnessscript_asm : vin.inner_redeemscript_asm;
|
||||
const script = isP2shP2Wsh || isP2wsh ? vin.inner_witnessscript_asm : vin.inner_redeemscript_asm;
|
||||
let replacementSize: number;
|
||||
if (
|
||||
// single sig
|
||||
isP2pk || isP2pkh || isP2wpkh || isP2sh2Wpkh ||
|
||||
isP2pk || isP2pkh || isP2wpkh || isP2shP2Wpkh ||
|
||||
// multisig
|
||||
isBareMultisig || parseMultisigScript(script)
|
||||
) {
|
||||
@@ -105,11 +111,11 @@ export function calcSegwitFeeGains(tx: Transaction) {
|
||||
|
||||
// returned as percentage of the total tx weight
|
||||
return {
|
||||
realizedBech32Gains: realizedBech32Gains / (tx.weight + realizedBech32Gains), // percent of the pre-segwit tx size
|
||||
potentialBech32Gains: potentialBech32Gains / tx.weight,
|
||||
potentialP2shGains: potentialP2shGains / tx.weight,
|
||||
realizedSegwitGains: realizedSegwitGains / (tx.weight + realizedSegwitGains), // percent of the pre-segwit tx size
|
||||
potentialSegwitGains: potentialSegwitGains / tx.weight,
|
||||
potentialP2shSegwitGains: potentialP2shSegwitGains / tx.weight,
|
||||
potentialTaprootGains: potentialTaprootGains / tx.weight,
|
||||
realizedTaprootGains: realizedTaprootGains / tx.weight
|
||||
realizedTaprootGains: realizedTaprootGains / (tx.weight + realizedTaprootGains)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -188,12 +194,12 @@ export function moveDec(num: number, n: number) {
|
||||
return neg + (int || '0') + (frac.length ? '.' + frac : '');
|
||||
}
|
||||
|
||||
function zeros(n) {
|
||||
function zeros(n: number) {
|
||||
return new Array(n + 1).join('0');
|
||||
}
|
||||
|
||||
// Formats a number for display. Treats the number as a string to avoid rounding errors.
|
||||
export const formatNumber = (s, precision = null) => {
|
||||
export const formatNumber = (s: number | string, precision: number | null = null) => {
|
||||
let [ whole, dec ] = s.toString().split('.');
|
||||
|
||||
// divide numbers into groups of three separated with a thin space (U+202F, "NARROW NO-BREAK SPACE"),
|
||||
@@ -219,27 +225,27 @@ const witnessSize = (vin: Vin) => vin.witness ? vin.witness.reduce((S, w) => S +
|
||||
const scriptSigSize = (vin: Vin) => vin.scriptsig ? vin.scriptsig.length / 2 : 0;
|
||||
|
||||
// Power of ten wrapper
|
||||
export function selectPowerOfTen(val: number) {
|
||||
export function selectPowerOfTen(val: number): { divider: number, unit: string } {
|
||||
const powerOfTen = {
|
||||
exa: Math.pow(10, 18),
|
||||
peta: Math.pow(10, 15),
|
||||
terra: Math.pow(10, 12),
|
||||
tera: Math.pow(10, 12),
|
||||
giga: Math.pow(10, 9),
|
||||
mega: Math.pow(10, 6),
|
||||
kilo: Math.pow(10, 3),
|
||||
};
|
||||
|
||||
let selectedPowerOfTen;
|
||||
let selectedPowerOfTen: { divider: number, unit: string };
|
||||
if (val < powerOfTen.kilo) {
|
||||
selectedPowerOfTen = { divider: 1, unit: '' }; // no scaling
|
||||
} else if (val < powerOfTen.mega) {
|
||||
selectedPowerOfTen = { divider: powerOfTen.kilo, unit: 'k' };
|
||||
} else if (val < powerOfTen.giga) {
|
||||
selectedPowerOfTen = { divider: powerOfTen.mega, unit: 'M' };
|
||||
} else if (val < powerOfTen.terra) {
|
||||
} else if (val < powerOfTen.tera) {
|
||||
selectedPowerOfTen = { divider: powerOfTen.giga, unit: 'G' };
|
||||
} else if (val < powerOfTen.peta) {
|
||||
selectedPowerOfTen = { divider: powerOfTen.terra, unit: 'T' };
|
||||
selectedPowerOfTen = { divider: powerOfTen.tera, unit: 'T' };
|
||||
} else if (val < powerOfTen.exa) {
|
||||
selectedPowerOfTen = { divider: powerOfTen.peta, unit: 'P' };
|
||||
} else {
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
</div>
|
||||
</ng-template>
|
||||
<ng-container *ngIf="{ val: connectionState$ | async } as connectionState">
|
||||
<img *ngIf="!officialMempoolSpace" src="/resources/mempool-logo.png" height="35" width="140" class="logo" [ngStyle]="{'opacity': connectionState.val === 2 ? 1 : 0.5 }" alt="The Mempool Open Source Project logo">
|
||||
<app-svg-images *ngIf="officialMempoolSpace" name="officialMempoolSpace" style="width: 140px; height: 35px" width="500" height="126" viewBox="0 0 500 126"></app-svg-images>
|
||||
<img *ngIf="!officialMempoolSpace" src="/resources/mempool-logo.png" class="mempool-logo" [ngStyle]="{'opacity': connectionState.val === 2 ? 1 : 0.5 }" alt="The Mempool Open Source Project logo">
|
||||
<app-svg-images *ngIf="officialMempoolSpace" name="officialMempoolSpace" viewBox="0 0 500 126"></app-svg-images>
|
||||
<div class="connection-badge">
|
||||
<div class="badge badge-warning" *ngIf="connectionState.val === 0" i18n="master-page.offline">Offline</div>
|
||||
<div class="badge badge-warning" *ngIf="connectionState.val === 1" i18n="master-page.reconnecting">Reconnecting...</div>
|
||||
|
||||
@@ -78,6 +78,7 @@ li.nav-item {
|
||||
|
||||
.navbar-brand {
|
||||
position: relative;
|
||||
height: 65px;
|
||||
}
|
||||
|
||||
nav {
|
||||
@@ -86,7 +87,7 @@ nav {
|
||||
|
||||
.connection-badge {
|
||||
position: absolute;
|
||||
top: 13px;
|
||||
top: 22px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -150,6 +151,7 @@ nav {
|
||||
width: 140px;
|
||||
margin-right: 15px;
|
||||
text-align: center;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.logo-holder {
|
||||
@@ -161,3 +163,9 @@ nav {
|
||||
flex-direction: row;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.mempool-logo, app-svg-images {
|
||||
align-self: center;
|
||||
width: 140px;
|
||||
height: 35px;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<span *ngIf="segwitGains.realizedBech32Gains && !segwitGains.potentialBech32Gains; else segwitTwo" class="badge badge-success mr-1" i18n-ngbTooltip="ngbTooltip about segwit gains" ngbTooltip="This transaction saved {{ segwitGains.realizedBech32Gains * 100 | number: '1.0-0' }}% on fees by using native SegWit-Bech32" placement="bottom" i18n="tx-features.tag.segwit|SegWit">SegWit</span>
|
||||
<span *ngIf="segwitGains.realizedSegwitGains && !segwitGains.potentialSegwitGains; else segwitTwo" class="badge badge-success mr-1" i18n-ngbTooltip="ngbTooltip about segwit gains" ngbTooltip="This transaction saved {{ segwitGains.realizedSegwitGains * 100 | number: '1.0-0' }}% on fees by using native SegWit" placement="bottom" i18n="tx-features.tag.segwit|SegWit">SegWit</span>
|
||||
<ng-template #segwitTwo>
|
||||
<span *ngIf="segwitGains.realizedBech32Gains && segwitGains.potentialBech32Gains; else potentialP2shGains" class="badge badge-warning mr-1" i18n-ngbTooltip="ngbTooltip about double segwit gains" ngbTooltip="This transaction saved {{ segwitGains.realizedBech32Gains * 100 | number: '1.0-0' }}% on fees by using SegWit and could save {{ segwitGains.potentialBech32Gains * 100 | number : '1.0-0' }}% more by fully upgrading to native SegWit-Bech32" placement="bottom" i18n="tx-features.tag.segwit|SegWit">SegWit</span>
|
||||
<ng-template #potentialP2shGains>
|
||||
<span *ngIf="segwitGains.potentialP2shGains" class="badge badge-danger mr-1" i18n-ngbTooltip="ngbTooltip about missed out gains" ngbTooltip="This transaction could save {{ segwitGains.potentialBech32Gains * 100 | number : '1.0-0' }}% on fees by upgrading to native SegWit-Bech32 or {{ segwitGains.potentialP2shGains * 100 | number: '1.0-0' }}% by upgrading to SegWit-P2SH" placement="bottom"><del i18n="tx-features.tag.segwit|SegWit">SegWit</del></span>
|
||||
<span *ngIf="segwitGains.realizedSegwitGains && segwitGains.potentialSegwitGains; else potentialP2shSegwitGains" class="badge badge-warning mr-1" i18n-ngbTooltip="ngbTooltip about double segwit gains" ngbTooltip="This transaction saved {{ segwitGains.realizedSegwitGains * 100 | number: '1.0-0' }}% on fees by using SegWit and could save {{ segwitGains.potentialSegwitGains * 100 | number : '1.0-0' }}% more by fully upgrading to native SegWit" placement="bottom" i18n="tx-features.tag.segwit|SegWit">SegWit</span>
|
||||
<ng-template #potentialP2shSegwitGains>
|
||||
<span *ngIf="segwitGains.potentialP2shSegwitGains" class="badge badge-danger mr-1" i18n-ngbTooltip="ngbTooltip about missed out gains" ngbTooltip="This transaction could save {{ segwitGains.potentialSegwitGains * 100 | number : '1.0-0' }}% on fees by upgrading to native SegWit or {{ segwitGains.potentialP2shSegwitGains * 100 | number: '1.0-0' }}% by upgrading to SegWit-P2SH" placement="bottom"><del i18n="tx-features.tag.segwit|SegWit">SegWit</del></span>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
|
||||
<span *ngIf="segwitGains.realizedTaprootGains && !segwitGains.potentialTaprootGains; else notFullyTaproot" class="badge badge-success mr-1" i18n-ngbTooltip="Tooltip about privacy and fees saved with taproot" ngbTooltip="This transaction uses Taproot and the user's thereby increased privacy and saved at least {{ segwitGains.realizedTaprootGains * 100 | number: '1.0-0' }}% on fees" placement="bottom" i18n="tx-features.tag.taproot|Taproot">Taproot</span>
|
||||
<span *ngIf="segwitGains.realizedTaprootGains && !segwitGains.potentialTaprootGains; else notFullyTaproot" class="badge badge-success mr-1" i18n-ngbTooltip="Tooltip about privacy and fees saved with taproot" ngbTooltip="This transaction uses Taproot and thereby increased the user's privacy and saved at least {{ segwitGains.realizedTaprootGains * 100 | number: '1.0-0' }}% on fees" placement="bottom" i18n="tx-features.tag.taproot|Taproot">Taproot</span>
|
||||
<ng-template #notFullyTaproot>
|
||||
<span *ngIf="segwitGains.realizedTaprootGains && segwitGains.potentialTaprootGains; else noTaproot" class="badge badge-warning mr-1" i18n-ngbTooltip="Tooltip about privacy and more fees that could be saved with more taproot" ngbTooltip="This transaction uses Taproot and thereby increased the user's privacy 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>
|
||||
|
||||
@@ -12,9 +12,9 @@ export class TxFeaturesComponent implements OnChanges {
|
||||
@Input() tx: Transaction;
|
||||
|
||||
segwitGains = {
|
||||
realizedBech32Gains: 0,
|
||||
potentialBech32Gains: 0,
|
||||
potentialP2shGains: 0,
|
||||
realizedSegwitGains: 0,
|
||||
potentialSegwitGains: 0,
|
||||
potentialP2shSegwitGains: 0,
|
||||
potentialTaprootGains: 0,
|
||||
realizedTaprootGains: 0
|
||||
};
|
||||
|
||||
@@ -169,9 +169,6 @@ export class NodeStatisticsChartComponent implements OnInit {
|
||||
},
|
||||
yAxis: data.channels.length === 0 ? undefined : [
|
||||
{
|
||||
min: (value) => {
|
||||
return value.min * 0.9;
|
||||
},
|
||||
type: 'value',
|
||||
axisLabel: {
|
||||
color: 'rgb(110, 112, 121)',
|
||||
@@ -188,9 +185,6 @@ export class NodeStatisticsChartComponent implements OnInit {
|
||||
},
|
||||
},
|
||||
{
|
||||
min: (value) => {
|
||||
return value.min * 0.9;
|
||||
},
|
||||
type: 'value',
|
||||
position: 'right',
|
||||
axisLabel: {
|
||||
@@ -225,15 +219,6 @@ export class NodeStatisticsChartComponent implements OnInit {
|
||||
opacity: 1,
|
||||
width: 1,
|
||||
},
|
||||
data: [{
|
||||
yAxis: 1,
|
||||
label: {
|
||||
position: 'end',
|
||||
show: true,
|
||||
color: '#ffffff',
|
||||
formatter: `1 MB`
|
||||
}
|
||||
}],
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -43,11 +43,23 @@
|
||||
</tr>
|
||||
<tr *ngIf="node.country && node.city && node.subdivision">
|
||||
<td i18n="location">Location</td>
|
||||
<td>{{ node.city.en }}, {{ node.subdivision.en }}<br>{{ node.country.en }}</td>
|
||||
<td>
|
||||
<span>{{ node.city.en }}, {{ node.subdivision.en }}</span>
|
||||
<br>
|
||||
<a class="d-flex align-items-center" [routerLink]="['/lightning/nodes/country' | relativeUrl, node.iso_code]">
|
||||
<span class="link">{{ node.country.en }}</span>
|
||||
|
||||
<span class="flag">{{ node.flag }}</span>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr *ngIf="node.country && !node.city">
|
||||
<td i18n="location">Location</td>
|
||||
<td>{{ node.country.en }}</td>
|
||||
<td>
|
||||
<a [routerLink]="['/lightning/nodes/country' | relativeUrl, node.iso_code]">
|
||||
{{ node.country.en }} {{ node.flag }}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -77,7 +89,9 @@
|
||||
<tr *ngIf="node.country">
|
||||
<td i18n="isp">ISP</td>
|
||||
<td>
|
||||
{{ node.as_organization }} [ASN {{node.as_number}}]
|
||||
<a [routerLink]="['/lightning/nodes/isp' | relativeUrl, node.as_number]">
|
||||
{{ node.as_organization }} [ASN {{node.as_number}}]
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -126,13 +140,8 @@
|
||||
|
||||
<div class="d-flex justify-content-between" *ngIf="!error">
|
||||
<h2>Channels ({{ channelsListStatus === 'open' ? node.channel_active_count : node.channel_closed_count }})</h2>
|
||||
<div class="d-flex align-items-center justify-content-end">
|
||||
<span style="margin-bottom: 0.5rem">List</span>
|
||||
<label class="switch">
|
||||
<input type="checkbox" (change)="channelsListModeChange($event)">
|
||||
<span class="slider round"></span>
|
||||
</label>
|
||||
<span style="margin-bottom: 0.5rem">Map</span>
|
||||
<div class="d-flex justify-content-end">
|
||||
<app-toggle [textLeft]="'List'" [textRight]="'Map'" (toggleStatusChanged)="channelsListModeChange($event)"></app-toggle>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -56,67 +56,4 @@ app-fiat {
|
||||
display: inline-block;
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
/* The switch - the box around the slider */
|
||||
.switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 30px;
|
||||
height: 17px;
|
||||
}
|
||||
|
||||
/* Hide default HTML checkbox */
|
||||
.switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/* The slider */
|
||||
.slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #ccc;
|
||||
-webkit-transition: .4s;
|
||||
transition: .4s;
|
||||
}
|
||||
|
||||
.slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 13px;
|
||||
width: 13px;
|
||||
left: 2px;
|
||||
bottom: 2px;
|
||||
background-color: white;
|
||||
-webkit-transition: .4s;
|
||||
transition: .4s;
|
||||
}
|
||||
|
||||
input:checked + .slider {
|
||||
background-color: #2196F3;
|
||||
}
|
||||
|
||||
input:focus + .slider {
|
||||
box-shadow: 0 0 1px #2196F3;
|
||||
}
|
||||
|
||||
input:checked + .slider:before {
|
||||
-webkit-transform: translateX(13px);
|
||||
-ms-transform: translateX(13px);
|
||||
transform: translateX(13px);
|
||||
}
|
||||
|
||||
/* Rounded sliders */
|
||||
.slider.round {
|
||||
border-radius: 17px;
|
||||
}
|
||||
|
||||
.slider.round:before {
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ 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 { getFlagEmoji } from 'src/app/shared/graphs.utils';
|
||||
import { LightningApiService } from '../lightning-api.service';
|
||||
|
||||
@Component({
|
||||
@@ -51,6 +52,7 @@ export class NodeComponent implements OnInit {
|
||||
} else if (socket.indexOf('onion') > -1) {
|
||||
label = 'Tor';
|
||||
}
|
||||
node.flag = getFlagEmoji(node.iso_code);
|
||||
socketsObject.push({
|
||||
label: label,
|
||||
socket: node.public_key + '@' + socket,
|
||||
@@ -73,8 +75,8 @@ export class NodeComponent implements OnInit {
|
||||
this.selectedSocketIndex = index;
|
||||
}
|
||||
|
||||
channelsListModeChange(e) {
|
||||
if (e.target.checked === true) {
|
||||
channelsListModeChange(toggle) {
|
||||
if (toggle === true) {
|
||||
this.channelsListMode = 'map';
|
||||
} else {
|
||||
this.channelsListMode = 'list';
|
||||
|
||||
@@ -59,7 +59,7 @@ export class NodesNetworksChartComponent implements OnInit {
|
||||
let firstRun = true;
|
||||
|
||||
if (this.widget) {
|
||||
this.miningWindowPreference = '1y';
|
||||
this.miningWindowPreference = '3y';
|
||||
} else {
|
||||
this.seoService.setTitle($localize`Lightning nodes per network`);
|
||||
this.miningWindowPreference = this.miningService.getDefaultTimespan('all');
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
<fa-icon [icon]="['fas', 'download']" [fixedWidth]="true"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
<small style="color: #ffffff66" i18n="lightning.tor-nodes-excluded">(Tor nodes excluded)</small>
|
||||
<small class="d-block" style="color: #ffffff66; min-height: 25px" i18n="lightning.tor-nodes-excluded">
|
||||
<span *ngIf="!(showTorObservable$ | async)">(Tor nodes excluded)</span>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="container pb-lg-0 bottom-padding">
|
||||
@@ -21,6 +23,11 @@
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex toggle">
|
||||
<app-toggle [textLeft]="'Show Tor'" [textRight]="" (toggleStatusChanged)="onTorToggleStatusChanged($event)"></app-toggle>
|
||||
<app-toggle [textLeft]="'Nodes'" [textRight]="'Capacity'" (toggleStatusChanged)="onGroupToggleStatusChanged($event)"></app-toggle>
|
||||
</div>
|
||||
|
||||
<table class="table table-borderless text-center m-auto" style="max-width: 900px">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -34,8 +41,9 @@
|
||||
<tbody [attr.data-cy]="'pools-table'" *ngIf="(nodesPerAsObservable$ | async) as asList">
|
||||
<tr *ngFor="let asEntry of asList">
|
||||
<td class="rank text-left pl-0">{{ asEntry.rank }}</td>
|
||||
<td class="name text-left text-truncate" style="max-width: 250px">
|
||||
<a [routerLink]="[('/lightning/nodes/isp/' + asEntry.ispId) | relativeUrl]">{{ asEntry.name }}</a>
|
||||
<td class="name text-left text-truncate">
|
||||
<a *ngIf="asEntry.ispId" [routerLink]="[('/lightning/nodes/isp/' + asEntry.ispId) | relativeUrl]">{{ asEntry.name }}</a>
|
||||
<span *ngIf="!asEntry.ispId">{{ asEntry.name }}</span>
|
||||
</td>
|
||||
<td class="share text-right">{{ asEntry.share }}%</td>
|
||||
<td class="nodes text-right">{{ asEntry.count }}</td>
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
.name {
|
||||
width: 25%;
|
||||
@media (max-width: 576px) {
|
||||
width: 80%;
|
||||
width: 70%;
|
||||
max-width: 150px;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
@@ -69,7 +69,17 @@
|
||||
.capacity {
|
||||
width: 20%;
|
||||
@media (max-width: 576px) {
|
||||
width: 10%;
|
||||
width: 20%;
|
||||
max-width: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle {
|
||||
justify-content: space-between;
|
||||
padding-top: 15px;
|
||||
@media (min-width: 576px) {
|
||||
padding-bottom: 15px;
|
||||
padding-left: 105px;
|
||||
padding-right: 105px;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ChangeDetectionStrategy, Component, OnInit, HostBinding, NgZone } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { EChartsOption, PieSeriesOption } from 'echarts';
|
||||
import { map, Observable, share, tap } from 'rxjs';
|
||||
import { combineLatest, map, Observable, share, Subject, switchMap, tap } from 'rxjs';
|
||||
import { chartColors } from 'src/app/app.constants';
|
||||
import { ApiService } from 'src/app/services/api.service';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
@@ -17,19 +17,20 @@ import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class NodesPerISPChartComponent implements OnInit {
|
||||
miningWindowPreference: string;
|
||||
|
||||
isLoading = true;
|
||||
chartOptions: EChartsOption = {};
|
||||
chartInitOptions = {
|
||||
renderer: 'svg',
|
||||
};
|
||||
timespan = '';
|
||||
chartInstance: any = undefined;
|
||||
chartInstance = undefined;
|
||||
|
||||
@HostBinding('attr.dir') dir = 'ltr';
|
||||
|
||||
nodesPerAsObservable$: Observable<any>;
|
||||
showTorObservable$: Observable<boolean>;
|
||||
groupBySubject = new Subject<boolean>();
|
||||
showTorSubject = new Subject<boolean>();
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
@@ -44,23 +45,32 @@ export class NodesPerISPChartComponent implements OnInit {
|
||||
ngOnInit(): void {
|
||||
this.seoService.setTitle($localize`Lightning nodes per ISP`);
|
||||
|
||||
this.nodesPerAsObservable$ = this.apiService.getNodesPerAs()
|
||||
this.showTorObservable$ = this.showTorSubject.asObservable();
|
||||
this.nodesPerAsObservable$ = combineLatest([this.groupBySubject, this.showTorSubject])
|
||||
.pipe(
|
||||
tap(data => {
|
||||
this.isLoading = false;
|
||||
this.prepareChartOptions(data);
|
||||
}),
|
||||
map(data => {
|
||||
for (let i = 0; i < data.length; ++i) {
|
||||
data[i].rank = i + 1;
|
||||
}
|
||||
return data.slice(0, 100);
|
||||
switchMap((selectedFilters) => {
|
||||
return this.apiService.getNodesPerAs(
|
||||
selectedFilters[0] ? 'capacity' : 'node-count',
|
||||
selectedFilters[1] // Show Tor nodes
|
||||
)
|
||||
.pipe(
|
||||
tap(data => {
|
||||
this.isLoading = false;
|
||||
this.prepareChartOptions(data);
|
||||
}),
|
||||
map(data => {
|
||||
for (let i = 0; i < data.length; ++i) {
|
||||
data[i].rank = i + 1;
|
||||
}
|
||||
return data.slice(0, 100);
|
||||
})
|
||||
);
|
||||
}),
|
||||
share()
|
||||
);
|
||||
}
|
||||
|
||||
generateChartSerieData(as) {
|
||||
generateChartSerieData(as): PieSeriesOption[] {
|
||||
const shareThreshold = this.isMobile() ? 2 : 0.5;
|
||||
const data: object[] = [];
|
||||
let totalShareOther = 0;
|
||||
@@ -78,6 +88,9 @@ export class NodesPerISPChartComponent implements OnInit {
|
||||
return;
|
||||
}
|
||||
data.push({
|
||||
itemStyle: {
|
||||
color: as.ispId === null ? '#7D4698' : undefined,
|
||||
},
|
||||
value: as.share,
|
||||
name: as.name + (this.isMobile() ? `` : ` (${as.share}%)`),
|
||||
label: {
|
||||
@@ -138,14 +151,14 @@ export class NodesPerISPChartComponent implements OnInit {
|
||||
return data;
|
||||
}
|
||||
|
||||
prepareChartOptions(as) {
|
||||
prepareChartOptions(as): void {
|
||||
let pieSize = ['20%', '80%']; // Desktop
|
||||
if (this.isMobile()) {
|
||||
pieSize = ['15%', '60%'];
|
||||
}
|
||||
|
||||
this.chartOptions = {
|
||||
color: chartColors,
|
||||
color: chartColors.slice(3),
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
textStyle: {
|
||||
@@ -191,18 +204,18 @@ export class NodesPerISPChartComponent implements OnInit {
|
||||
};
|
||||
}
|
||||
|
||||
isMobile() {
|
||||
isMobile(): boolean {
|
||||
return (window.innerWidth <= 767.98);
|
||||
}
|
||||
|
||||
onChartInit(ec) {
|
||||
onChartInit(ec): void {
|
||||
if (this.chartInstance !== undefined) {
|
||||
return;
|
||||
}
|
||||
this.chartInstance = ec;
|
||||
|
||||
this.chartInstance.on('click', (e) => {
|
||||
if (e.data.data === 9999) { // "Other"
|
||||
if (e.data.data === 9999 || e.data.data === null) { // "Other" or Tor
|
||||
return;
|
||||
}
|
||||
this.zone.run(() => {
|
||||
@@ -212,7 +225,7 @@ export class NodesPerISPChartComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
onSaveChart() {
|
||||
onSaveChart(): void {
|
||||
const now = new Date();
|
||||
this.chartOptions.backgroundColor = '#11131f';
|
||||
this.chartInstance.setOption(this.chartOptions);
|
||||
@@ -224,8 +237,12 @@ export class NodesPerISPChartComponent implements OnInit {
|
||||
this.chartInstance.setOption(this.chartOptions);
|
||||
}
|
||||
|
||||
isEllipsisActive(e) {
|
||||
return (e.offsetWidth < e.scrollWidth);
|
||||
onTorToggleStatusChanged(e): void {
|
||||
this.showTorSubject.next(e);
|
||||
}
|
||||
|
||||
onGroupToggleStatusChanged(e): void {
|
||||
this.groupBySubject.next(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ export class LightningStatisticsChartComponent implements OnInit {
|
||||
let firstRun = true;
|
||||
|
||||
if (this.widget) {
|
||||
this.miningWindowPreference = '1y';
|
||||
this.miningWindowPreference = '3y';
|
||||
} else {
|
||||
this.seoService.setTitle($localize`Channels and Capacity`);
|
||||
this.miningWindowPreference = this.miningService.getDefaultTimespan('all');
|
||||
|
||||
@@ -255,8 +255,9 @@ export class ApiService {
|
||||
return this.httpClient.get<any[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/search', { params });
|
||||
}
|
||||
|
||||
getNodesPerAs(): Observable<any> {
|
||||
return this.httpClient.get<any[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/nodes/isp');
|
||||
getNodesPerAs(groupBy: 'capacity' | 'node-count', showTorNodes: boolean): Observable<any> {
|
||||
return this.httpClient.get<any[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/nodes/isp-ranking'
|
||||
+ `?groupBy=${groupBy}&showTor=${showTorNodes}`);
|
||||
}
|
||||
|
||||
getNodeForCountry$(country: string): Observable<any> {
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<div class="d-flex align-items-center">
|
||||
<span style="margin-bottom: 0.5rem">{{ textLeft }}</span>
|
||||
<label class="switch">
|
||||
<input type="checkbox" (change)="onToggleStatusChanged($event)">
|
||||
<span class="slider round"></span>
|
||||
</label>
|
||||
<span style="margin-bottom: 0.5rem">{{ textRight }}</span>
|
||||
</div>
|
||||
@@ -0,0 +1,62 @@
|
||||
/* The switch - the box around the slider */
|
||||
.switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 30px;
|
||||
height: 17px;
|
||||
}
|
||||
|
||||
/* Hide default HTML checkbox */
|
||||
.switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/* The slider */
|
||||
.slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #ccc;
|
||||
-webkit-transition: .4s;
|
||||
transition: .4s;
|
||||
}
|
||||
|
||||
.slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 13px;
|
||||
width: 13px;
|
||||
left: 2px;
|
||||
bottom: 2px;
|
||||
background-color: white;
|
||||
-webkit-transition: .4s;
|
||||
transition: .4s;
|
||||
}
|
||||
|
||||
input:checked+.slider {
|
||||
background-color: #2196F3;
|
||||
}
|
||||
|
||||
input:focus+.slider {
|
||||
box-shadow: 0 0 1px #2196F3;
|
||||
}
|
||||
|
||||
input:checked+.slider:before {
|
||||
-webkit-transform: translateX(13px);
|
||||
-ms-transform: translateX(13px);
|
||||
transform: translateX(13px);
|
||||
}
|
||||
|
||||
/* Rounded sliders */
|
||||
.slider.round {
|
||||
border-radius: 17px;
|
||||
}
|
||||
|
||||
.slider.round:before {
|
||||
border-radius: 50%;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Component, Input, Output, ChangeDetectionStrategy, EventEmitter, AfterViewInit } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-toggle',
|
||||
templateUrl: './toggle.component.html',
|
||||
styleUrls: ['./toggle.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ToggleComponent implements AfterViewInit {
|
||||
@Output() toggleStatusChanged = new EventEmitter<boolean>();
|
||||
@Input() textLeft: string;
|
||||
@Input() textRight: string;
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.toggleStatusChanged.emit(false);
|
||||
}
|
||||
|
||||
onToggleStatusChanged(e): void {
|
||||
this.toggleStatusChanged.emit(e.target.checked);
|
||||
}
|
||||
}
|
||||
@@ -92,6 +92,9 @@ export function detectWebGL() {
|
||||
}
|
||||
|
||||
export function getFlagEmoji(countryCode) {
|
||||
if (!countryCode) {
|
||||
return '';
|
||||
}
|
||||
const codePoints = countryCode
|
||||
.toUpperCase()
|
||||
.split('')
|
||||
|
||||
@@ -80,6 +80,7 @@ import { ChangeComponent } from '../components/change/change.component';
|
||||
import { SatsComponent } from './components/sats/sats.component';
|
||||
import { SearchResultsComponent } from '../components/search-form/search-results/search-results.component';
|
||||
import { TimestampComponent } from './components/timestamp/timestamp.component';
|
||||
import { ToggleComponent } from './components/toggle/toggle.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@@ -154,6 +155,7 @@ import { TimestampComponent } from './components/timestamp/timestamp.component';
|
||||
SatsComponent,
|
||||
SearchResultsComponent,
|
||||
TimestampComponent,
|
||||
ToggleComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
@@ -255,6 +257,7 @@ import { TimestampComponent } from './components/timestamp/timestamp.component';
|
||||
SatsComponent,
|
||||
SearchResultsComponent,
|
||||
TimestampComponent,
|
||||
ToggleComponent,
|
||||
]
|
||||
})
|
||||
export class SharedModule {
|
||||
|
||||
Reference in New Issue
Block a user