Merge branch 'master' into add-advanced-and-2q
This commit is contained in:
@@ -152,15 +152,14 @@
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/resources",
|
||||
"src/robots.txt"
|
||||
"src/robots.txt",
|
||||
"src/config.js",
|
||||
"src/config.template.js"
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.scss",
|
||||
"node_modules/@fortawesome/fontawesome-svg-core/styles.css"
|
||||
],
|
||||
"scripts": [
|
||||
"generated-config.js"
|
||||
],
|
||||
"vendorChunk": true,
|
||||
"extractLicenses": false,
|
||||
"buildOptimizer": false,
|
||||
@@ -222,6 +221,10 @@
|
||||
"proxyConfig": "proxy.conf.local.js",
|
||||
"verbose": true
|
||||
},
|
||||
"local-esplora": {
|
||||
"proxyConfig": "proxy.conf.local-esplora.js",
|
||||
"verbose": true
|
||||
},
|
||||
"mixed": {
|
||||
"proxyConfig": "proxy.conf.mixed.js",
|
||||
"verbose": true
|
||||
|
||||
@@ -2,7 +2,8 @@ var fs = require('fs');
|
||||
const { spawnSync } = require('child_process');
|
||||
|
||||
const CONFIG_FILE_NAME = 'mempool-frontend-config.json';
|
||||
const GENERATED_CONFIG_FILE_NAME = 'generated-config.js';
|
||||
const GENERATED_CONFIG_FILE_NAME = 'src/resources/config.js';
|
||||
const GENERATED_TEMPLATE_CONFIG_FILE_NAME = 'src/resources/config.template.js';
|
||||
|
||||
let settings = [];
|
||||
let configContent = {};
|
||||
@@ -67,10 +68,17 @@ if (process.env.DOCKER_COMMIT_HASH) {
|
||||
|
||||
const newConfig = `(function (window) {
|
||||
window.__env = window.__env || {};${settings.reduce((str, obj) => `${str}
|
||||
window.__env.${obj.key} = ${ typeof obj.value === 'string' ? `'${obj.value}'` : obj.value };`, '')}
|
||||
window.__env.${obj.key} = ${typeof obj.value === 'string' ? `'${obj.value}'` : obj.value};`, '')}
|
||||
window.__env.GIT_COMMIT_HASH = '${gitCommitHash}';
|
||||
window.__env.PACKAGE_JSON_VERSION = '${packetJsonVersion}';
|
||||
}(global || this));`;
|
||||
}(this));`;
|
||||
|
||||
const newConfigTemplate = `(function (window) {
|
||||
window.__env = window.__env || {};${settings.reduce((str, obj) => `${str}
|
||||
window.__env.${obj.key} = ${typeof obj.value === 'string' ? `'\${__${obj.key}__}'` : `\${__${obj.key}__}`};`, '')}
|
||||
window.__env.GIT_COMMIT_HASH = '${gitCommitHash}';
|
||||
window.__env.PACKAGE_JSON_VERSION = '${packetJsonVersion}';
|
||||
}(this));`;
|
||||
|
||||
function readConfig(path) {
|
||||
try {
|
||||
@@ -89,6 +97,16 @@ function writeConfig(path, config) {
|
||||
}
|
||||
}
|
||||
|
||||
function writeConfigTemplate(path, config) {
|
||||
try {
|
||||
fs.writeFileSync(path, config, 'utf8');
|
||||
} catch (e) {
|
||||
throw new Error(e);
|
||||
}
|
||||
}
|
||||
|
||||
writeConfigTemplate(GENERATED_TEMPLATE_CONFIG_FILE_NAME, newConfigTemplate);
|
||||
|
||||
const currentConfig = readConfig(GENERATED_CONFIG_FILE_NAME);
|
||||
|
||||
if (currentConfig && currentConfig === newConfig) {
|
||||
@@ -106,4 +124,4 @@ if (currentConfig && currentConfig === newConfig) {
|
||||
console.log('NEW CONFIG: ', newConfig);
|
||||
writeConfig(GENERATED_CONFIG_FILE_NAME, newConfig);
|
||||
console.log(`${GENERATED_CONFIG_FILE_NAME} file updated`);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"serve:local-prod": "npm run generate-config && npm run ng -- serve -c local-prod",
|
||||
"serve:local-staging": "npm run generate-config && npm run ng -- serve -c local-staging",
|
||||
"start": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c local",
|
||||
"start:local-esplora": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c local-esplora",
|
||||
"start:stg": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c staging",
|
||||
"start:local-prod": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c local-prod",
|
||||
"start:local-staging": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c local-staging",
|
||||
|
||||
137
frontend/proxy.conf.local-esplora.js
Normal file
137
frontend/proxy.conf.local-esplora.js
Normal file
@@ -0,0 +1,137 @@
|
||||
const fs = require('fs');
|
||||
|
||||
const FRONTEND_CONFIG_FILE_NAME = 'mempool-frontend-config.json';
|
||||
|
||||
let configContent;
|
||||
|
||||
// Read frontend config
|
||||
try {
|
||||
const rawConfig = fs.readFileSync(FRONTEND_CONFIG_FILE_NAME);
|
||||
configContent = JSON.parse(rawConfig);
|
||||
console.log(`${FRONTEND_CONFIG_FILE_NAME} file found, using provided config`);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
if (e.code !== 'ENOENT') {
|
||||
throw new Error(e);
|
||||
} else {
|
||||
console.log(`${FRONTEND_CONFIG_FILE_NAME} file not found, using default config`);
|
||||
}
|
||||
}
|
||||
|
||||
let PROXY_CONFIG = [];
|
||||
|
||||
if (configContent && configContent.BASE_MODULE === 'liquid') {
|
||||
PROXY_CONFIG.push(...[
|
||||
{
|
||||
context: ['/liquid/api/v1/**'],
|
||||
target: `http://127.0.0.1:8999`,
|
||||
secure: false,
|
||||
ws: true,
|
||||
changeOrigin: true,
|
||||
proxyTimeout: 30000,
|
||||
pathRewrite: {
|
||||
"^/liquid": ""
|
||||
},
|
||||
},
|
||||
{
|
||||
context: ['/liquid/api/**'],
|
||||
target: `http://127.0.0.1:3000`,
|
||||
secure: false,
|
||||
changeOrigin: true,
|
||||
proxyTimeout: 30000,
|
||||
pathRewrite: {
|
||||
"^/liquid/api/": ""
|
||||
},
|
||||
},
|
||||
{
|
||||
context: ['/liquidtestnet/api/v1/**'],
|
||||
target: `http://127.0.0.1:8999`,
|
||||
secure: false,
|
||||
ws: true,
|
||||
changeOrigin: true,
|
||||
proxyTimeout: 30000,
|
||||
pathRewrite: {
|
||||
"^/liquidtestnet": ""
|
||||
},
|
||||
},
|
||||
{
|
||||
context: ['/liquidtestnet/api/**'],
|
||||
target: `http://127.0.0.1:3000`,
|
||||
secure: false,
|
||||
changeOrigin: true,
|
||||
proxyTimeout: 30000,
|
||||
pathRewrite: {
|
||||
"^/liquidtestnet/api/": "/"
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
if (configContent && configContent.BASE_MODULE === 'bisq') {
|
||||
PROXY_CONFIG.push(...[
|
||||
{
|
||||
context: ['/bisq/api/v1/ws'],
|
||||
target: `http://127.0.0.1:8999`,
|
||||
secure: false,
|
||||
ws: true,
|
||||
changeOrigin: true,
|
||||
proxyTimeout: 30000,
|
||||
pathRewrite: {
|
||||
"^/bisq": ""
|
||||
},
|
||||
},
|
||||
{
|
||||
context: ['/bisq/api/v1/**'],
|
||||
target: `http://127.0.0.1:8999`,
|
||||
secure: false,
|
||||
changeOrigin: true,
|
||||
proxyTimeout: 30000,
|
||||
},
|
||||
{
|
||||
context: ['/bisq/api/**'],
|
||||
target: `http://127.0.0.1:8999`,
|
||||
secure: false,
|
||||
changeOrigin: true,
|
||||
proxyTimeout: 30000,
|
||||
pathRewrite: {
|
||||
"^/bisq/api/": "/api/v1/bisq/"
|
||||
},
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
PROXY_CONFIG.push(...[
|
||||
{
|
||||
context: ['/testnet/api/v1/lightning/**'],
|
||||
target: `http://127.0.0.1:8999`,
|
||||
secure: false,
|
||||
changeOrigin: true,
|
||||
proxyTimeout: 30000,
|
||||
pathRewrite: {
|
||||
"^/testnet": ""
|
||||
},
|
||||
},
|
||||
{
|
||||
context: ['/api/v1/**'],
|
||||
target: `http://127.0.0.1:8999`,
|
||||
secure: false,
|
||||
ws: true,
|
||||
changeOrigin: true,
|
||||
proxyTimeout: 30000,
|
||||
},
|
||||
{
|
||||
context: ['/api/**'],
|
||||
target: `http://127.0.0.1:3000`,
|
||||
secure: false,
|
||||
changeOrigin: true,
|
||||
proxyTimeout: 30000,
|
||||
pathRewrite: {
|
||||
"^/api": ""
|
||||
},
|
||||
}
|
||||
]);
|
||||
|
||||
console.log(PROXY_CONFIG);
|
||||
|
||||
module.exports = PROXY_CONFIG;
|
||||
@@ -79,7 +79,7 @@ export const poolsColor = {
|
||||
'binancepool': '#1E88E5',
|
||||
'viabtc': '#039BE5',
|
||||
'btccom': '#00897B',
|
||||
'slushpool': '#00ACC1',
|
||||
'braiinspool': '#00ACC1',
|
||||
'sbicrypto': '#43A047',
|
||||
'marapool': '#7CB342',
|
||||
'luxor': '#C0CA33',
|
||||
|
||||
@@ -129,7 +129,7 @@
|
||||
<span>Gemini</span>
|
||||
</a>
|
||||
<a href="https://exodus.com/" target="_blank" title="Exodus">
|
||||
<svg width="80" height="80" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg width="81" height="81" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="250" cy="250" r="250" fill="#1F2033"/>
|
||||
<g clip-path="url(#clip0_2_14)">
|
||||
<path d="M411.042 178.303L271.79 87V138.048L361.121 196.097L350.612 229.351H271.79V271.648H350.612L361.121 304.903L271.79 362.952V414L411.042 322.989L388.271 250.646L411.042 178.303Z" fill="url(#paint0_linear_2_14)"/>
|
||||
@@ -274,6 +274,10 @@
|
||||
<img class="image" src="/resources/profile/schildbach.svg" />
|
||||
<span>Schildbach</span>
|
||||
</a>
|
||||
<a href="https://github.com/nunchuk-io" target="_blank" title="Nunchuck">
|
||||
<img class="image" src="/resources/profile/nunchuk.svg" />
|
||||
<span>Nunchuk</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
text-align: center;
|
||||
|
||||
.image {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
width: 81px;
|
||||
height: 81px;
|
||||
background-size: 100%, 100%;
|
||||
border-radius: 50%;
|
||||
margin: 25px;
|
||||
|
||||
@@ -41,10 +41,6 @@
|
||||
</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>
|
||||
@@ -61,6 +57,10 @@
|
||||
<div class="col-sm" *ngIf="blockAudit">
|
||||
<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.health">Block health</td>
|
||||
<td>{{ blockAudit.matchRate }}%</td>
|
||||
@@ -69,18 +69,10 @@
|
||||
<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>
|
||||
@@ -97,21 +89,6 @@
|
||||
</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">
|
||||
@@ -123,7 +100,6 @@
|
||||
<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>
|
||||
@@ -136,7 +112,6 @@
|
||||
<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>
|
||||
@@ -180,16 +155,16 @@
|
||||
<div class="col-sm" *ngIf="webGlEnabled">
|
||||
<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>
|
||||
[blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx"
|
||||
(txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)"></app-block-overview-graph>
|
||||
</div>
|
||||
|
||||
<!-- ADDED TX RENDERING -->
|
||||
<div class="col-sm" *ngIf="webGlEnabled && !isMobile">
|
||||
<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>
|
||||
[blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx"
|
||||
(txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)"></app-block-overview-graph>
|
||||
</div>
|
||||
</div> <!-- row -->
|
||||
</div> <!-- box -->
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Component, OnDestroy, OnInit, AfterViewInit, ViewChildren, QueryList } from '@angular/core';
|
||||
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
|
||||
import { Subscription, combineLatest } from 'rxjs';
|
||||
import { map, switchMap, startWith, catchError } from 'rxjs/operators';
|
||||
import { Subscription, combineLatest, of } from 'rxjs';
|
||||
import { map, switchMap, startWith, catchError, filter } from 'rxjs/operators';
|
||||
import { BlockAudit, TransactionStripped } from '../../interfaces/node-api.interface';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { detectWebGL } from '../../shared/graphs.utils';
|
||||
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
||||
@@ -37,6 +38,7 @@ export class BlockAuditComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
isLoading = true;
|
||||
webGlEnabled = true;
|
||||
isMobile = window.innerWidth <= 767.98;
|
||||
hoverTx: string;
|
||||
|
||||
childChangeSubscription: Subscription;
|
||||
|
||||
@@ -51,7 +53,8 @@ export class BlockAuditComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
private route: ActivatedRoute,
|
||||
public stateService: StateService,
|
||||
private router: Router,
|
||||
private apiService: ApiService
|
||||
private apiService: ApiService,
|
||||
private electrsApiService: ElectrsApiService,
|
||||
) {
|
||||
this.webGlEnabled = detectWebGL();
|
||||
}
|
||||
@@ -76,69 +79,95 @@ export class BlockAuditComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
|
||||
this.auditSubscription = this.route.paramMap.pipe(
|
||||
switchMap((params: ParamMap) => {
|
||||
this.blockHash = params.get('id') || null;
|
||||
if (!this.blockHash) {
|
||||
const blockHash = params.get('id') || null;
|
||||
if (!blockHash) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let isBlockHeight = false;
|
||||
if (/^[0-9]+$/.test(blockHash)) {
|
||||
isBlockHeight = true;
|
||||
} else {
|
||||
this.blockHash = blockHash;
|
||||
}
|
||||
|
||||
if (isBlockHeight) {
|
||||
return this.electrsApiService.getBlockHashFromHeight$(parseInt(blockHash, 10))
|
||||
.pipe(
|
||||
switchMap((hash: string) => {
|
||||
if (hash) {
|
||||
this.blockHash = hash;
|
||||
return this.apiService.getBlockAudit$(this.blockHash)
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
catchError((err) => {
|
||||
this.error = err;
|
||||
return of(null);
|
||||
}),
|
||||
);
|
||||
}
|
||||
return this.apiService.getBlockAudit$(this.blockHash)
|
||||
.pipe(
|
||||
map((response) => {
|
||||
const blockAudit = response.body;
|
||||
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 {
|
||||
tx.status = 'missing';
|
||||
isMissing[tx.txid] = true;
|
||||
this.numMissing++;
|
||||
}
|
||||
}
|
||||
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 {
|
||||
tx.status = 'selected';
|
||||
isSelected[tx.txid] = true;
|
||||
this.numUnexpected++;
|
||||
}
|
||||
}
|
||||
for (const tx of blockAudit.transactions) {
|
||||
inBlock[tx.txid] = true;
|
||||
}
|
||||
return blockAudit;
|
||||
})
|
||||
);
|
||||
}),
|
||||
filter((response) => response != null),
|
||||
map((response) => {
|
||||
const blockAudit = response.body;
|
||||
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 {
|
||||
tx.status = 'missing';
|
||||
isMissing[tx.txid] = true;
|
||||
this.numMissing++;
|
||||
}
|
||||
}
|
||||
for (const [index, tx] of blockAudit.transactions.entries()) {
|
||||
if (index === 0) {
|
||||
tx.status = null;
|
||||
} else if (isAdded[tx.txid]) {
|
||||
tx.status = 'added';
|
||||
} else if (inTemplate[tx.txid]) {
|
||||
tx.status = 'found';
|
||||
} else {
|
||||
tx.status = 'selected';
|
||||
isSelected[tx.txid] = true;
|
||||
this.numUnexpected++;
|
||||
}
|
||||
}
|
||||
for (const tx of blockAudit.transactions) {
|
||||
inBlock[tx.txid] = true;
|
||||
}
|
||||
return blockAudit;
|
||||
}),
|
||||
catchError((err) => {
|
||||
console.log(err);
|
||||
this.error = err;
|
||||
this.isLoading = false;
|
||||
return null;
|
||||
return of(null);
|
||||
}),
|
||||
).subscribe((blockAudit) => {
|
||||
this.blockAudit = blockAudit;
|
||||
@@ -189,4 +218,12 @@ export class BlockAuditComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
const url = new RelativeUrlPipe(this.stateService).transform(`/tx/${event.txid}`);
|
||||
this.router.navigate([url]);
|
||||
}
|
||||
|
||||
onTxHover(txid: string): void {
|
||||
if (txid && txid.length) {
|
||||
this.hoverTx = txid;
|
||||
} else {
|
||||
this.hoverTx = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,9 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
||||
@Input() orientation = 'left';
|
||||
@Input() flip = true;
|
||||
@Input() disableSpinner = false;
|
||||
@Input() mirrorTxid: string | void;
|
||||
@Output() txClickEvent = new EventEmitter<TransactionStripped>();
|
||||
@Output() txHoverEvent = new EventEmitter<string>();
|
||||
@Output() readyEvent = new EventEmitter();
|
||||
|
||||
@ViewChild('blockCanvas')
|
||||
@@ -37,6 +39,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
||||
scene: BlockScene;
|
||||
hoverTx: TxView | void;
|
||||
selectedTx: TxView | void;
|
||||
mirrorTx: TxView | void;
|
||||
tooltipPosition: Position;
|
||||
|
||||
readyNextFrame = false;
|
||||
@@ -63,6 +66,9 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
||||
this.scene.setOrientation(this.orientation, this.flip);
|
||||
}
|
||||
}
|
||||
if (changes.mirrorTxid) {
|
||||
this.setMirror(this.mirrorTxid);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
@@ -76,6 +82,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
||||
this.exit(direction);
|
||||
this.hoverTx = null;
|
||||
this.selectedTx = null;
|
||||
this.onTxHover(null);
|
||||
this.start();
|
||||
}
|
||||
|
||||
@@ -181,7 +188,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
||||
this.gl.viewport(0, 0, this.displayWidth, this.displayHeight);
|
||||
}
|
||||
if (this.scene) {
|
||||
this.scene.resize({ width: this.displayWidth, height: this.displayHeight });
|
||||
this.scene.resize({ width: this.displayWidth, height: this.displayHeight, animate: false });
|
||||
this.start();
|
||||
} else {
|
||||
this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: this.resolution,
|
||||
@@ -301,6 +308,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
||||
}
|
||||
this.hoverTx = null;
|
||||
this.selectedTx = null;
|
||||
this.onTxHover(null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -352,17 +360,20 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
||||
this.selectedTx = selected;
|
||||
} else {
|
||||
this.hoverTx = selected;
|
||||
this.onTxHover(this.hoverTx ? this.hoverTx.txid : null);
|
||||
}
|
||||
} else {
|
||||
if (clicked) {
|
||||
this.selectedTx = null;
|
||||
}
|
||||
this.hoverTx = null;
|
||||
this.onTxHover(null);
|
||||
}
|
||||
} else if (clicked) {
|
||||
if (selected === this.selectedTx) {
|
||||
this.hoverTx = this.selectedTx;
|
||||
this.selectedTx = null;
|
||||
this.onTxHover(this.hoverTx ? this.hoverTx.txid : null);
|
||||
} else {
|
||||
this.selectedTx = selected;
|
||||
}
|
||||
@@ -370,6 +381,18 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
||||
}
|
||||
}
|
||||
|
||||
setMirror(txid: string | void) {
|
||||
if (this.mirrorTx) {
|
||||
this.scene.setHover(this.mirrorTx, false);
|
||||
this.start();
|
||||
}
|
||||
if (txid && this.scene.txs[txid]) {
|
||||
this.mirrorTx = this.scene.txs[txid];
|
||||
this.scene.setHover(this.mirrorTx, true);
|
||||
this.start();
|
||||
}
|
||||
}
|
||||
|
||||
onTxClick(cssX: number, cssY: number) {
|
||||
const x = cssX * window.devicePixelRatio;
|
||||
const y = cssY * window.devicePixelRatio;
|
||||
@@ -378,6 +401,10 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
||||
this.txClickEvent.emit(selected);
|
||||
}
|
||||
}
|
||||
|
||||
onTxHover(hoverId: string) {
|
||||
this.txHoverEvent.emit(hoverId);
|
||||
}
|
||||
}
|
||||
|
||||
// WebGL shader attributes
|
||||
|
||||
@@ -29,7 +29,7 @@ export default class BlockScene {
|
||||
this.init({ width, height, resolution, blockLimit, orientation, flip, vertexArray });
|
||||
}
|
||||
|
||||
resize({ width = this.width, height = this.height }: { width?: number, height?: number}): void {
|
||||
resize({ width = this.width, height = this.height, animate = true }: { width?: number, height?: number, animate: boolean }): void {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.gridSize = this.width / this.gridWidth;
|
||||
@@ -38,7 +38,7 @@ export default class BlockScene {
|
||||
|
||||
this.dirty = true;
|
||||
if (this.initialised && this.scene) {
|
||||
this.updateAll(performance.now(), 50);
|
||||
this.updateAll(performance.now(), 50, 'left', animate);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,7 +212,7 @@ export default class BlockScene {
|
||||
this.vbytesPerUnit = blockLimit / Math.pow(resolution / 1.02, 2);
|
||||
this.gridWidth = resolution;
|
||||
this.gridHeight = resolution;
|
||||
this.resize({ width, height });
|
||||
this.resize({ width, height, animate: true });
|
||||
this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight });
|
||||
|
||||
this.txs = {};
|
||||
@@ -225,14 +225,14 @@ export default class BlockScene {
|
||||
this.animateUntil = Math.max(this.animateUntil, tx.update(update));
|
||||
}
|
||||
|
||||
private updateTx(tx: TxView, startTime: number, delay: number, direction: string = 'left'): void {
|
||||
private updateTx(tx: TxView, startTime: number, delay: number, direction: string = 'left', animate: boolean = true): void {
|
||||
if (tx.dirty || this.dirty) {
|
||||
this.saveGridToScreenPosition(tx);
|
||||
this.setTxOnScreen(tx, startTime, delay, direction);
|
||||
this.setTxOnScreen(tx, startTime, delay, direction, animate);
|
||||
}
|
||||
}
|
||||
|
||||
private setTxOnScreen(tx: TxView, startTime: number, delay: number = 50, direction: string = 'left'): void {
|
||||
private setTxOnScreen(tx: TxView, startTime: number, delay: number = 50, direction: string = 'left', animate: boolean = true): void {
|
||||
if (!tx.initialised) {
|
||||
const txColor = tx.getColor();
|
||||
this.applyTxUpdate(tx, {
|
||||
@@ -252,30 +252,42 @@ export default class BlockScene {
|
||||
position: tx.screenPosition,
|
||||
color: txColor
|
||||
},
|
||||
duration: 1000,
|
||||
duration: animate ? 1000 : 1,
|
||||
start: startTime,
|
||||
delay,
|
||||
delay: animate ? delay : 0,
|
||||
});
|
||||
} else {
|
||||
this.applyTxUpdate(tx, {
|
||||
display: {
|
||||
position: tx.screenPosition
|
||||
},
|
||||
duration: 1000,
|
||||
minDuration: 500,
|
||||
duration: animate ? 1000 : 0,
|
||||
minDuration: animate ? 500 : 0,
|
||||
start: startTime,
|
||||
delay,
|
||||
adjust: true
|
||||
delay: animate ? delay : 0,
|
||||
adjust: animate
|
||||
});
|
||||
if (!animate) {
|
||||
this.applyTxUpdate(tx, {
|
||||
display: {
|
||||
position: tx.screenPosition
|
||||
},
|
||||
duration: 0,
|
||||
minDuration: 0,
|
||||
start: startTime,
|
||||
delay: 0,
|
||||
adjust: false
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private updateAll(startTime: number, delay: number = 50, direction: string = 'left'): void {
|
||||
private updateAll(startTime: number, delay: number = 50, direction: string = 'left', animate: boolean = true): void {
|
||||
this.scene.count = 0;
|
||||
const ids = this.getTxList();
|
||||
startTime = startTime || performance.now();
|
||||
for (const id of ids) {
|
||||
this.updateTx(this.txs[id], startTime, delay, direction);
|
||||
this.updateTx(this.txs[id], startTime, delay, direction, animate);
|
||||
}
|
||||
this.dirty = false;
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@ 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),
|
||||
added: hexToColor('0099ff'),
|
||||
selected: darken(desaturate(hexToColor('0099ff'), 0.3), 0.7),
|
||||
}
|
||||
|
||||
// convert from this class's update format to TxSprite's update format
|
||||
|
||||
@@ -37,9 +37,9 @@
|
||||
<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="'missing'" i18n="transaction.audit.omitted">omitted</td>
|
||||
<td *ngSwitchCase="'added'" i18n="transaction.audit.added">added</td>
|
||||
<td *ngSwitchCase="'selected'" i18n="transaction.audit.included">included</td>
|
||||
<td *ngSwitchCase="'selected'" i18n="transaction.audit.extra">extra</td>
|
||||
</ng-container>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
@@ -114,7 +114,7 @@
|
||||
<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>
|
||||
<span *ngIf="block.extras?.matchRate === null" i18n="unknown">Unknown</span>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ActivatedRoute, ParamMap, Router } from '@angular/router';
|
||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
||||
import { switchMap, tap, throttleTime, catchError, map, shareReplay, startWith, pairwise } from 'rxjs/operators';
|
||||
import { Transaction, Vout } from '../../interfaces/electrs.interface';
|
||||
import { Observable, of, Subscription, asyncScheduler, EMPTY } from 'rxjs';
|
||||
import { Observable, of, Subscription, asyncScheduler, EMPTY, Subject } from 'rxjs';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { WebsocketService } from '../../services/websocket.service';
|
||||
@@ -60,6 +60,8 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
nextBlockTxListSubscription: Subscription = undefined;
|
||||
timeLtrSubscription: Subscription;
|
||||
timeLtr: boolean;
|
||||
fetchAuditScore$ = new Subject<string>();
|
||||
fetchAuditScoreSubscription: Subscription;
|
||||
|
||||
@ViewChild('blockGraph') blockGraph: BlockOverviewGraphComponent;
|
||||
|
||||
@@ -105,12 +107,30 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
|
||||
if (block.id === this.blockHash) {
|
||||
this.block = block;
|
||||
if (this.block.id && this.block?.extras?.matchRate == null) {
|
||||
this.fetchAuditScore$.next(this.block.id);
|
||||
}
|
||||
if (block?.extras?.reward != undefined) {
|
||||
this.fees = block.extras.reward / 100000000 - this.blockSubsidy;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (this.indexingAvailable) {
|
||||
this.fetchAuditScoreSubscription = this.fetchAuditScore$
|
||||
.pipe(
|
||||
switchMap((hash) => this.apiService.getBlockAuditScore$(hash)),
|
||||
catchError(() => EMPTY),
|
||||
)
|
||||
.subscribe((score) => {
|
||||
if (score && score.hash === this.block.id) {
|
||||
this.block.extras.matchRate = score.matchRate || null;
|
||||
} else {
|
||||
this.block.extras.matchRate = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const block$ = this.route.paramMap.pipe(
|
||||
switchMap((params: ParamMap) => {
|
||||
const blockHash: string = params.get('id') || '';
|
||||
@@ -209,6 +229,9 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
this.fees = block.extras.reward / 100000000 - this.blockSubsidy;
|
||||
}
|
||||
this.stateService.markBlock$.next({ blockHeight: this.blockHeight });
|
||||
if (this.block.id && this.block?.extras?.matchRate == null) {
|
||||
this.fetchAuditScore$.next(this.block.id);
|
||||
}
|
||||
this.isLoadingTransactions = true;
|
||||
this.transactions = null;
|
||||
this.transactionsError = null;
|
||||
@@ -311,6 +334,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
this.networkChangedSubscription.unsubscribe();
|
||||
this.queryParamsSubscription.unsubscribe();
|
||||
this.timeLtrSubscription.unsubscribe();
|
||||
this.fetchAuditScoreSubscription?.unsubscribe();
|
||||
this.unsubscribeNextBlockSubscriptions();
|
||||
}
|
||||
|
||||
|
||||
@@ -46,22 +46,17 @@
|
||||
<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]">
|
||||
<a class="clear-link" [routerLink]="auditScores[block.id] != null ? ['/block-audit/' | relativeUrl, block.id] : null">
|
||||
<div class="progress progress-health">
|
||||
<div class="progress-bar progress-bar-health" role="progressbar"
|
||||
[ngStyle]="{'width': (100 - (block.extras?.matchRate || 0)) + '%' }"></div>
|
||||
[ngStyle]="{'width': (100 - (auditScores[block.id] || 0)) + '%' }"></div>
|
||||
<div class="progress-text">
|
||||
<span>{{ block.extras.matchRate }}%</span>
|
||||
<span *ngIf="auditScores[block.id] != null;">{{ auditScores[block.id] }}%</span>
|
||||
<span *ngIf="auditScores[block.id] === undefined" class="skeleton-loader"></span>
|
||||
<span *ngIf="auditScores[block.id] === null">~</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>
|
||||
|
||||
@@ -196,6 +196,10 @@ tr, td, th {
|
||||
@media (max-width: 950px) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.progress-text .skeleton-loader {
|
||||
top: -8.5px;
|
||||
}
|
||||
}
|
||||
.health.widget {
|
||||
width: 25%;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Component, OnInit, ChangeDetectionStrategy, Input } from '@angular/core';
|
||||
import { BehaviorSubject, combineLatest, concat, Observable, timer } from 'rxjs';
|
||||
import { delayWhen, map, retryWhen, scan, skip, switchMap, tap } from 'rxjs/operators';
|
||||
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, Input } from '@angular/core';
|
||||
import { BehaviorSubject, combineLatest, concat, Observable, timer, EMPTY, Subscription, of } from 'rxjs';
|
||||
import { catchError, delayWhen, map, retryWhen, scan, skip, switchMap, tap } from 'rxjs/operators';
|
||||
import { BlockExtended } from '../../interfaces/node-api.interface';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { StateService } from '../../services/state.service';
|
||||
@@ -12,10 +12,14 @@ import { WebsocketService } from '../../services/websocket.service';
|
||||
styleUrls: ['./blocks-list.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class BlocksList implements OnInit {
|
||||
export class BlocksList implements OnInit, OnDestroy {
|
||||
@Input() widget: boolean = false;
|
||||
|
||||
blocks$: Observable<BlockExtended[]> = undefined;
|
||||
auditScores: { [hash: string]: number | void } = {};
|
||||
|
||||
auditScoreSubscription: Subscription;
|
||||
latestScoreSubscription: Subscription;
|
||||
|
||||
indexingAvailable = false;
|
||||
isLoading = true;
|
||||
@@ -105,6 +109,53 @@ export class BlocksList implements OnInit {
|
||||
return acc;
|
||||
}, [])
|
||||
);
|
||||
|
||||
if (this.indexingAvailable) {
|
||||
this.auditScoreSubscription = this.fromHeightSubject.pipe(
|
||||
switchMap((fromBlockHeight) => {
|
||||
return this.apiService.getBlockAuditScores$(this.page === 1 ? undefined : fromBlockHeight)
|
||||
.pipe(
|
||||
catchError(() => {
|
||||
return EMPTY;
|
||||
})
|
||||
);
|
||||
})
|
||||
).subscribe((scores) => {
|
||||
Object.values(scores).forEach(score => {
|
||||
this.auditScores[score.hash] = score?.matchRate != null ? score.matchRate : null;
|
||||
});
|
||||
});
|
||||
|
||||
this.latestScoreSubscription = this.stateService.blocks$.pipe(
|
||||
switchMap((block) => {
|
||||
if (block[0]?.extras?.matchRate != null) {
|
||||
return of({
|
||||
hash: block[0].id,
|
||||
matchRate: block[0]?.extras?.matchRate,
|
||||
});
|
||||
}
|
||||
else if (block[0]?.id && this.auditScores[block[0].id] === undefined) {
|
||||
return this.apiService.getBlockAuditScore$(block[0].id)
|
||||
.pipe(
|
||||
catchError(() => {
|
||||
return EMPTY;
|
||||
})
|
||||
);
|
||||
} else {
|
||||
return EMPTY;
|
||||
}
|
||||
}),
|
||||
).subscribe((score) => {
|
||||
if (score && score.hash) {
|
||||
this.auditScores[score.hash] = score?.matchRate != null ? score.matchRate : null;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.auditScoreSubscription?.unsubscribe();
|
||||
this.latestScoreSubscription?.unsubscribe();
|
||||
}
|
||||
|
||||
pageChange(page: number) {
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
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';
|
||||
@@ -23,6 +23,16 @@ export class SearchFormComponent implements OnInit {
|
||||
isTypeaheading$ = new BehaviorSubject<boolean>(false);
|
||||
typeAhead$: Observable<any>;
|
||||
searchForm: FormGroup;
|
||||
dropdownHidden = false;
|
||||
|
||||
@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}$/;
|
||||
@@ -45,6 +55,7 @@ export class SearchFormComponent implements OnInit {
|
||||
private electrsApiService: ElectrsApiService,
|
||||
private apiService: ApiService,
|
||||
private relativeUrlPipe: RelativeUrlPipe,
|
||||
private elementRef: ElementRef,
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
|
||||
@@ -126,9 +126,13 @@ export class LiquidUnblinding {
|
||||
}
|
||||
|
||||
async checkUnblindedTx(tx: Transaction) {
|
||||
const windowLocationHash = window.location.hash.substring('#blinded='.length);
|
||||
if (windowLocationHash.length > 0) {
|
||||
const blinders = this.parseBlinders(windowLocationHash);
|
||||
if (!window.location.hash?.length) {
|
||||
return tx;
|
||||
}
|
||||
const fragmentParams = new URLSearchParams(window.location.hash.slice(1) || '');
|
||||
const blinderStr = fragmentParams.get('blinded');
|
||||
if (blinderStr && blinderStr.length) {
|
||||
const blinders = this.parseBlinders(blinderStr);
|
||||
if (blinders) {
|
||||
this.commitments = await this.makeCommitmentMap(blinders);
|
||||
return this.tryUnblindTx(tx);
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
|
||||
<div class="row graph-wrapper">
|
||||
<tx-bowtie-graph [tx]="tx" [width]="1112" [height]="346" [network]="network"></tx-bowtie-graph>
|
||||
<tx-bowtie-graph [tx]="tx" [width]="1132" [height]="346" [network]="network"></tx-bowtie-graph>
|
||||
<div class="above-bow">
|
||||
<p class="field pair">
|
||||
<span [innerHTML]="'‎' + (tx.size | bytes: 2)"></span>
|
||||
@@ -41,24 +41,20 @@
|
||||
</div>
|
||||
<div class="overlaid">
|
||||
<ng-container [ngSwitch]="extraData">
|
||||
<table class="opreturns" *ngSwitchCase="'coinbase'">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="label">Coinbase</td>
|
||||
<td class="message">{{ tx.vin[0].scriptsig | hex2ascii }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table class="opreturns" *ngSwitchCase="'opreturn'">
|
||||
<tbody>
|
||||
<div class="opreturns" *ngSwitchCase="'coinbase'">
|
||||
<div class="opreturn-row">
|
||||
<span class="label">Coinbase</span>
|
||||
<span class="message">{{ tx.vin[0].scriptsig | hex2ascii }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="opreturns" *ngSwitchCase="'opreturn'">
|
||||
<ng-container *ngFor="let vout of opReturns.slice(0,3)">
|
||||
<tr>
|
||||
<td class="label">OP_RETURN</td>
|
||||
<td *ngIf="vout.scriptpubkey_asm !== 'OP_RETURN'" class="message">{{ vout.scriptpubkey_asm | hex2ascii }}</td>
|
||||
</tr>
|
||||
<div class="opreturn-row">
|
||||
<span class="label">OP_RETURN</span>
|
||||
<span *ngIf="vout.scriptpubkey_asm !== 'OP_RETURN'" class="message">{{ vout.scriptpubkey_asm | hex2ascii }}</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -29,6 +29,8 @@
|
||||
.features {
|
||||
font-size: 24px;
|
||||
margin-left: 1em;
|
||||
margin-top: 0.5em;
|
||||
margin-right: -4px;
|
||||
}
|
||||
|
||||
.top-data {
|
||||
@@ -60,6 +62,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
.top-data .field {
|
||||
&:first-child {
|
||||
padding-left: 0;
|
||||
}
|
||||
&:last-child {
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.tx-link {
|
||||
display: inline;
|
||||
font-size: 28px;
|
||||
@@ -69,7 +80,7 @@
|
||||
.graph-wrapper {
|
||||
position: relative;
|
||||
background: #181b2d;
|
||||
padding: 10px;
|
||||
padding: 10px 0;
|
||||
padding-bottom: 0;
|
||||
|
||||
.above-bow {
|
||||
@@ -92,26 +103,37 @@
|
||||
max-width: 90%;
|
||||
margin: auto;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
|
||||
.opreturns {
|
||||
display: inline-block;
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
margin: auto;
|
||||
table-layout: auto;
|
||||
background: #2d3348af;
|
||||
border-top-left-radius: 5px;
|
||||
border-top-right-radius: 5px;
|
||||
|
||||
td {
|
||||
padding: 10px 10px;
|
||||
.opreturn-row {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
&.message {
|
||||
overflow: hidden;
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
}
|
||||
.label {
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
.message {
|
||||
flex-shrink: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,8 +117,9 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy {
|
||||
}),
|
||||
switchMap(() => {
|
||||
let transactionObservable$: Observable<Transaction>;
|
||||
if (history.state.data && history.state.data.fee !== -1) {
|
||||
transactionObservable$ = of(history.state.data);
|
||||
const cached = this.stateService.getTxFromCache(this.txId);
|
||||
if (cached && cached.fee !== -1) {
|
||||
transactionObservable$ = of(cached);
|
||||
} else {
|
||||
transactionObservable$ = this.electrsApiService
|
||||
.getTransaction$(this.txId)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="title-block">
|
||||
<div *ngIf="rbfTransaction" class="alert alert-mempool" role="alert">
|
||||
<span i18n="transaction.rbf.replacement|RBF replacement">This transaction has been replaced by:</span>
|
||||
<a class="alert-link" [routerLink]="['/tx/' | relativeUrl, rbfTransaction.txid]" [state]="{ data: rbfTransaction.size ? rbfTransaction : null }">
|
||||
<a class="alert-link" [routerLink]="['/tx/' | relativeUrl, rbfTransaction.txid]">
|
||||
<span class="d-inline d-lg-none">{{ rbfTransaction.txid | shortenString : 24 }}</span>
|
||||
<span class="d-none d-lg-inline">{{ rbfTransaction.txid }}</span>
|
||||
</a>
|
||||
@@ -209,6 +209,7 @@
|
||||
[maxStrands]="graphExpanded ? maxInOut : 24"
|
||||
[network]="network"
|
||||
[tooltip]="true"
|
||||
[connectors]="true"
|
||||
[inputIndex]="inputIndex" [outputIndex]="outputIndex"
|
||||
>
|
||||
</tx-bowtie-graph>
|
||||
|
||||
@@ -86,7 +86,7 @@
|
||||
position: relative;
|
||||
width: 100%;
|
||||
background: #181b2d;
|
||||
padding: 10px;
|
||||
padding: 10px 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -183,8 +183,9 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}),
|
||||
switchMap(() => {
|
||||
let transactionObservable$: Observable<Transaction>;
|
||||
if (history.state.data && history.state.data.fee !== -1) {
|
||||
transactionObservable$ = of(history.state.data);
|
||||
const cached = this.stateService.getTxFromCache(this.txId);
|
||||
if (cached && cached.fee !== -1) {
|
||||
transactionObservable$ = of(cached);
|
||||
} else {
|
||||
transactionObservable$ = this.electrsApiService
|
||||
.getTransaction$(this.txId)
|
||||
@@ -279,6 +280,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.waitingForTransaction = false;
|
||||
}
|
||||
this.rbfTransaction = rbfTransaction;
|
||||
this.stateService.setTxCache([this.rbfTransaction]);
|
||||
});
|
||||
|
||||
this.queryParamsSubscription = this.route.queryParams.subscribe((params) => {
|
||||
@@ -402,7 +404,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
@HostListener('window:resize', ['$event'])
|
||||
setGraphSize(): void {
|
||||
if (this.graphContainer) {
|
||||
this.graphWidth = this.graphContainer.nativeElement.clientWidth - 24;
|
||||
this.graphWidth = this.graphContainer.nativeElement.clientWidth;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<ng-container *ngFor="let tx of transactions; let i = index; trackBy: trackByFn">
|
||||
<div *ngIf="!transactionPage" class="header-bg box tx-page-container">
|
||||
<a class="float-left" [routerLink]="['/tx/' | relativeUrl, tx.txid]" [state]="{ data: tx }">
|
||||
<a class="float-left" [routerLink]="['/tx/' | relativeUrl, tx.txid]">
|
||||
<span style="float: left;" class="d-block d-md-none">{{ tx.txid | shortenString : 16 }}</span>
|
||||
<span style="float: left;" class="d-none d-md-block">{{ tx.txid }}</span>
|
||||
</a>
|
||||
|
||||
@@ -119,7 +119,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
||||
}
|
||||
|
||||
this.transactionsLength = this.transactions.length;
|
||||
|
||||
this.stateService.setTxCache(this.transactions);
|
||||
|
||||
this.transactions.forEach((tx) => {
|
||||
tx['@voutLimit'] = true;
|
||||
|
||||
@@ -22,13 +22,13 @@
|
||||
|
||||
<ng-template #pegin>
|
||||
<ng-container *ngIf="line.pegin; else pegout">
|
||||
<p>Peg In</p>
|
||||
<p *ngIf="!isConnector">Peg In</p>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #pegout>
|
||||
<ng-container *ngIf="line.pegout; else normal">
|
||||
<p>Peg Out</p>
|
||||
<p *ngIf="!isConnector">Peg Out</p>
|
||||
<p *ngIf="line.value != null"><app-amount [satoshis]="line.value"></app-amount></p>
|
||||
<p class="address">
|
||||
<span class="first">{{ line.pegout.slice(0, -4) }}</span>
|
||||
@@ -38,7 +38,7 @@
|
||||
</ng-template>
|
||||
|
||||
<ng-template #normal>
|
||||
<p>
|
||||
<p *ngIf="!isConnector">
|
||||
<ng-container [ngSwitch]="line.type">
|
||||
<span *ngSwitchCase="'input'" i18n="transaction.input">Input</span>
|
||||
<span *ngSwitchCase="'output'" i18n="transaction.output">Output</span>
|
||||
@@ -46,6 +46,17 @@
|
||||
</ng-container>
|
||||
<span *ngIf="line.type !== 'fee'"> #{{ line.index + 1 }}</span>
|
||||
</p>
|
||||
<ng-container *ngIf="isConnector && line.txid">
|
||||
<p>
|
||||
<span i18n="transaction">Transaction</span>
|
||||
<span class="first">{{ line.txid.slice(0, 8) }}</span>...
|
||||
<span class="last-four">{{ line.txid.slice(-4) }}</span>
|
||||
</p>
|
||||
<ng-container [ngSwitch]="line.type">
|
||||
<p *ngSwitchCase="'input'"><span i18n="transaction.output">Output</span> #{{ line.vout + 1 }}</p>
|
||||
<p *ngSwitchCase="'output'"><span i18n="transaction.input">Input</span> #{{ line.vin + 1 }}</p>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<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>
|
||||
<p *ngIf="line.type !== 'fee' && line.address" class="address">
|
||||
|
||||
@@ -5,6 +5,9 @@ interface Xput {
|
||||
type: 'input' | 'output' | 'fee';
|
||||
value?: number;
|
||||
index?: number;
|
||||
txid?: string;
|
||||
vin?: number;
|
||||
vout?: number;
|
||||
address?: string;
|
||||
rest?: number;
|
||||
coinbase?: boolean;
|
||||
@@ -21,6 +24,7 @@ interface Xput {
|
||||
export class TxBowtieGraphTooltipComponent implements OnChanges {
|
||||
@Input() line: Xput | void;
|
||||
@Input() cursorPosition: { x: number, y: number };
|
||||
@Input() isConnector: boolean = false;
|
||||
|
||||
tooltipPosition = { x: 0, y: 0 };
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div class="bowtie-graph">
|
||||
<svg *ngIf="inputs && outputs" class="bowtie" [attr.height]="(height + 10) + 'px'" [attr.width]="width + 'px'">
|
||||
<svg *ngIf="inputs && outputs" class="bowtie" [class.rtl]="dir === 'rtl'" [attr.height]="(height + 10) + 'px'" [attr.width]="width + 'px'">
|
||||
<defs>
|
||||
<marker id="input-arrow" viewBox="-5 -5 10 10"
|
||||
refX="0" refY="0"
|
||||
@@ -21,6 +21,15 @@
|
||||
markerWidth="1.5" markerHeight="1"
|
||||
orient="auto">
|
||||
</marker>
|
||||
<radialGradient id="gradient0" x1="0%" y1="0%" x2="100%" y2="100%" gradientUnits="userSpaceOnUse">
|
||||
<stop [attr.stop-color]="gradient[0]" />
|
||||
</radialGradient>
|
||||
<radialGradient id="gradient1" x1="0%" y1="0%" x2="100%" y2="100%" gradientUnits="userSpaceOnUse">
|
||||
<stop [attr.stop-color]="gradient[1]" />
|
||||
</radialGradient>
|
||||
<radialGradient id="gradient2" x1="0%" y1="0%" x2="100%" y2="100%" gradientUnits="userSpaceOnUse">
|
||||
<stop [attr.stop-color]="gradient[2]" />
|
||||
</radialGradient>
|
||||
<linearGradient id="input-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" [attr.stop-color]="gradient[0]" />
|
||||
<stop offset="100%" [attr.stop-color]="gradient[1]" />
|
||||
@@ -29,6 +38,14 @@
|
||||
<stop offset="0%" [attr.stop-color]="gradient[1]" />
|
||||
<stop offset="100%" [attr.stop-color]="gradient[0]" />
|
||||
</linearGradient>
|
||||
<linearGradient id="input-connector-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" [attr.stop-color]="gradient[2]" />
|
||||
<stop offset="80%" [attr.stop-color]="gradient[0]" />
|
||||
</linearGradient>
|
||||
<linearGradient id="output-connector-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="20%" [attr.stop-color]="gradient[0]" />
|
||||
<stop offset="100%" [attr.stop-color]="gradient[2]" />
|
||||
</linearGradient>
|
||||
<linearGradient id="input-hover-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]" />
|
||||
@@ -41,6 +58,14 @@
|
||||
<stop offset="98%" [attr.stop-color]="gradient[0]" />
|
||||
<stop offset="100%" [attr.stop-color]="gradient[0]" />
|
||||
</linearGradient>
|
||||
<linearGradient id="input-hover-connector-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" stop-color="white" />
|
||||
<stop offset="80%" [attr.stop-color]="gradient[0]" />
|
||||
</linearGradient>
|
||||
<linearGradient id="output-hover-connector-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="20%" [attr.stop-color]="gradient[0]" />
|
||||
<stop offset="100%" stop-color="white" />
|
||||
</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]" />
|
||||
@@ -65,6 +90,22 @@
|
||||
</defs>
|
||||
<path [attr.d]="middle.path" class="line middle" [style]="middle.style"/>
|
||||
<ng-container *ngFor="let input of inputs; let i = index">
|
||||
<path *ngIf="connectors && !inputData[i].coinbase && !inputData[i].pegin"
|
||||
[attr.d]="input.connectorPath"
|
||||
class="input connector {{input.class}}"
|
||||
[class.highlight]="inputData[i].index === inputIndex"
|
||||
(pointerover)="onHover($event, 'input-connector', i);"
|
||||
(pointerout)="onBlur($event, 'input-connector', i);"
|
||||
(click)="onClick($event, 'input-connector', inputData[i].index);"
|
||||
/>
|
||||
<path
|
||||
[attr.d]="input.markerPath"
|
||||
class="input marker-target {{input.class}}"
|
||||
[class.highlight]="inputData[i].index === inputIndex"
|
||||
(pointerover)="onHover($event, 'input', i);"
|
||||
(pointerout)="onBlur($event, 'input', i);"
|
||||
(click)="onClick($event, 'input', inputData[i].index);"
|
||||
/>
|
||||
<path
|
||||
[attr.d]="input.path"
|
||||
class="line {{input.class}}"
|
||||
@@ -77,7 +118,23 @@
|
||||
/>
|
||||
</ng-container>
|
||||
<ng-container *ngFor="let output of outputs; let i = index">
|
||||
<path
|
||||
<path *ngIf="connectors && outspends[outputData[i].index]?.spent"
|
||||
[attr.d]="output.connectorPath"
|
||||
class="output connector {{output.class}}"
|
||||
[class.highlight]="outputData[i].index === outputIndex"
|
||||
(pointerover)="onHover($event, 'output-connector', i);"
|
||||
(pointerout)="onBlur($event, 'output-connector', i);"
|
||||
(click)="onClick($event, 'output-connector', outputData[i].index);"
|
||||
/>
|
||||
<path *ngIf="!output.zeroValue"
|
||||
[attr.d]="output.markerPath"
|
||||
class="output marker-target {{output.class}}"
|
||||
[class.highlight]="outputData[i].index === outputIndex"
|
||||
(pointerover)="onHover($event, 'output', i);"
|
||||
(pointerout)="onBlur($event, 'output', i);"
|
||||
(click)="onClick($event, 'output', outputData[i].index);"
|
||||
/>
|
||||
<path *ngIf="!output.zeroValue"
|
||||
[attr.d]="output.path"
|
||||
class="line {{output.class}}"
|
||||
[class.highlight]="outputIndex != null && outputData[i].index === outputIndex"
|
||||
@@ -87,6 +144,16 @@
|
||||
(pointerout)="onBlur($event, 'output', i);"
|
||||
(click)="onClick($event, 'output', outputData[i].index);"
|
||||
/>
|
||||
<path *ngIf="output.zeroValue"
|
||||
[attr.d]="output.path"
|
||||
class="line {{output.class}} zerovalue"
|
||||
[class.highlight]="outputIndex != null && outputData[i].index === outputIndex"
|
||||
[class.zerovalue]="output.zeroValue"
|
||||
[style]="output.style"
|
||||
(pointerover)="onHover($event, 'output', i);"
|
||||
(pointerout)="onBlur($event, 'output', i);"
|
||||
(click)="onClick($event, 'output', outputData[i].index);"
|
||||
/>
|
||||
</ng-container>
|
||||
</svg>
|
||||
|
||||
@@ -94,5 +161,6 @@
|
||||
*ngIf=[tooltip]
|
||||
[line]="hoverLine"
|
||||
[cursorPosition]="tooltipPosition"
|
||||
[isConnector]="hoverConnector"
|
||||
></app-tx-bowtie-graph-tooltip>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
.bowtie {
|
||||
&.rtl {
|
||||
transform: scale(-1, 1);
|
||||
}
|
||||
|
||||
.line {
|
||||
fill: none;
|
||||
|
||||
@@ -11,6 +15,10 @@
|
||||
&.fee {
|
||||
stroke: url(#fee-gradient);
|
||||
}
|
||||
&.zerovalue {
|
||||
stroke: url(#gradient0);
|
||||
stroke-linecap: round;
|
||||
}
|
||||
|
||||
&.highlight {
|
||||
z-index: 8;
|
||||
@@ -21,20 +29,53 @@
|
||||
&.output {
|
||||
stroke: url(#output-highlight-gradient);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
z-index: 10;
|
||||
cursor: pointer;
|
||||
&.input {
|
||||
stroke: url(#input-hover-gradient);
|
||||
}
|
||||
&.output {
|
||||
stroke: url(#output-hover-gradient);
|
||||
}
|
||||
&.fee {
|
||||
stroke: url(#fee-hover-gradient);
|
||||
&.zerovalue {
|
||||
stroke: #1bd8f4;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.line:hover, .marker-target:hover + .line {
|
||||
z-index: 10;
|
||||
cursor: pointer;
|
||||
&.input {
|
||||
stroke: url(#input-hover-gradient);
|
||||
}
|
||||
&.output {
|
||||
stroke: url(#output-hover-gradient);
|
||||
}
|
||||
&.fee {
|
||||
stroke: url(#fee-hover-gradient);
|
||||
}
|
||||
&.zerovalue {
|
||||
stroke: white;
|
||||
}
|
||||
}
|
||||
|
||||
.connector {
|
||||
stroke: none;
|
||||
opacity: 0.75;
|
||||
cursor: pointer;
|
||||
&.input {
|
||||
fill: url(#input-connector-gradient);
|
||||
}
|
||||
&.output {
|
||||
fill: url(#output-connector-gradient);
|
||||
}
|
||||
}
|
||||
|
||||
.connector:hover {
|
||||
&.input {
|
||||
fill: url(#input-hover-connector-gradient);
|
||||
}
|
||||
&.output {
|
||||
fill: url(#output-hover-connector-gradient);
|
||||
}
|
||||
}
|
||||
|
||||
.marker-target {
|
||||
stroke: none;
|
||||
fill: transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, OnInit, Input, OnChanges, HostListener } from '@angular/core';
|
||||
import { Component, OnInit, Input, OnChanges, HostListener, Inject, LOCALE_ID } from '@angular/core';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { Outspend, Transaction } from '../../interfaces/electrs.interface';
|
||||
import { Router } from '@angular/router';
|
||||
@@ -11,12 +11,18 @@ interface SvgLine {
|
||||
path: string;
|
||||
style: string;
|
||||
class?: string;
|
||||
connectorPath?: string;
|
||||
markerPath?: string;
|
||||
zeroValue?: boolean;
|
||||
}
|
||||
|
||||
interface Xput {
|
||||
type: 'input' | 'output' | 'fee';
|
||||
value?: number;
|
||||
index?: number;
|
||||
txid?: string;
|
||||
vin?: number;
|
||||
vout?: number;
|
||||
address?: string;
|
||||
rest?: number;
|
||||
coinbase?: boolean;
|
||||
@@ -40,35 +46,43 @@ 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() connectors = false;
|
||||
@Input() inputIndex: number;
|
||||
@Input() outputIndex: number;
|
||||
|
||||
dir: 'rtl' | 'ltr' = 'ltr';
|
||||
|
||||
inputData: Xput[];
|
||||
outputData: Xput[];
|
||||
inputs: SvgLine[];
|
||||
outputs: SvgLine[];
|
||||
middle: SvgLine;
|
||||
midWidth: number;
|
||||
txWidth: number;
|
||||
connectorWidth: number;
|
||||
combinedWeight: number;
|
||||
isLiquid: boolean = false;
|
||||
hoverLine: Xput | void = null;
|
||||
hoverConnector: boolean = false;
|
||||
tooltipPosition = { x: 0, y: 0 };
|
||||
outspends: Outspend[] = [];
|
||||
zeroValueWidth = 60;
|
||||
zeroValueThickness = 20;
|
||||
|
||||
outspendsSubscription: Subscription;
|
||||
refreshOutspends$: ReplaySubject<string> = new ReplaySubject();
|
||||
|
||||
gradientColors = {
|
||||
'': ['#9339f4', '#105fb0'],
|
||||
bisq: ['#9339f4', '#105fb0'],
|
||||
'': ['#9339f4', '#105fb0', '#9339f400'],
|
||||
bisq: ['#9339f4', '#105fb0', '#9339f400'],
|
||||
// liquid: ['#116761', '#183550'],
|
||||
liquid: ['#09a197', '#0f62af'],
|
||||
liquid: ['#09a197', '#0f62af', '#09a19700'],
|
||||
// 'liquidtestnet': ['#494a4a', '#272e46'],
|
||||
'liquidtestnet': ['#d2d2d2', '#979797'],
|
||||
'liquidtestnet': ['#d2d2d2', '#979797', '#d2d2d200'],
|
||||
// testnet: ['#1d486f', '#183550'],
|
||||
testnet: ['#4edf77', '#10a0af'],
|
||||
testnet: ['#4edf77', '#10a0af', '#4edf7700'],
|
||||
// signet: ['#6f1d5d', '#471850'],
|
||||
signet: ['#d24fc8', '#a84fd2'],
|
||||
signet: ['#d24fc8', '#a84fd2', '#d24fc800'],
|
||||
};
|
||||
|
||||
gradient: string[] = ['#105fb0', '#105fb0'];
|
||||
@@ -78,7 +92,12 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
|
||||
private relativeUrlPipe: RelativeUrlPipe,
|
||||
private stateService: StateService,
|
||||
private apiService: ApiService,
|
||||
) { }
|
||||
@Inject(LOCALE_ID) private locale: string,
|
||||
) {
|
||||
if (this.locale.startsWith('ar') || this.locale.startsWith('fa') || this.locale.startsWith('he')) {
|
||||
this.dir = 'rtl';
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.initGraph();
|
||||
@@ -118,7 +137,10 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
|
||||
this.isLiquid = (this.network === 'liquid' || this.network === 'liquidtestnet');
|
||||
this.gradient = this.gradientColors[this.network];
|
||||
this.midWidth = Math.min(10, Math.ceil(this.width / 100));
|
||||
this.combinedWeight = Math.min(this.maxCombinedWeight, Math.floor((this.width - (2 * this.midWidth)) / 6));
|
||||
this.txWidth = this.connectors ? Math.max(this.width - 200, this.width * 0.8) : this.width - 20;
|
||||
this.combinedWeight = Math.min(this.maxCombinedWeight, Math.floor((this.txWidth - (2 * this.midWidth)) / 6));
|
||||
this.connectorWidth = (this.width - this.txWidth) / 2;
|
||||
this.zeroValueWidth = Math.max(20, Math.min((this.txWidth / 2) - this.midWidth - 110, 60));
|
||||
|
||||
const totalValue = this.calcTotalValue(this.tx);
|
||||
let voutWithFee = this.tx.vout.map((v, i) => {
|
||||
@@ -141,6 +163,8 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
|
||||
return {
|
||||
type: 'input',
|
||||
value: v?.prevout?.value,
|
||||
txid: v.txid,
|
||||
vout: v.vout,
|
||||
address: v?.prevout?.scriptpubkey_address || v?.prevout?.scriptpubkey_type?.toUpperCase(),
|
||||
index: i,
|
||||
coinbase: v?.is_coinbase,
|
||||
@@ -223,10 +247,10 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
|
||||
}
|
||||
|
||||
linesFromWeights(side: 'in' | 'out', xputs: Xput[], weights: number[], maxVisibleStrands: number): SvgLine[] {
|
||||
const lineParams = weights.map((w) => {
|
||||
const lineParams = weights.map((w, i) => {
|
||||
return {
|
||||
weight: w,
|
||||
thickness: Math.max(this.minWeight - 1, w) + 1,
|
||||
thickness: xputs[i].value === 0 ? this.zeroValueThickness : Math.max(this.minWeight - 1, w) + 1,
|
||||
offset: 0,
|
||||
innerY: 0,
|
||||
outerY: 0,
|
||||
@@ -243,7 +267,7 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
|
||||
let lastOuter = 0;
|
||||
let lastInner = innerTop;
|
||||
// gap between strands
|
||||
const spacing = (this.height - visibleWeight) / gaps;
|
||||
const spacing = Math.max(4, (this.height - visibleWeight) / gaps);
|
||||
|
||||
// curve adjustments to prevent overlaps
|
||||
let offset = 0;
|
||||
@@ -252,6 +276,12 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
|
||||
let lastWeight = 0;
|
||||
let pad = 0;
|
||||
lineParams.forEach((line, i) => {
|
||||
if (xputs[i].value === 0) {
|
||||
line.outerY = lastOuter + (this.zeroValueThickness / 2);
|
||||
lastOuter += this.zeroValueThickness + spacing;
|
||||
return;
|
||||
}
|
||||
|
||||
// set the vertical position of the (center of the) outer side of the line
|
||||
line.outerY = lastOuter + (line.thickness / 2);
|
||||
line.innerY = Math.min(innerBottom + (line.thickness / 2), Math.max(innerTop + (line.thickness / 2), lastInner + (line.weight / 2)));
|
||||
@@ -268,7 +298,7 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
|
||||
// required to prevent this line overlapping its neighbor
|
||||
|
||||
if (this.tooltip || !xputs[i].rest) {
|
||||
const w = (this.width - Math.max(lastWeight, line.weight)) / 2; // approximate horizontal width of the curved section of the line
|
||||
const w = (this.width - Math.max(lastWeight, line.weight) - (2 * this.connectorWidth)) / 2; // approximate horizontal width of the curved section of the line
|
||||
const y1 = line.outerY;
|
||||
const y2 = line.innerY;
|
||||
const t = (lastWeight + line.weight) / 2; // distance between center of this line and center of previous line
|
||||
@@ -305,17 +335,28 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
|
||||
maxOffset -= minOffset;
|
||||
|
||||
return lineParams.map((line, i) => {
|
||||
return {
|
||||
path: this.makePath(side, line.outerY, line.innerY, line.thickness, line.offset, pad + maxOffset),
|
||||
style: this.makeStyle(line.thickness, xputs[i].type),
|
||||
class: xputs[i].type
|
||||
};
|
||||
if (xputs[i].value === 0) {
|
||||
return {
|
||||
path: this.makeZeroValuePath(side, line.outerY),
|
||||
style: this.makeStyle(this.zeroValueThickness, xputs[i].type),
|
||||
class: xputs[i].type,
|
||||
zeroValue: true,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
path: this.makePath(side, line.outerY, line.innerY, line.thickness, line.offset, pad + maxOffset),
|
||||
style: this.makeStyle(line.thickness, xputs[i].type),
|
||||
class: xputs[i].type,
|
||||
connectorPath: this.connectors ? this.makeConnectorPath(side, line.outerY, line.innerY, line.thickness): null,
|
||||
markerPath: this.makeMarkerPath(side, line.outerY, line.innerY, line.thickness),
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
makePath(side: 'in' | 'out', outer: number, inner: number, weight: number, offset: number, pad: number): string {
|
||||
const start = (weight * 0.5);
|
||||
const curveStart = Math.max(start + 1, pad - offset);
|
||||
const start = (weight * 0.5) + this.connectorWidth;
|
||||
const curveStart = Math.max(start + 5, pad - offset);
|
||||
const end = this.width / 2 - (this.midWidth * 0.9) + 1;
|
||||
const curveEnd = end - offset - 10;
|
||||
const midpoint = (curveStart + curveEnd) / 2;
|
||||
@@ -332,6 +373,50 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
|
||||
}
|
||||
}
|
||||
|
||||
makeZeroValuePath(side: 'in' | 'out', y: number): string {
|
||||
const offset = this.zeroValueThickness / 2;
|
||||
const start = (this.connectorWidth / 2) + 10;
|
||||
if (side === 'in') {
|
||||
return `M ${start + offset} ${y} L ${start + this.zeroValueWidth + offset} ${y}`;
|
||||
} else { // mirrored in y-axis for the right hand side
|
||||
return `M ${this.width - start - offset} ${y} L ${this.width - start - this.zeroValueWidth - offset} ${y}`;
|
||||
}
|
||||
}
|
||||
|
||||
makeConnectorPath(side: 'in' | 'out', y: number, inner, weight: number): string {
|
||||
const halfWidth = weight * 0.5;
|
||||
const offset = 10; //Math.max(2, halfWidth * 0.2);
|
||||
const lineEnd = this.connectorWidth;
|
||||
|
||||
// align with for svg horizontal gradient bug correction
|
||||
if (Math.round(y) === Math.round(inner)) {
|
||||
y -= 1;
|
||||
}
|
||||
|
||||
if (side === 'in') {
|
||||
return `M ${lineEnd - offset} ${y - halfWidth} L ${halfWidth + lineEnd - offset} ${y} L ${lineEnd - offset} ${y + halfWidth} L -${10} ${ y + halfWidth} L -${10} ${y - halfWidth}`;
|
||||
} else {
|
||||
return `M ${this.width - halfWidth - lineEnd + offset} ${y - halfWidth} L ${this.width - lineEnd + offset} ${y} L ${this.width - halfWidth - lineEnd + offset} ${y + halfWidth} L ${this.width + 10} ${ y + halfWidth} L ${this.width + 10} ${y - halfWidth}`;
|
||||
}
|
||||
}
|
||||
|
||||
makeMarkerPath(side: 'in' | 'out', y: number, inner, weight: number): string {
|
||||
const halfWidth = weight * 0.5;
|
||||
const offset = 10; //Math.max(2, halfWidth * 0.2);
|
||||
const lineEnd = this.connectorWidth;
|
||||
|
||||
// align with for svg horizontal gradient bug correction
|
||||
if (Math.round(y) === Math.round(inner)) {
|
||||
y -= 1;
|
||||
}
|
||||
|
||||
if (side === 'in') {
|
||||
return `M ${lineEnd - offset} ${y - halfWidth} L ${halfWidth + lineEnd - offset} ${y} L ${lineEnd - offset} ${y + halfWidth} L ${weight + lineEnd} ${ y + halfWidth} L ${weight + lineEnd} ${y - halfWidth}`;
|
||||
} else {
|
||||
return `M ${this.width - halfWidth - lineEnd + offset} ${y - halfWidth} L ${this.width - lineEnd + offset} ${y} L ${this.width - halfWidth - lineEnd + offset} ${y + halfWidth} L ${this.width - halfWidth - lineEnd} ${ y + halfWidth} L ${this.width - halfWidth - lineEnd} ${y - halfWidth}`;
|
||||
}
|
||||
}
|
||||
|
||||
makeStyle(minWeight, type): string {
|
||||
if (type === 'fee') {
|
||||
return `stroke-width: ${minWeight}`;
|
||||
@@ -342,30 +427,39 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
|
||||
|
||||
@HostListener('pointermove', ['$event'])
|
||||
onPointerMove(event) {
|
||||
this.tooltipPosition = { x: event.offsetX, y: event.offsetY };
|
||||
if (this.dir === 'rtl') {
|
||||
this.tooltipPosition = { x: this.width - event.offsetX, y: event.offsetY };
|
||||
} else {
|
||||
this.tooltipPosition = { x: event.offsetX, y: event.offsetY };
|
||||
}
|
||||
}
|
||||
|
||||
onHover(event, side, index): void {
|
||||
if (side === 'input') {
|
||||
if (side.startsWith('input')) {
|
||||
this.hoverLine = {
|
||||
...this.inputData[index],
|
||||
index
|
||||
};
|
||||
this.hoverConnector = (side === 'input-connector');
|
||||
|
||||
} else {
|
||||
this.hoverLine = {
|
||||
...this.outputData[index]
|
||||
...this.outputData[index],
|
||||
...this.outspends[this.outputData[index].index]
|
||||
};
|
||||
this.hoverConnector = (side === 'output-connector');
|
||||
}
|
||||
}
|
||||
|
||||
onBlur(event, side, index): void {
|
||||
this.hoverLine = null;
|
||||
this.hoverConnector = false;
|
||||
}
|
||||
|
||||
onClick(event, side, index): void {
|
||||
if (side === 'input') {
|
||||
if (side.startsWith('input')) {
|
||||
const input = this.tx.vin[index];
|
||||
if (input && !input.is_coinbase && !input.is_pegin && input.txid && input.vout != null) {
|
||||
if (side === 'input-connector' && 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({
|
||||
@@ -385,7 +479,7 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
|
||||
} else {
|
||||
const output = this.tx.vout[index];
|
||||
const outspend = this.outspends[index];
|
||||
if (output && outspend && outspend.spent && outspend.txid) {
|
||||
if (side === 'output-connector' && output && outspend && outspend.spent && outspend.txid) {
|
||||
this.router.navigate([this.relativeUrlPipe.transform('/tx'), outspend.txid], {
|
||||
queryParamsHandling: 'merge',
|
||||
fragment: (new URLSearchParams({
|
||||
|
||||
@@ -9,6 +9,11 @@
|
||||
|
||||
<div class="doc-content">
|
||||
|
||||
<div id="disclaimer">
|
||||
<table><tr><td><svg viewBox="0 0 304 304" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd" style="fill:#ffc107;fill-opacity:1"><path d="M135.3 34.474c-15.62 27.306-54.206 95.63-85.21 150.534L9.075 257.583C5.382 264.08 6.76 269.217 7.908 271.7c2.326 5.028 7.29 7.537 11.155 8.215l.78.133 264.698.006-.554-.02c4.152.255 9.664-1.24 12.677-6.194 1.926-3.18 3.31-8.589-1.073-16.278L213.637 114.37l-45.351-79.205c-5.681-9.932-12.272-12.022-16.8-12.022-4.42 0-10.818 1.964-16.181 11.331h-.006zm-69.072 159.94c30.997-54.885 69.563-123.184 85.16-150.446l.186-.297c.2.303.393.582.618.981l45.363 79.22s72.377 126.47 78.569 137.283l-247.618-.007 37.719-66.734" style="fill:#ffc107;fill-opacity:1"/><path d="M152.597 247.445c8.02 0 14.518-6.728 14.518-15.025 0-8.29-6.499-15.018-14.518-15.018-8.031 0-14.529 6.728-14.529 15.018 0 8.297 6.498 15.025 14.53 15.025m-.001-147.18c11.586 0 22.23 10.958 20.977 21.7l-9.922 75.564c-.966 6.601-4.95 11.433-11.055 11.433s-10.102-4.832-11.056-11.433l-9.927-75.564c-1.26-10.742 9.39-21.7 20.983-21.7" style="fill:#ffc107;fill-opacity:1"/></g></svg></td><td><p><b>mempool.space merely provides data about the Bitcoin network.</b> It cannot help you with retrieving funds, confirming your transaction quicker, etc.</p><p>For any such requests, you need to get in touch with the entity that helped make the transaction (wallet software, exchange company, etc).</p></td></tr></table>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="doc-item-container" *ngFor="let item of faq">
|
||||
<h3 *ngIf="item.type === 'category'">{{ item.title }}</h3>
|
||||
<div *ngIf="item.type !== 'category'" class="endpoint-container" id="{{ item.fragment }}">
|
||||
|
||||
@@ -220,6 +220,22 @@ h3 {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#disclaimer {
|
||||
background-color: #1d1f31;
|
||||
padding: 24px;
|
||||
margin: 24px 0;
|
||||
}
|
||||
|
||||
#disclaimer svg {
|
||||
width: 50px;
|
||||
height: auto;
|
||||
margin-right: 32px;
|
||||
}
|
||||
|
||||
#disclaimer p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
|
||||
h3 {
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
<pre><code [innerText]="wrapEsModule(code)"></code></pre>
|
||||
</ng-template>
|
||||
</li>
|
||||
<li ngbNavItem *ngIf="showCodeExample[network] && network !== 'liquid' && network !== 'liquidtestnet'" role="presentation">
|
||||
<li ngbNavItem *ngIf="code.codeTemplate.python && network !== 'liquid' && network !== 'liquidtestnet'" role="presentation">
|
||||
<a ngbNavLink (click)="adjustContainerHeight( $event )" role="tab">Python</a>
|
||||
<ng-template ngbNavContent>
|
||||
<div class="subtitle"><ng-container i18n="API Docs code example">Code Example</ng-container> <app-clipboard [text]="wrapEsModule(code)"></app-clipboard></div>
|
||||
|
||||
@@ -152,6 +152,11 @@ export interface RewardStats {
|
||||
totalTx: number;
|
||||
}
|
||||
|
||||
export interface AuditScore {
|
||||
hash: string;
|
||||
matchRate?: number;
|
||||
}
|
||||
|
||||
export interface ITopNodesPerChannels {
|
||||
publicKey: string,
|
||||
alias: string,
|
||||
|
||||
31
frontend/src/app/lightning/node/liquidity-ad.ts
Normal file
31
frontend/src/app/lightning/node/liquidity-ad.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
export interface ILiquidityAd {
|
||||
funding_weight: number;
|
||||
lease_fee_basis: number; // lease fee rate in parts-per-thousandth
|
||||
lease_fee_base_sat: number; // fixed lease fee in sats
|
||||
channel_fee_max_rate: number; // max routing fee rate in parts-per-thousandth
|
||||
channel_fee_max_base: number; // max routing base fee in milli-sats
|
||||
compact_lease?: string;
|
||||
}
|
||||
|
||||
export function parseLiquidityAdHex(compact_lease: string): ILiquidityAd | false {
|
||||
if (!compact_lease || compact_lease.length < 20 || compact_lease.length > 28) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const liquidityAd: ILiquidityAd = {
|
||||
funding_weight: parseInt(compact_lease.slice(0, 4), 16),
|
||||
lease_fee_basis: parseInt(compact_lease.slice(4, 8), 16),
|
||||
channel_fee_max_rate: parseInt(compact_lease.slice(8, 12), 16),
|
||||
lease_fee_base_sat: parseInt(compact_lease.slice(12, 20), 16),
|
||||
channel_fee_max_base: compact_lease.length > 20 ? parseInt(compact_lease.slice(20), 16) : 0,
|
||||
}
|
||||
if (Object.values(liquidityAd).reduce((valid: boolean, value: number): boolean => (valid && !isNaN(value) && value >= 0), true)) {
|
||||
liquidityAd.compact_lease = compact_lease;
|
||||
return liquidityAd;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,10 @@
|
||||
<span i18n="unknown">Unknown</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr *ngIf="(avgChannelDistance$ | async) as avgDistance;">
|
||||
<td i18n="lightning.avg-distance" class="text-truncate">Avg channel distance</td>
|
||||
<td>{{ avgDistance | number : '1.0-0' }} <span class="symbol">km</span> <span class="separator">/</span> {{ kmToMiles(avgDistance) | number : '1.0-0' }} <span class="symbol">mi</span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -125,6 +129,93 @@
|
||||
<app-clipboard [button]="true" [text]="node.socketsObject[selectedSocketIndex].socket" [leftPadding]="false"></app-clipboard>
|
||||
</div>
|
||||
|
||||
<div *ngIf="hasDetails" [hidden]="!showDetails" id="details" class="details mt-3">
|
||||
<div class="box">
|
||||
<ng-template [ngIf]="liquidityAd">
|
||||
<div class="detail-section">
|
||||
<h5 class="mb-3" i18n="node.liquidity-ad">Liquidity ad</h5>
|
||||
<div class="row">
|
||||
<div class="col-md">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="label" i18n="liquidity-ad.lease-fee-rate|Liquidity ad lease fee rate">Lease fee rate</td>
|
||||
<td>
|
||||
<span class="d-inline-block">
|
||||
{{ liquidityAd.lease_fee_basis !== null ? ((liquidityAd.lease_fee_basis * 1000) | amountShortener : 2 : undefined : true) : '-' }} <span class="symbol">ppm {{ liquidityAd.lease_fee_basis !== null ? '(' + (liquidityAd.lease_fee_basis / 10 | amountShortener : 2 : undefined : true) + '%)' : '' }}</span>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label" i18n="liquidity-ad.lease-base-fee">Lease base fee</td>
|
||||
<td>
|
||||
<app-sats [valueOverride]="liquidityAd.lease_fee_base_sat === null ? '- ' : undefined" [satoshis]="liquidityAd.lease_fee_base_sat"></app-sats>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label" i18n="liquidity-ad.funding-weight">Funding weight</td>
|
||||
<td [innerHTML]="'‎' + (liquidityAd.funding_weight | wuBytes: 2)"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-md">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="label" i18n="liquidity-ad.channel-fee-rate|Liquidity ad channel fee rate">Channel fee rate</td>
|
||||
<td>
|
||||
<span class="d-inline-block">
|
||||
{{ liquidityAd.channel_fee_max_rate !== null ? ((liquidityAd.channel_fee_max_rate * 1000) | amountShortener : 2 : undefined : true) : '-' }} <span class="symbol">ppm {{ liquidityAd.channel_fee_max_rate !== null ? '(' + (liquidityAd.channel_fee_max_rate / 10 | amountShortener : 2 : undefined : true) + '%)' : '' }}</span>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label" i18n="liquidity-ad.channel-base-fee">Channel base fee</td>
|
||||
<td>
|
||||
<span *ngIf="liquidityAd.channel_fee_max_base !== null">
|
||||
{{ liquidityAd.channel_fee_max_base | amountShortener : 0 }}
|
||||
<span class="symbol" i18n="shared.m-sats">mSats</span>
|
||||
</span>
|
||||
<span *ngIf="liquidityAd.channel_fee_max_base === null">
|
||||
-
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="label" i18n="liquidity-ad.compact-lease">Compact lease</td>
|
||||
<td class="compact-lease">{{ liquidityAd.compact_lease }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
<ng-template [ngIf]="tlvRecords?.length">
|
||||
<div class="detail-section">
|
||||
<h5 class="mb-3" i18n="node.tlv.records">TLV extension records</h5>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr *ngFor="let recordItem of tlvRecords">
|
||||
<td class="tlv-type">{{ recordItem.type }}</td>
|
||||
<td class="tlv-payload">{{ recordItem.payload }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="hasDetails" class="text-right mt-3">
|
||||
<button type="button" class="btn btn-outline-info btn-sm btn-details" (click)="toggleShowDetails()" i18n="node.details|Node Details">Details</button>
|
||||
</div>
|
||||
|
||||
<div *ngIf="!error">
|
||||
<div class="row" *ngIf="node.as_number && node.active_channel_count">
|
||||
<div class="col-sm">
|
||||
|
||||
@@ -72,3 +72,36 @@ app-fiat {
|
||||
height: 28px !important;
|
||||
};
|
||||
}
|
||||
|
||||
.details {
|
||||
|
||||
.detail-section {
|
||||
margin-bottom: 1.5rem;
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.tlv-type {
|
||||
font-size: 12px;
|
||||
color: #ffffff66;
|
||||
}
|
||||
|
||||
.tlv-payload {
|
||||
font-size: 12px;
|
||||
width: 100%;
|
||||
word-break: break-all;
|
||||
white-space: normal;
|
||||
font-family: "Courier New", Courier, monospace;
|
||||
}
|
||||
|
||||
.compact-lease {
|
||||
word-break: break-all;
|
||||
white-space: normal;
|
||||
font-family: "Courier New", Courier, monospace;
|
||||
}
|
||||
}
|
||||
|
||||
.separator {
|
||||
margin: 0 1em;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
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 { catchError, map, switchMap, tap } from 'rxjs/operators';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { LightningApiService } from '../lightning-api.service';
|
||||
import { GeolocationData } from '../../shared/components/geolocation/geolocation.component';
|
||||
import { ILiquidityAd, parseLiquidityAdHex } from './liquidity-ad';
|
||||
import { haversineDistance, kmToMiles } from 'src/app/shared/common.utils';
|
||||
|
||||
interface CustomRecord {
|
||||
type: string;
|
||||
payload: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-node',
|
||||
@@ -24,8 +32,16 @@ export class NodeComponent implements OnInit {
|
||||
channelListLoading = false;
|
||||
clearnetSocketCount = 0;
|
||||
torSocketCount = 0;
|
||||
hasDetails = false;
|
||||
showDetails = false;
|
||||
liquidityAd: ILiquidityAd;
|
||||
tlvRecords: CustomRecord[];
|
||||
avgChannelDistance$: Observable<number | null>;
|
||||
|
||||
kmToMiles = kmToMiles;
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private lightningApiService: LightningApiService,
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private seoService: SeoService,
|
||||
@@ -36,6 +52,8 @@ export class NodeComponent implements OnInit {
|
||||
.pipe(
|
||||
switchMap((params: ParamMap) => {
|
||||
this.publicKey = params.get('public_key');
|
||||
this.tlvRecords = [];
|
||||
this.liquidityAd = null;
|
||||
return this.lightningApiService.getNode$(params.get('public_key'));
|
||||
}),
|
||||
map((node) => {
|
||||
@@ -79,6 +97,26 @@ export class NodeComponent implements OnInit {
|
||||
|
||||
return node;
|
||||
}),
|
||||
tap((node) => {
|
||||
this.hasDetails = Object.keys(node.custom_records).length > 0;
|
||||
for (const [type, payload] of Object.entries(node.custom_records)) {
|
||||
if (typeof payload !== 'string') {
|
||||
break;
|
||||
}
|
||||
|
||||
let parsed = false;
|
||||
if (type === '1') {
|
||||
const ad = parseLiquidityAdHex(payload);
|
||||
if (ad) {
|
||||
parsed = true;
|
||||
this.liquidityAd = ad;
|
||||
}
|
||||
}
|
||||
if (!parsed) {
|
||||
this.tlvRecords.push({ type, payload });
|
||||
}
|
||||
}
|
||||
}),
|
||||
catchError(err => {
|
||||
this.error = err;
|
||||
return [{
|
||||
@@ -87,6 +125,30 @@ export class NodeComponent implements OnInit {
|
||||
}];
|
||||
})
|
||||
);
|
||||
|
||||
this.avgChannelDistance$ = this.activatedRoute.paramMap
|
||||
.pipe(
|
||||
switchMap((params: ParamMap) => {
|
||||
return this.apiService.getChannelsGeo$(params.get('public_key'), 'nodepage');
|
||||
}),
|
||||
map((channelsGeo) => {
|
||||
if (channelsGeo?.length) {
|
||||
const totalDistance = channelsGeo.reduce((sum, chan) => {
|
||||
return sum + haversineDistance(chan[3], chan[2], chan[7], chan[6]);
|
||||
}, 0);
|
||||
return totalDistance / channelsGeo.length;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
catchError(() => {
|
||||
return null;
|
||||
})
|
||||
) as Observable<number | null>;
|
||||
}
|
||||
|
||||
toggleShowDetails(): void {
|
||||
this.showDetails = !this.showDetails;
|
||||
}
|
||||
|
||||
changeSocket(index: number) {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<td>{{ isp?.id }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="lightning.node-count">Nodes</td>
|
||||
<td i18n="lightning.active-node-count">Active nodes</td>
|
||||
<td>{{ ispNodes.nodes.length }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { CpfpInfo, OptimizedMempoolStats, AddressInformation, LiquidPegs, ITranslators,
|
||||
PoolStat, BlockExtended, TransactionStripped, RewardStats } from '../interfaces/node-api.interface';
|
||||
PoolStat, BlockExtended, TransactionStripped, RewardStats, AuditScore } from '../interfaces/node-api.interface';
|
||||
import { Observable } from 'rxjs';
|
||||
import { StateService } from './state.service';
|
||||
import { WebsocketResponse } from '../interfaces/websocket.interface';
|
||||
@@ -234,6 +234,19 @@ export class ApiService {
|
||||
);
|
||||
}
|
||||
|
||||
getBlockAuditScores$(from: number): Observable<AuditScore[]> {
|
||||
return this.httpClient.get<AuditScore[]>(
|
||||
this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/blocks/audit/scores` +
|
||||
(from !== undefined ? `/${from}` : ``)
|
||||
);
|
||||
}
|
||||
|
||||
getBlockAuditScore$(hash: string) : Observable<any> {
|
||||
return this.httpClient.get<any>(
|
||||
this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/blocks/audit/score/` + hash
|
||||
);
|
||||
}
|
||||
|
||||
getRewardStats$(blockCount: number = 144): Observable<RewardStats> {
|
||||
return this.httpClient.get<RewardStats>(this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/reward-stats/${blockCount}`);
|
||||
}
|
||||
|
||||
@@ -112,6 +112,8 @@ export class StateService {
|
||||
timeLtr: BehaviorSubject<boolean>;
|
||||
hideFlow: BehaviorSubject<boolean>;
|
||||
|
||||
txCache: { [txid: string]: Transaction } = {};
|
||||
|
||||
constructor(
|
||||
@Inject(PLATFORM_ID) private platformId: any,
|
||||
@Inject(LOCALE_ID) private locale: string,
|
||||
@@ -265,4 +267,19 @@ export class StateService {
|
||||
isLiquid() {
|
||||
return this.network === 'liquid' || this.network === 'liquidtestnet';
|
||||
}
|
||||
|
||||
setTxCache(transactions) {
|
||||
this.txCache = {};
|
||||
transactions.forEach(tx => {
|
||||
this.txCache[tx.txid] = tx;
|
||||
});
|
||||
}
|
||||
|
||||
getTxFromCache(txid) {
|
||||
if (this.txCache && this.txCache[txid]) {
|
||||
return this.txCache[txid];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,3 +118,21 @@ export function convertRegion(input, to: 'name' | 'abbreviated'): string {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function haversineDistance(lat1: number, lon1: number, lat2: number, lon2: number): number {
|
||||
const rlat1 = lat1 * Math.PI / 180;
|
||||
const rlon1 = lon1 * Math.PI / 180;
|
||||
const rlat2 = lat2 * Math.PI / 180;
|
||||
const rlon2 = lon2 * Math.PI / 180;
|
||||
|
||||
const dlat = Math.sin((rlat2 - rlat1) / 2);
|
||||
const dlon = Math.sin((rlon2 - rlon1) / 2);
|
||||
const a = Math.min(1, Math.max(0, (dlat * dlat) + (Math.cos(rlat1) * Math.cos(rlat2) * dlon * dlon)));
|
||||
const d = 2 * 6371 * Math.asin(Math.sqrt(a));
|
||||
|
||||
return d;
|
||||
}
|
||||
|
||||
export function kmToMiles(km: number): number {
|
||||
return km * 0.62137119;
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>mempool - Bisq Markets</title>
|
||||
<script src="/resources/config.js"></script>
|
||||
<base href="/">
|
||||
|
||||
<meta name="description" content="The Mempool Open Source Project™ - Explore the full Bitcoin ecosystem.">
|
||||
@@ -31,11 +33,13 @@
|
||||
<link rel="manifest" href="/resources/bisq/favicons/site.webmanifest">
|
||||
<link rel="mask-icon" href="/resources/bisq/favicons/safari-pinned-tab.svg" color="#5bbad5">
|
||||
<link rel="shortcut icon" href="/resources/bisq/favicons/favicon.ico">
|
||||
|
||||
|
||||
<link id="canonical" rel="canonical" href="https://bisq.markets">
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>mempool - Liquid Network</title>
|
||||
<script src="/resources/config.js"></script>
|
||||
<base href="/">
|
||||
|
||||
<meta name="description" content="The Mempool Open Source Project™ - Explore the full Bitcoin ecosystem.">
|
||||
@@ -17,7 +19,7 @@
|
||||
<meta property="twitter:description" content="Explore the full Bitcoin ecosystem with mempool.space™" />
|
||||
<meta property="twitter:image:src" content="https://liquid.network/resources/liquid/liquid-network-preview.png" />
|
||||
<meta property="twitter:domain" content="liquid.network">
|
||||
|
||||
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/resources/liquid/favicons/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="48x48" href="/resources/liquid/favicons/favicon-48x48.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/resources/liquid/favicons/favicon-32x32.png">
|
||||
@@ -33,7 +35,9 @@
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>mempool - Bitcoin Explorer</title>
|
||||
<script src="/resources/config.js"></script>
|
||||
<base href="/">
|
||||
|
||||
<meta name="description" content="The Mempool Open Source Project™ - Explore the full Bitcoin ecosystem." />
|
||||
@@ -17,7 +19,7 @@
|
||||
<meta property="twitter:description" content="Explore the full Bitcoin ecosystem with mempool.space™" />
|
||||
<meta property="twitter:image:src" content="https://mempool.space/resources/mempool-space-preview.png" />
|
||||
<meta property="twitter:domain" content="mempool.space">
|
||||
|
||||
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/resources/favicons/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/resources/favicons/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/resources/favicons/favicon-16x16.png">
|
||||
@@ -32,7 +34,9 @@
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
1
frontend/src/resources/profile/nunchuk.svg
Normal file
1
frontend/src/resources/profile/nunchuk.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="120" height="120" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#a)"><path d="M93.28 40.282 69.81 63.75a1.107 1.107 0 0 1-1.889-.784v-10.81h-14.24a1.107 1.107 0 0 1-1.106-1.108V40.3H38.939L13.156 66.15 26.72 79.718l23.265-23.27a1.108 1.108 0 0 1 1.89.783v10.976h14.442a1.108 1.108 0 0 1 1.107 1.108V79.69H81.06l25.783-25.85-13.564-13.559Z" fill="#FFCB2E"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h120v120H0z"/></clipPath></defs></svg>
|
||||
|
After Width: | Height: | Size: 478 B |
Reference in New Issue
Block a user