Merge branch 'master' into mononaut/testmempoolaccept
28
backend/package-lock.json
generated
@ -18,12 +18,12 @@
|
||||
"crypto-js": "~4.2.0",
|
||||
"express": "~4.19.2",
|
||||
"maxmind": "~4.3.11",
|
||||
"mysql2": "~3.9.4",
|
||||
"mysql2": "~3.9.7",
|
||||
"redis": "^4.6.6",
|
||||
"rust-gbt": "file:./rust-gbt",
|
||||
"socks-proxy-agent": "~7.0.0",
|
||||
"typescript": "~4.9.3",
|
||||
"ws": "~8.16.0"
|
||||
"ws": "~8.17.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/code-frame": "^7.18.6",
|
||||
@ -6197,9 +6197,9 @@
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"node_modules/mysql2": {
|
||||
"version": "3.9.4",
|
||||
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.9.4.tgz",
|
||||
"integrity": "sha512-OEESQuwxMza803knC1YSt7NMuc1BrK9j7gZhCSs2WAyxr1vfiI7QLaLOKTh5c9SWGz98qVyQUbK8/WckevNQhg==",
|
||||
"version": "3.9.7",
|
||||
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.9.7.tgz",
|
||||
"integrity": "sha512-KnJT8vYRcNAZv73uf9zpXqNbvBG7DJrs+1nACsjZP1HMJ1TgXEy8wnNilXAn/5i57JizXKtrUtwDB7HxT9DDpw==",
|
||||
"dependencies": {
|
||||
"denque": "^2.1.0",
|
||||
"generate-function": "^2.3.1",
|
||||
@ -7690,9 +7690,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.16.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz",
|
||||
"integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==",
|
||||
"version": "8.17.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz",
|
||||
"integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
@ -12382,9 +12382,9 @@
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"mysql2": {
|
||||
"version": "3.9.4",
|
||||
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.9.4.tgz",
|
||||
"integrity": "sha512-OEESQuwxMza803knC1YSt7NMuc1BrK9j7gZhCSs2WAyxr1vfiI7QLaLOKTh5c9SWGz98qVyQUbK8/WckevNQhg==",
|
||||
"version": "3.9.7",
|
||||
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.9.7.tgz",
|
||||
"integrity": "sha512-KnJT8vYRcNAZv73uf9zpXqNbvBG7DJrs+1nACsjZP1HMJ1TgXEy8wnNilXAn/5i57JizXKtrUtwDB7HxT9DDpw==",
|
||||
"requires": {
|
||||
"denque": "^2.1.0",
|
||||
"generate-function": "^2.3.1",
|
||||
@ -13424,9 +13424,9 @@
|
||||
}
|
||||
},
|
||||
"ws": {
|
||||
"version": "8.16.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz",
|
||||
"integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==",
|
||||
"version": "8.17.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz",
|
||||
"integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==",
|
||||
"requires": {}
|
||||
},
|
||||
"y18n": {
|
||||
|
@ -47,12 +47,12 @@
|
||||
"crypto-js": "~4.2.0",
|
||||
"express": "~4.19.2",
|
||||
"maxmind": "~4.3.11",
|
||||
"mysql2": "~3.9.4",
|
||||
"mysql2": "~3.9.7",
|
||||
"rust-gbt": "file:./rust-gbt",
|
||||
"redis": "^4.6.6",
|
||||
"socks-proxy-agent": "~7.0.0",
|
||||
"typescript": "~4.9.3",
|
||||
"ws": "~8.16.0"
|
||||
"ws": "~8.17.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/code-frame": "^7.18.6",
|
||||
|
@ -54,9 +54,11 @@ class ChannelsRoutes {
|
||||
|
||||
if (index < -1) {
|
||||
res.status(400).send('Invalid index');
|
||||
return;
|
||||
}
|
||||
if (['open', 'active', 'closed'].includes(status) === false) {
|
||||
res.status(400).send('Invalid status');
|
||||
return;
|
||||
}
|
||||
|
||||
const channels = await channelsApi.$getChannelsForNode(req.query.public_key, index, 10, status);
|
||||
|
@ -3,6 +3,7 @@ import * as WebSocket from 'ws';
|
||||
import {
|
||||
BlockExtended, TransactionExtended, MempoolTransactionExtended, WebsocketResponse,
|
||||
OptimizedStatistic, ILoadingIndicators, GbtCandidates, TxTrackingInfo,
|
||||
MempoolBlockDelta, MempoolDelta, MempoolDeltaTxids
|
||||
} from '../mempool.interfaces';
|
||||
import blocks from './blocks';
|
||||
import memPool from './mempool';
|
||||
@ -364,6 +365,18 @@ class WebsocketHandler {
|
||||
client['track-donation'] = parsedMessage['track-donation'];
|
||||
}
|
||||
|
||||
if (parsedMessage['track-mempool-txids'] === true) {
|
||||
client['track-mempool-txids'] = true;
|
||||
} else if (parsedMessage['track-mempool-txids'] === false) {
|
||||
delete client['track-mempool-txids'];
|
||||
}
|
||||
|
||||
if (parsedMessage['track-mempool'] === true) {
|
||||
client['track-mempool'] = true;
|
||||
} else if (parsedMessage['track-mempool'] === false) {
|
||||
delete client['track-mempool'];
|
||||
}
|
||||
|
||||
if (Object.keys(response).length) {
|
||||
client.send(this.serializeResponse(response));
|
||||
}
|
||||
@ -545,6 +558,33 @@ class WebsocketHandler {
|
||||
|
||||
const latestTransactions = memPool.getLatestTransactions();
|
||||
|
||||
if (memPool.isInSync()) {
|
||||
this.mempoolSequence++;
|
||||
}
|
||||
|
||||
const replacedTransactions: { replaced: string, by: TransactionExtended }[] = [];
|
||||
for (const tx of newTransactions) {
|
||||
if (rbfTransactions[tx.txid]) {
|
||||
for (const replaced of rbfTransactions[tx.txid]) {
|
||||
replacedTransactions.push({ replaced: replaced.txid, by: tx });
|
||||
}
|
||||
}
|
||||
}
|
||||
const mempoolDeltaTxids: MempoolDeltaTxids = {
|
||||
sequence: this.mempoolSequence,
|
||||
added: newTransactions.map(tx => tx.txid),
|
||||
removed: deletedTransactions.map(tx => tx.txid),
|
||||
mined: [],
|
||||
replaced: replacedTransactions.map(replacement => ({ replaced: replacement.replaced, by: replacement.by.txid })),
|
||||
};
|
||||
const mempoolDelta: MempoolDelta = {
|
||||
sequence: this.mempoolSequence,
|
||||
added: newTransactions,
|
||||
removed: deletedTransactions.map(tx => tx.txid),
|
||||
mined: [],
|
||||
replaced: replacedTransactions,
|
||||
};
|
||||
|
||||
// update init data
|
||||
const socketDataFields = {
|
||||
'mempoolInfo': mempoolInfo,
|
||||
@ -604,10 +644,6 @@ class WebsocketHandler {
|
||||
const addressCache = this.makeAddressCache(newTransactions);
|
||||
const removedAddressCache = this.makeAddressCache(deletedTransactions);
|
||||
|
||||
if (memPool.isInSync()) {
|
||||
this.mempoolSequence++;
|
||||
}
|
||||
|
||||
// TODO - Fix indentation after PR is merged
|
||||
for (const server of this.webSocketServers) {
|
||||
server.clients.forEach(async (client) => {
|
||||
@ -847,6 +883,14 @@ class WebsocketHandler {
|
||||
response['rbfLatestSummary'] = getCachedResponse('rbfLatestSummary', rbfSummary);
|
||||
}
|
||||
|
||||
if (client['track-mempool-txids']) {
|
||||
response['mempool-txids'] = getCachedResponse('mempool-txids', mempoolDeltaTxids);
|
||||
}
|
||||
|
||||
if (client['track-mempool']) {
|
||||
response['mempool-transactions'] = getCachedResponse('mempool-transactions', mempoolDelta);
|
||||
}
|
||||
|
||||
if (Object.keys(response).length) {
|
||||
client.send(this.serializeResponse(response));
|
||||
}
|
||||
@ -992,6 +1036,31 @@ class WebsocketHandler {
|
||||
|
||||
const mBlocksWithTransactions = mempoolBlocks.getMempoolBlocksWithTransactions();
|
||||
|
||||
if (memPool.isInSync()) {
|
||||
this.mempoolSequence++;
|
||||
}
|
||||
|
||||
const replacedTransactions: { replaced: string, by: TransactionExtended }[] = [];
|
||||
for (const txid of Object.keys(rbfTransactions)) {
|
||||
for (const replaced of rbfTransactions[txid].replaced) {
|
||||
replacedTransactions.push({ replaced: replaced.txid, by: rbfTransactions[txid].replacedBy });
|
||||
}
|
||||
}
|
||||
const mempoolDeltaTxids: MempoolDeltaTxids = {
|
||||
sequence: this.mempoolSequence,
|
||||
added: [],
|
||||
removed: [],
|
||||
mined: transactions.map(tx => tx.txid),
|
||||
replaced: replacedTransactions.map(replacement => ({ replaced: replacement.replaced, by: replacement.by.txid })),
|
||||
};
|
||||
const mempoolDelta: MempoolDelta = {
|
||||
sequence: this.mempoolSequence,
|
||||
added: [],
|
||||
removed: [],
|
||||
mined: transactions.map(tx => tx.txid),
|
||||
replaced: replacedTransactions,
|
||||
};
|
||||
|
||||
const responseCache = { ...this.socketData };
|
||||
function getCachedResponse(key, data): string {
|
||||
if (!responseCache[key]) {
|
||||
@ -1000,10 +1069,6 @@ class WebsocketHandler {
|
||||
return responseCache[key];
|
||||
}
|
||||
|
||||
if (memPool.isInSync()) {
|
||||
this.mempoolSequence++;
|
||||
}
|
||||
|
||||
// TODO - Fix indentation after PR is merged
|
||||
for (const server of this.webSocketServers) {
|
||||
server.clients.forEach((client) => {
|
||||
@ -1185,6 +1250,14 @@ class WebsocketHandler {
|
||||
}
|
||||
}
|
||||
|
||||
if (client['track-mempool-txids']) {
|
||||
response['mempool-txids'] = getCachedResponse('mempool-txids', mempoolDeltaTxids);
|
||||
}
|
||||
|
||||
if (client['track-mempool']) {
|
||||
response['mempool-transactions'] = getCachedResponse('mempool-transactions', mempoolDelta);
|
||||
}
|
||||
|
||||
if (Object.keys(response).length) {
|
||||
client.send(this.serializeResponse(response));
|
||||
}
|
||||
|
@ -71,6 +71,22 @@ export interface MempoolBlockDelta {
|
||||
changed: MempoolDeltaChange[];
|
||||
}
|
||||
|
||||
export interface MempoolDeltaTxids {
|
||||
sequence: number,
|
||||
added: string[];
|
||||
removed: string[];
|
||||
mined: string[];
|
||||
replaced: { replaced: string, by: string }[];
|
||||
}
|
||||
|
||||
export interface MempoolDelta {
|
||||
sequence: number,
|
||||
added: MempoolTransactionExtended[];
|
||||
removed: string[];
|
||||
mined: string[];
|
||||
replaced: { replaced: string, by: TransactionExtended }[];
|
||||
}
|
||||
|
||||
interface VinStrippedToScriptsig {
|
||||
scriptsig: string;
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ RUN npm install --omit=dev --omit=optional
|
||||
|
||||
RUN npm run build
|
||||
|
||||
FROM nginx:1.25.4-alpine
|
||||
FROM nginx:1.26.0-alpine
|
||||
|
||||
WORKDIR /patch
|
||||
|
||||
|
1
frontend/.gitignore
vendored
@ -63,6 +63,7 @@ src/resources/pools.json
|
||||
src/resources/mining-pools/*
|
||||
src/resources/**/*.mp4
|
||||
src/resources/**/*.vtt
|
||||
src/resources/customize.js
|
||||
|
||||
# environment config
|
||||
mempool-frontend-config.json
|
||||
|
@ -166,6 +166,7 @@
|
||||
"src/resources",
|
||||
"src/robots.txt",
|
||||
"src/config.js",
|
||||
"src/customize.js",
|
||||
"src/config.template.js"
|
||||
],
|
||||
"styles": [
|
||||
|
44
frontend/custom-sv-config.json
Normal file
@ -0,0 +1,44 @@
|
||||
{
|
||||
"theme": "contrast",
|
||||
"enterprise": "onbtc",
|
||||
"branding": {
|
||||
"name": "onbtc",
|
||||
"title": "Oficina Nacional del Bitcoin",
|
||||
"site_id": 19,
|
||||
"header_img": "/resources/onbtc.svg",
|
||||
"img": "/resources/elsalvador.svg",
|
||||
"rounded_corner": true
|
||||
},
|
||||
"dashboard": {
|
||||
"widgets": [
|
||||
{
|
||||
"component": "fees"
|
||||
},
|
||||
{
|
||||
"component": "balance",
|
||||
"props": {
|
||||
"address": "32ixEdVJWo3kmvJGMTZq5jAQVZZeuwnqzo"
|
||||
}
|
||||
},
|
||||
{
|
||||
"component": "goggles"
|
||||
},
|
||||
{
|
||||
"component": "address",
|
||||
"props": {
|
||||
"address": "32ixEdVJWo3kmvJGMTZq5jAQVZZeuwnqzo",
|
||||
"period": "1m"
|
||||
}
|
||||
},
|
||||
{
|
||||
"component": "blocks"
|
||||
},
|
||||
{
|
||||
"component": "addressTransactions",
|
||||
"props": {
|
||||
"address": "32ixEdVJWo3kmvJGMTZq5jAQVZZeuwnqzo"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
@ -4,6 +4,7 @@ const { spawnSync } = require('child_process');
|
||||
const CONFIG_FILE_NAME = 'mempool-frontend-config.json';
|
||||
const GENERATED_CONFIG_FILE_NAME = 'src/resources/config.js';
|
||||
const GENERATED_TEMPLATE_CONFIG_FILE_NAME = 'src/resources/config.template.js';
|
||||
const GENERATED_CUSTOMIZATION_FILE_NAME = 'src/resources/customize.js';
|
||||
|
||||
let settings = [];
|
||||
let configContent = {};
|
||||
@ -109,6 +110,23 @@ writeConfigTemplate(GENERATED_TEMPLATE_CONFIG_FILE_NAME, newConfigTemplate);
|
||||
|
||||
const currentConfig = readConfig(GENERATED_CONFIG_FILE_NAME);
|
||||
|
||||
let customConfigJs = '';
|
||||
if (configContent && configContent.CUSTOMIZATION) {
|
||||
const customConfig = readConfig(configContent.CUSTOMIZATION);
|
||||
if (customConfig) {
|
||||
console.log(`Customizing frontend using ${configContent.CUSTOMIZATION}`);
|
||||
customConfigJs = `(function (window) {
|
||||
window.__env = window.__env || {};
|
||||
window.__env.customize = ${customConfig};
|
||||
}((typeof global !== 'undefined') ? global : this));
|
||||
`;
|
||||
} else {
|
||||
throw new Error('Failed to load customization file');
|
||||
}
|
||||
}
|
||||
|
||||
writeConfig(GENERATED_CUSTOMIZATION_FILE_NAME, customConfigJs);
|
||||
|
||||
if (currentConfig && currentConfig === newConfig) {
|
||||
console.log(`No configuration updates, skipping ${GENERATED_CONFIG_FILE_NAME} file update`);
|
||||
return;
|
||||
|
@ -23,7 +23,7 @@
|
||||
<span class="font-weight-bold">Accelerate</span>
|
||||
<span style="color: rgb(186, 186, 186); font-size: 14px;">Confirmation expected within ~30 minutes<br>
|
||||
@if (!calculating) {
|
||||
<app-fiat [value]="cost"></app-fiat>fee (<span><small style="font-family: monospace;">{{ cost | number }}</small> <span class="symbol" i18n="shared.sats|sats">sats</span></span>)
|
||||
<app-fiat [value]="cost"></app-fiat>fee (<span><small style="font-family: monospace;">{{ cost | number }}</small> <span class="symbol" i18n="shared.sats">sats</span></span>)
|
||||
} @else {
|
||||
<span class="estimating">Calculating cost...</span>
|
||||
}
|
||||
|
@ -1,14 +1,14 @@
|
||||
<app-indexing-progress></app-indexing-progress>
|
||||
<app-indexing-progress *ngIf="!widget"></app-indexing-progress>
|
||||
|
||||
<div class="full-container">
|
||||
<div class="card-header mb-0 mb-md-2">
|
||||
<div [class.full-container]="!widget">
|
||||
<div *ngIf="!widget" class="card-header mb-0 mb-md-2">
|
||||
<div class="d-flex d-md-block align-items-baseline">
|
||||
<span i18n="address.balance-history">Balance History</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="!error">
|
||||
<div class="chart" *browserOnly echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
<div [class]="!widget ? 'chart' : 'chart-widget'" *browserOnly [style]="{ height: widget ? ((height + 20) + 'px') : null}" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
(chartInit)="onChartInit($event)">
|
||||
</div>
|
||||
<div class="text-center loadingGraphs" *ngIf="isLoading">
|
||||
@ -20,4 +20,8 @@
|
||||
<p class="error">{{ error }}</p>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<div class="text-center loadingGraphs" *ngIf="!stateService.isBrowser || isLoading">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -66,7 +66,6 @@
|
||||
.chart-widget {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: 270px;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
|
@ -1,12 +1,22 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { echarts, EChartsOption } from '../../graphs/echarts';
|
||||
import { of } from 'rxjs';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
import { ChainStats } from '../../interfaces/electrs.interface';
|
||||
import { AddressTxSummary, ChainStats } from '../../interfaces/electrs.interface';
|
||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
||||
import { AmountShortenerPipe } from '../../shared/pipes/amount-shortener.pipe';
|
||||
import { Router } from '@angular/router';
|
||||
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
||||
import { StateService } from '../../services/state.service';
|
||||
|
||||
const periodSeconds = {
|
||||
'1d': (60 * 60 * 24),
|
||||
'3d': (60 * 60 * 24 * 3),
|
||||
'1w': (60 * 60 * 24 * 7),
|
||||
'1m': (60 * 60 * 24 * 30),
|
||||
'6m': (60 * 60 * 24 * 180),
|
||||
'1y': (60 * 60 * 24 * 365),
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: 'app-address-graph',
|
||||
@ -26,8 +36,12 @@ export class AddressGraphComponent implements OnChanges {
|
||||
@Input() address: string;
|
||||
@Input() isPubkey: boolean = false;
|
||||
@Input() stats: ChainStats;
|
||||
@Input() addressSummary$: Observable<AddressTxSummary[]> | null;
|
||||
@Input() period: '1d' | '3d' | '1w' | '1m' | '6m' | '1y' | 'all' = 'all';
|
||||
@Input() height: number = 200;
|
||||
@Input() right: number | string = 10;
|
||||
@Input() left: number | string = 70;
|
||||
@Input() widget: boolean = false;
|
||||
|
||||
data: any[] = [];
|
||||
hoverData: any[] = [];
|
||||
@ -43,6 +57,7 @@ export class AddressGraphComponent implements OnChanges {
|
||||
|
||||
constructor(
|
||||
@Inject(LOCALE_ID) public locale: string,
|
||||
public stateService: StateService,
|
||||
private electrsApiService: ElectrsApiService,
|
||||
private router: Router,
|
||||
private amountShortenerPipe: AmountShortenerPipe,
|
||||
@ -52,14 +67,17 @@ export class AddressGraphComponent implements OnChanges {
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
this.isLoading = true;
|
||||
(this.isPubkey
|
||||
if (!this.address || !this.stats) {
|
||||
return;
|
||||
}
|
||||
(this.addressSummary$ || (this.isPubkey
|
||||
? this.electrsApiService.getScriptHashSummary$((this.address.length === 66 ? '21' : '41') + this.address + 'ac')
|
||||
: this.electrsApiService.getAddressSummary$(this.address)).pipe(
|
||||
catchError(e => {
|
||||
this.error = `Failed to fetch address balance history: ${e?.status || ''} ${e?.statusText || 'unknown error'}`;
|
||||
return of(null);
|
||||
}),
|
||||
).subscribe(addressSummary => {
|
||||
)).subscribe(addressSummary => {
|
||||
if (addressSummary) {
|
||||
this.error = null;
|
||||
this.prepareChartOptions(addressSummary);
|
||||
@ -70,14 +88,24 @@ export class AddressGraphComponent implements OnChanges {
|
||||
}
|
||||
|
||||
prepareChartOptions(summary): void {
|
||||
let total = (this.stats.funded_txo_sum - this.stats.spent_txo_sum); // + (summary[0]?.value || 0);
|
||||
let total = (this.stats.funded_txo_sum - this.stats.spent_txo_sum);
|
||||
this.data = summary.map(d => {
|
||||
const balance = total;
|
||||
total -= d.value;
|
||||
return [d.time * 1000, balance, d];
|
||||
}).reverse();
|
||||
|
||||
const maxValue = this.data.reduce((acc, d) => Math.max(acc, Math.abs(d[1])), 0);
|
||||
if (this.period !== 'all') {
|
||||
const now = Date.now();
|
||||
const start = now - (periodSeconds[this.period] * 1000);
|
||||
this.data = this.data.filter(d => d[0] >= start);
|
||||
this.data.push(
|
||||
{value: [now, this.stats.funded_txo_sum - this.stats.spent_txo_sum], symbol: 'none', tooltip: { show: false }}
|
||||
);
|
||||
}
|
||||
|
||||
const maxValue = this.data.reduce((acc, d) => Math.max(acc, Math.abs(d[1] || d.value[1])), 0);
|
||||
const minValue = this.data.reduce((acc, d) => Math.min(acc, Math.abs(d[1] || d.value[1])), maxValue);
|
||||
|
||||
this.chartOptions = {
|
||||
color: [
|
||||
@ -108,6 +136,9 @@ export class AddressGraphComponent implements OnChanges {
|
||||
},
|
||||
borderColor: '#000',
|
||||
formatter: function (data): string {
|
||||
if (!data?.length || !data[0]?.data?.[2]?.txid) {
|
||||
return '';
|
||||
}
|
||||
const header = data.length === 1
|
||||
? `${data[0].data[2].txid.slice(0, 6)}...${data[0].data[2].txid.slice(-6)}`
|
||||
: `${data.length} transactions`;
|
||||
@ -141,13 +172,17 @@ export class AddressGraphComponent implements OnChanges {
|
||||
axisLabel: {
|
||||
color: 'rgb(110, 112, 121)',
|
||||
formatter: (val): string => {
|
||||
if (maxValue > 1_000_000_000) {
|
||||
let valSpan = maxValue - (this.period === 'all' ? 0 : minValue);
|
||||
if (valSpan > 100_000_000_000) {
|
||||
return `${this.amountShortenerPipe.transform(Math.round(val / 100_000_000), 0)} BTC`;
|
||||
} else if (maxValue > 100_000_000) {
|
||||
}
|
||||
else if (valSpan > 1_000_000_000) {
|
||||
return `${this.amountShortenerPipe.transform(Math.round(val / 100_000_000), 2)} BTC`;
|
||||
} else if (valSpan > 100_000_000) {
|
||||
return `${(val / 100_000_000).toFixed(1)} BTC`;
|
||||
} else if (maxValue > 10_000_000) {
|
||||
} else if (valSpan > 10_000_000) {
|
||||
return `${(val / 100_000_000).toFixed(2)} BTC`;
|
||||
} else if (maxValue > 1_000_000) {
|
||||
} else if (valSpan > 1_000_000) {
|
||||
return `${(val / 100_000_000).toFixed(3)} BTC`;
|
||||
} else {
|
||||
return `${this.amountShortenerPipe.transform(val, 0)} sats`;
|
||||
@ -157,6 +192,7 @@ export class AddressGraphComponent implements OnChanges {
|
||||
splitLine: {
|
||||
show: false,
|
||||
},
|
||||
min: this.period === 'all' ? 0 : 'dataMin'
|
||||
},
|
||||
],
|
||||
series: [
|
||||
|
@ -0,0 +1,32 @@
|
||||
<table class="table latest-transactions">
|
||||
<thead>
|
||||
<th class="table-cell-txid" i18n="dashboard.latest-transactions.txid">TXID</th>
|
||||
<th class="table-cell-satoshis" i18n="dashboard.latest-transactions.amount">Amount</th>
|
||||
<th class="table-cell-fiat">{{ currency }}</th>
|
||||
<th class="table-cell-date" i18n="shared.date">Date</th>
|
||||
</thead>
|
||||
<tbody *ngIf="transactions$ | async as transactions else recentTransactionsSkeleton">
|
||||
<tr *ngFor="let transaction of transactions; let i = index;">
|
||||
<td class="table-cell-txid">
|
||||
<a [routerLink]="['/tx' | relativeUrl, transaction.txid]">
|
||||
<app-truncate [text]="transaction.txid" [lastChars]="5"></app-truncate>
|
||||
</a>
|
||||
</td>
|
||||
<td class="table-cell-satoshis"><app-amount [satoshis]="transaction.value" digitsInfo="1.2-4" [noFiat]="true"></app-amount></td>
|
||||
<td class="table-cell-fiat" ><app-fiat [value]="transaction.value" [blockConversion]="transaction.price" digitsInfo="1.0-0"></app-fiat></td>
|
||||
<td class="table-cell-date"><app-time kind="since" [time]="transaction.time" [fastRender]="true"></app-time></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<div class=""> </div>
|
||||
</table>
|
||||
|
||||
<ng-template #recentTransactionsSkeleton>
|
||||
<tbody>
|
||||
<tr *ngFor="let i of [1,2,3,4,5,6]">
|
||||
<td class="table-cell-txid"><div class="skeleton-loader skeleton-loader-transactions"></div> </td>
|
||||
<td class="table-cell-satoshis"><div class="skeleton-loader skeleton-loader-transactions"></div></td>
|
||||
<td class="table-cell-fiat"><div class="skeleton-loader skeleton-loader-transactions"></div></td>
|
||||
<td class="table-cell-fees"><div class="skeleton-loader skeleton-loader-transactions"></div></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</ng-template>
|
@ -0,0 +1,50 @@
|
||||
.latest-transactions {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
table-layout:fixed;
|
||||
tr, td, th {
|
||||
border: 0px;
|
||||
padding-top: 0.71rem !important;
|
||||
padding-bottom: 0.75rem !important;
|
||||
}
|
||||
td {
|
||||
overflow:hidden;
|
||||
width: 25%;
|
||||
}
|
||||
.table-cell-satoshis {
|
||||
display: none;
|
||||
text-align: right;
|
||||
@media (min-width: 576px) {
|
||||
display: table-cell;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
display: none;
|
||||
}
|
||||
@media (min-width: 1100px) {
|
||||
display: table-cell;
|
||||
}
|
||||
}
|
||||
.table-cell-fiat {
|
||||
display: none;
|
||||
text-align: right;
|
||||
@media (min-width: 485px) {
|
||||
display: table-cell;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
display: none;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
display: table-cell;
|
||||
}
|
||||
}
|
||||
.table-cell-date {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
.skeleton-loader-transactions {
|
||||
max-width: 250px;
|
||||
position: relative;
|
||||
top: 2px;
|
||||
margin-bottom: -3px;
|
||||
height: 18px;
|
||||
}
|
@ -0,0 +1,76 @@
|
||||
import { Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { Address, AddressTxSummary } from '../../interfaces/electrs.interface';
|
||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
||||
import { Observable, Subscription, catchError, map, of, switchMap, zip } from 'rxjs';
|
||||
import { PriceService } from '../../services/price.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-address-transactions-widget',
|
||||
templateUrl: './address-transactions-widget.component.html',
|
||||
styleUrls: ['./address-transactions-widget.component.scss'],
|
||||
})
|
||||
export class AddressTransactionsWidgetComponent implements OnInit, OnChanges, OnDestroy {
|
||||
@Input() address: string;
|
||||
@Input() addressInfo: Address;
|
||||
@Input() addressSummary$: Observable<AddressTxSummary[]> | null;
|
||||
@Input() isPubkey: boolean = false;
|
||||
|
||||
currencySubscription: Subscription;
|
||||
currency: string;
|
||||
|
||||
transactions$: Observable<any[]>;
|
||||
|
||||
isLoading: boolean = true;
|
||||
error: any;
|
||||
|
||||
constructor(
|
||||
public stateService: StateService,
|
||||
private electrsApiService: ElectrsApiService,
|
||||
private priceService: PriceService,
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.currencySubscription = this.stateService.fiatCurrency$.subscribe((fiat) => {
|
||||
this.currency = fiat;
|
||||
});
|
||||
this.startAddressSubscription();
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
this.startAddressSubscription();
|
||||
}
|
||||
|
||||
startAddressSubscription(): void {
|
||||
this.isLoading = true;
|
||||
if (!this.address || !this.addressInfo) {
|
||||
return;
|
||||
}
|
||||
this.transactions$ = (this.addressSummary$ || (this.isPubkey
|
||||
? this.electrsApiService.getScriptHashSummary$((this.address.length === 66 ? '21' : '41') + this.address + 'ac')
|
||||
: this.electrsApiService.getAddressSummary$(this.address)).pipe(
|
||||
catchError(e => {
|
||||
this.error = `Failed to fetch address history: ${e?.status || ''} ${e?.statusText || 'unknown error'}`;
|
||||
return of(null);
|
||||
})
|
||||
)).pipe(
|
||||
map(summary => {
|
||||
return summary?.slice(0, 6);
|
||||
}),
|
||||
switchMap(txs => {
|
||||
return (zip(txs.map(tx => this.priceService.getBlockPrice$(tx.time, true, this.currency).pipe(
|
||||
map(price => {
|
||||
return {
|
||||
...tx,
|
||||
price,
|
||||
};
|
||||
})
|
||||
))));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.currencySubscription.unsubscribe();
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
<ng-container *ngIf="!noFiat && (viewFiat$ | async) && (conversions$ | async) as conversions; else viewFiatVin">
|
||||
<ng-container *ngIf="!noFiat && (viewAmountMode$ | async) === 'fiat' && (conversions$ | async) as conversions; else viewFiatVin">
|
||||
<span class="fiat" *ngIf="blockConversion; else noblockconversion">
|
||||
{{ addPlus && satoshis >= 0 ? '+' : '' }}{{
|
||||
(
|
||||
@ -20,10 +20,28 @@
|
||||
<ng-template [ngIf]="(network === 'liquid' || network === 'liquidtestnet') && (satoshis === undefined || satoshis === null)" [ngIfElse]="default">
|
||||
<span i18n="shared.confidential">Confidential</span>
|
||||
</ng-template>
|
||||
<ng-template #default>‎{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ satoshis / 100000000 | number : digitsInfo }}
|
||||
<span class="symbol"><ng-template [ngIf]="network === 'liquid' && !forceBtc">L-</ng-template>
|
||||
<ng-template [ngIf]="network === 'liquidtestnet'">tL-</ng-template>
|
||||
<ng-template [ngIf]="network === 'testnet'">t</ng-template>
|
||||
<ng-template [ngIf]="network === 'signet'">s</ng-template>BTC</span>
|
||||
<ng-template #default>
|
||||
@if ((viewAmountMode$ | async) === 'btc' || (viewAmountMode$ | async) === 'fiat') {
|
||||
‎{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ satoshis / 100000000 | number : digitsInfo }}
|
||||
<span class="symbol">
|
||||
<ng-container *ngTemplateOutlet="prefix"></ng-container>BTC
|
||||
</span>
|
||||
} @else {
|
||||
@if (digitsInfo === '1.8-8') {
|
||||
‎{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ satoshis | number }}
|
||||
} @else {
|
||||
‎{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ satoshis | amountShortener : satoshis < 1000 && satoshis > -1000 ? 0 : 1 }}
|
||||
}
|
||||
<span class="symbol">
|
||||
<ng-container *ngTemplateOutlet="prefix"></ng-container>sats
|
||||
</span>
|
||||
}
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #prefix>
|
||||
<ng-template [ngIf]="network === 'liquid' && !forceBtc">L-</ng-template>
|
||||
<ng-template [ngIf]="network === 'liquidtestnet'">tL-</ng-template>
|
||||
<ng-template [ngIf]="network === 'testnet'">t</ng-template>
|
||||
<ng-template [ngIf]="network === 'signet'">s</ng-template>
|
||||
</ng-template>
|
||||
|
@ -12,7 +12,7 @@ import { Price } from '../../services/price.service';
|
||||
export class AmountComponent implements OnInit, OnDestroy {
|
||||
conversions$: Observable<any>;
|
||||
currency: string;
|
||||
viewFiat$: Observable<boolean>;
|
||||
viewAmountMode$: Observable<'btc' | 'sats' | 'fiat'>;
|
||||
network = '';
|
||||
|
||||
stateSubscription: Subscription;
|
||||
@ -37,7 +37,7 @@ export class AmountComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.viewFiat$ = this.stateService.viewFiat$.asObservable();
|
||||
this.viewAmountMode$ = this.stateService.viewAmountMode$.asObservable();
|
||||
this.conversions$ = this.stateService.conversions$.asObservable();
|
||||
this.stateSubscription = this.stateService.networkChanged$.subscribe((network) => this.network = network);
|
||||
}
|
||||
|
@ -0,0 +1,59 @@
|
||||
<div class="card">
|
||||
<div class="card-body more-padding">
|
||||
<div class="balance-container" *ngIf="!isLoading; else loading">
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="dashboard.btc-holdings">BTC Holdings</h5>
|
||||
<div class="card-text">
|
||||
{{ ((addressInfo.chain_stats.funded_txo_sum - addressInfo.chain_stats.spent_txo_sum) / 100_000_000) | number: '1.2-2' }} <span class="symbol" i18n="shared.btc|BTC">BTC</span>
|
||||
</div>
|
||||
<div class="symbol">
|
||||
<app-fiat [value]="(addressInfo.chain_stats.funded_txo_sum - addressInfo.chain_stats.spent_txo_sum)"></app-fiat>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="dashboard.7d-change">Change (7d)</h5>
|
||||
<div class="card-text">
|
||||
{{ delta7d > 0 ? '+' : ''}}{{ ((delta7d) / 100_000_000) | number: '1.2-2' }} <span class="symbol" i18n="shared.btc|BTC">BTC</span>
|
||||
</div>
|
||||
<div class="symbol">
|
||||
<app-fiat [value]="delta7d"></app-fiat>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="dashboard.30d-change">Change (30d)</h5>
|
||||
<div class="card-text">
|
||||
{{ delta30d > 0 ? '+' : ''}}{{ ((delta30d) / 100_000_000) | number: '1.2-2' }} <span class="symbol" i18n="shared.btc|BTC">BTC</span>
|
||||
</div>
|
||||
<div class="symbol">
|
||||
<app-fiat [value]="delta30d"></app-fiat>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-template #loading>
|
||||
<div class="balance-skeleton">
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="dashboard.btc-holdings">BTC Holdings</h5>
|
||||
<div class="card-text">
|
||||
<div class="skeleton-loader"></div>
|
||||
<div class="skeleton-loader"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="dashboard.7d-change">Change (7d)</h5>
|
||||
<div class="card-text">
|
||||
<div class="skeleton-loader"></div>
|
||||
<div class="skeleton-loader"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="dashboard.30d-change">Change (30d)</h5>
|
||||
<div class="card-text">
|
||||
<div class="skeleton-loader"></div>
|
||||
<div class="skeleton-loader"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
@ -0,0 +1,160 @@
|
||||
.balance-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
height: 76px;
|
||||
.shared-block {
|
||||
color: var(--transparent-fg);
|
||||
font-size: 12px;
|
||||
}
|
||||
.item {
|
||||
padding: 0 5px;
|
||||
width: 100%;
|
||||
max-width: 150px;
|
||||
&:last-child {
|
||||
display: none;
|
||||
@media (min-width: 485px) {
|
||||
display: table-cell;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
display: none;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
display: table-cell;
|
||||
}
|
||||
}
|
||||
}
|
||||
.card-text {
|
||||
font-size: 22px;
|
||||
margin-top: -9px;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.balance-skeleton {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@media (min-width: 376px) {
|
||||
flex-direction: row;
|
||||
}
|
||||
.item {
|
||||
min-width: 120px;
|
||||
max-width: 150px;
|
||||
margin: 0;
|
||||
width: -webkit-fill-available;
|
||||
@media (min-width: 376px) {
|
||||
margin: 0 auto 0px;
|
||||
}
|
||||
&:last-child{
|
||||
display: none;
|
||||
@media (min-width: 485px) {
|
||||
display: block;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
display: none;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
.card-text {
|
||||
.skeleton-loader {
|
||||
width: 100%;
|
||||
display: block;
|
||||
&:first-child {
|
||||
margin: 14px auto 0;
|
||||
max-width: 80px;
|
||||
}
|
||||
&:last-child {
|
||||
margin: 10px auto 0;
|
||||
max-width: 120px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: var(--bg);
|
||||
height: 126px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
color: var(--title-fg);
|
||||
font-size: 1rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.progress {
|
||||
display: inline-flex;
|
||||
width: 100%;
|
||||
background-color: var(--secondary);
|
||||
height: 1.1rem;
|
||||
max-width: 180px;
|
||||
}
|
||||
|
||||
.skeleton-loader {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.more-padding {
|
||||
padding: 24px 20px;
|
||||
}
|
||||
|
||||
.small-bar {
|
||||
height: 8px;
|
||||
top: -4px;
|
||||
max-width: 120px;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
min-height: 76px;
|
||||
}
|
||||
|
||||
.main-title {
|
||||
position: relative;
|
||||
color: #ffffff91;
|
||||
margin-top: -13px;
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
|
||||
.card-wrapper {
|
||||
.card {
|
||||
height: auto !important;
|
||||
}
|
||||
.card-body {
|
||||
display: flex;
|
||||
flex: inherit;
|
||||
text-align: center;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
padding: 24px 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.retarget-sign {
|
||||
margin-right: -3px;
|
||||
font-size: 14px;
|
||||
top: -2px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.previous-retarget-sign {
|
||||
margin-right: -2px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.symbol {
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { Address, AddressTxSummary } from '../../interfaces/electrs.interface';
|
||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
||||
import { Observable, catchError, of } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-balance-widget',
|
||||
templateUrl: './balance-widget.component.html',
|
||||
styleUrls: ['./balance-widget.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class BalanceWidgetComponent implements OnInit, OnChanges {
|
||||
@Input() address: string;
|
||||
@Input() addressInfo: Address;
|
||||
@Input() addressSummary$: Observable<AddressTxSummary[]> | null;
|
||||
@Input() isPubkey: boolean = false;
|
||||
|
||||
isLoading: boolean = true;
|
||||
error: any;
|
||||
|
||||
delta7d: number = 0;
|
||||
delta30d: number = 0;
|
||||
|
||||
constructor(
|
||||
public stateService: StateService,
|
||||
private electrsApiService: ElectrsApiService,
|
||||
private cd: ChangeDetectorRef,
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
this.isLoading = true;
|
||||
if (!this.address || !this.addressInfo) {
|
||||
return;
|
||||
}
|
||||
(this.addressSummary$ || (this.isPubkey
|
||||
? this.electrsApiService.getScriptHashSummary$((this.address.length === 66 ? '21' : '41') + this.address + 'ac')
|
||||
: this.electrsApiService.getAddressSummary$(this.address)).pipe(
|
||||
catchError(e => {
|
||||
this.error = `Failed to fetch address balance history: ${e?.status || ''} ${e?.statusText || 'unknown error'}`;
|
||||
return of(null);
|
||||
}),
|
||||
)).subscribe(addressSummary => {
|
||||
if (addressSummary) {
|
||||
this.error = null;
|
||||
this.calculateStats(addressSummary);
|
||||
}
|
||||
this.isLoading = false;
|
||||
this.cd.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
calculateStats(summary: AddressTxSummary[]): void {
|
||||
let weekTotal = 0;
|
||||
let monthTotal = 0;
|
||||
const weekAgo = (Date.now() / 1000) - (60 * 60 * 24 * 7);
|
||||
const monthAgo = (Date.now() / 1000) - (60 * 60 * 24 * 30);
|
||||
for (let i = 0; i < summary.length && summary[i].time >= monthAgo; i++) {
|
||||
monthTotal += summary[i].value;
|
||||
if (summary[i].time >= weekAgo) {
|
||||
weekTotal += summary[i].value;
|
||||
}
|
||||
}
|
||||
this.delta7d = weekTotal;
|
||||
this.delta30d = monthTotal;
|
||||
}
|
||||
}
|
@ -81,6 +81,20 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
||||
tooltipPosition: Position;
|
||||
|
||||
readyNextFrame = false;
|
||||
lastUpdate: number = 0;
|
||||
pendingUpdate: {
|
||||
count: number,
|
||||
add: { [txid: string]: TransactionStripped },
|
||||
remove: { [txid: string]: string },
|
||||
change: { [txid: string]: { txid: string, rate: number | undefined, acc: boolean | undefined } },
|
||||
direction?: string,
|
||||
} = {
|
||||
count: 0,
|
||||
add: {},
|
||||
remove: {},
|
||||
change: {},
|
||||
direction: 'left',
|
||||
};
|
||||
|
||||
searchText: string;
|
||||
searchSubscription: Subscription;
|
||||
@ -176,6 +190,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
||||
destroy(): void {
|
||||
if (this.scene) {
|
||||
this.scene.destroy();
|
||||
this.clearUpdateQueue();
|
||||
this.start();
|
||||
}
|
||||
}
|
||||
@ -188,6 +203,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
||||
}
|
||||
this.filtersAvailable = filtersAvailable;
|
||||
if (this.scene) {
|
||||
this.clearUpdateQueue();
|
||||
this.scene.setup(transactions);
|
||||
this.readyNextFrame = true;
|
||||
this.start();
|
||||
@ -197,6 +213,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
||||
|
||||
enter(transactions: TransactionStripped[], direction: string): void {
|
||||
if (this.scene) {
|
||||
this.clearUpdateQueue();
|
||||
this.scene.enter(transactions, direction);
|
||||
this.start();
|
||||
this.updateSearchHighlight();
|
||||
@ -205,6 +222,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
||||
|
||||
exit(direction: string): void {
|
||||
if (this.scene) {
|
||||
this.clearUpdateQueue();
|
||||
this.scene.exit(direction);
|
||||
this.start();
|
||||
this.updateSearchHighlight();
|
||||
@ -213,13 +231,61 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
||||
|
||||
replace(transactions: TransactionStripped[], direction: string, sort: boolean = true, startTime?: number): void {
|
||||
if (this.scene) {
|
||||
this.clearUpdateQueue();
|
||||
this.scene.replace(transactions || [], direction, sort, startTime);
|
||||
this.start();
|
||||
this.updateSearchHighlight();
|
||||
}
|
||||
}
|
||||
|
||||
// collates non-urgent updates into a set of consistent pending changes
|
||||
queueUpdate(add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: boolean | undefined }[], direction: string = 'left'): void {
|
||||
for (const tx of add) {
|
||||
this.pendingUpdate.add[tx.txid] = tx;
|
||||
delete this.pendingUpdate.remove[tx.txid];
|
||||
delete this.pendingUpdate.change[tx.txid];
|
||||
}
|
||||
for (const txid of remove) {
|
||||
delete this.pendingUpdate.add[txid];
|
||||
this.pendingUpdate.remove[txid] = txid;
|
||||
delete this.pendingUpdate.change[txid];
|
||||
}
|
||||
for (const tx of change) {
|
||||
if (this.pendingUpdate.add[tx.txid]) {
|
||||
this.pendingUpdate.add[tx.txid].rate = tx.rate;
|
||||
this.pendingUpdate.add[tx.txid].acc = tx.acc;
|
||||
} else {
|
||||
this.pendingUpdate.change[tx.txid] = tx;
|
||||
}
|
||||
}
|
||||
this.pendingUpdate.direction = direction;
|
||||
this.pendingUpdate.count++;
|
||||
}
|
||||
|
||||
applyQueuedUpdates(): void {
|
||||
if (this.pendingUpdate.count && performance.now() > (this.lastUpdate + this.animationDuration)) {
|
||||
this.update([], [], [], this.pendingUpdate?.direction);
|
||||
}
|
||||
}
|
||||
|
||||
clearUpdateQueue(): void {
|
||||
this.pendingUpdate = {
|
||||
count: 0,
|
||||
add: {},
|
||||
remove: {},
|
||||
change: {},
|
||||
};
|
||||
this.lastUpdate = performance.now();
|
||||
}
|
||||
|
||||
update(add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: boolean | undefined }[], direction: string = 'left', resetLayout: boolean = false): void {
|
||||
// merge any pending changes into this update
|
||||
this.queueUpdate(add, remove, change);
|
||||
this.applyUpdate(Object.values(this.pendingUpdate.add), Object.values(this.pendingUpdate.remove), Object.values(this.pendingUpdate.change), direction, resetLayout);
|
||||
this.clearUpdateQueue();
|
||||
}
|
||||
|
||||
applyUpdate(add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: boolean | undefined }[], direction: string = 'left', resetLayout: boolean = false): void {
|
||||
if (this.scene) {
|
||||
add = add.filter(tx => !this.scene.txs[tx.txid]);
|
||||
remove = remove.filter(txid => this.scene.txs[txid]);
|
||||
@ -230,6 +296,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
||||
}
|
||||
this.scene.update(add, remove, change, direction, resetLayout);
|
||||
this.start();
|
||||
this.lastUpdate = performance.now();
|
||||
this.updateSearchHighlight();
|
||||
}
|
||||
}
|
||||
@ -370,6 +437,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
||||
if (!now) {
|
||||
now = performance.now();
|
||||
}
|
||||
this.applyQueuedUpdates();
|
||||
// skip re-render if there's no change to the scene
|
||||
if (this.scene && this.gl) {
|
||||
/* SET UP SHADER UNIFORMS */
|
||||
|
@ -13,7 +13,7 @@ export default class BlockScene {
|
||||
theme: ThemeService;
|
||||
orientation: string;
|
||||
flip: boolean;
|
||||
animationDuration: number = 900;
|
||||
animationDuration: number = 1000;
|
||||
configAnimationOffset: number | null;
|
||||
animationOffset: number;
|
||||
highlightingEnabled: boolean;
|
||||
@ -179,7 +179,7 @@ export default class BlockScene {
|
||||
removed.forEach(tx => {
|
||||
tx.destroy();
|
||||
});
|
||||
}, 1000);
|
||||
}, (startTime - performance.now()) + this.animationDuration + 1000);
|
||||
|
||||
if (resetLayout) {
|
||||
add.forEach(tx => {
|
||||
@ -239,7 +239,7 @@ export default class BlockScene {
|
||||
{ width: number, height: number, resolution: number, blockLimit: number, animationDuration: number, animationOffset: number,
|
||||
orientation: string, flip: boolean, vertexArray: FastVertexArray, theme: ThemeService, highlighting: boolean, colorFunction: ((tx: TxView) => Color) | null }
|
||||
): void {
|
||||
this.animationDuration = animationDuration || 1000;
|
||||
this.animationDuration = animationDuration || this.animationDuration || 1000;
|
||||
this.configAnimationOffset = animationOffset;
|
||||
this.animationOffset = this.configAnimationOffset == null ? (this.width * 1.4) : this.configAnimationOffset;
|
||||
this.orientation = orientation;
|
||||
|
@ -12,6 +12,7 @@
|
||||
class="text-center bitcoin-block mined-block blockchain-blocks-offset-{{ offset }}-index-{{ i }}"
|
||||
[class.offscreen]="!static && count && i >= count"
|
||||
id="bitcoin-block-{{ block.height }}" [ngStyle]="blockStyles[i]"
|
||||
[style]="blockTransformation"
|
||||
[class.blink-bg]="isSpecial(block.height)">
|
||||
<a draggable="false" [routerLink]="[getHref(i, block) | relativeUrl]" [state]="{ data: { block: block } }"
|
||||
class="blockLink" [ngClass]="{'disabled': (this.stateService.blockScrolling$ | async)}"> </a>
|
||||
@ -40,7 +41,7 @@
|
||||
<app-fee-rate unitClass=""></app-fee-rate>
|
||||
</div>
|
||||
</ng-template>
|
||||
<div [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-total-fees'" *ngIf="showMiningInfo$ | async; else noMiningInfo"
|
||||
<div [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-total-fees'" *ngIf="blockDisplayMode === 'fees'; else noMiningInfo"
|
||||
class="block-size">
|
||||
<app-amount [satoshis]="block.extras?.totalFees ?? 0" digitsInfo="1.2-3" [noFiat]="true"></app-amount>
|
||||
</div>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, Input, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
|
||||
import { Observable, Subscription, delay, filter, tap } from 'rxjs';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { specialBlocks } from '../../app.constants';
|
||||
import { BlockExtended } from '../../interfaces/node-api.interface';
|
||||
@ -45,7 +45,10 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
markBlockSubscription: Subscription;
|
||||
txConfirmedSubscription: Subscription;
|
||||
loadingBlocks$: Observable<boolean>;
|
||||
showMiningInfo$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||
showMiningInfoSubscription: Subscription;
|
||||
blockDisplayModeSubscription: Subscription;
|
||||
blockDisplayMode: 'size' | 'fees';
|
||||
blockTransformation = {};
|
||||
blockStyles = [];
|
||||
emptyBlockStyles = [];
|
||||
interval: any;
|
||||
@ -78,22 +81,38 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
) {
|
||||
}
|
||||
|
||||
enabledMiningInfoIfNeeded(url) {
|
||||
const urlParts = url.split('/');
|
||||
const onDashboard = ['','testnet','signet','mining','acceleration'].includes(urlParts[urlParts.length - 1]);
|
||||
if (onDashboard) { // Only update showMiningInfo if we are on the main, mining or acceleration dashboards
|
||||
this.stateService.showMiningInfo$.next(url.includes('/mining') || url.includes('/acceleration'));
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.dynamicBlocksAmount = Math.min(8, this.stateService.env.KEEP_BLOCKS_AMOUNT);
|
||||
|
||||
if (['', 'testnet', 'signet'].includes(this.stateService.network)) {
|
||||
this.enabledMiningInfoIfNeeded(this.location.path());
|
||||
this.location.onUrlChange((url) => this.enabledMiningInfoIfNeeded(url));
|
||||
this.showMiningInfo$ = this.stateService.showMiningInfo$;
|
||||
}
|
||||
this.blockDisplayMode = this.stateService.blockDisplayMode$.value as 'size' | 'fees';
|
||||
this.blockDisplayModeSubscription = this.stateService.blockDisplayMode$
|
||||
.pipe(
|
||||
filter((mode: 'size' | 'fees') => mode !== this.blockDisplayMode),
|
||||
tap(() => {
|
||||
this.blockTransformation = this.timeLtr ? {
|
||||
transform: 'scaleX(-1) rotateX(90deg)',
|
||||
transition: 'transform 0.375s'
|
||||
} : {
|
||||
transform: 'rotateX(90deg)',
|
||||
transition: 'transform 0.375s'
|
||||
};
|
||||
}),
|
||||
delay(375),
|
||||
tap((mode) => {
|
||||
this.blockDisplayMode = mode;
|
||||
this.blockTransformation = this.timeLtr ? {
|
||||
transform: 'scaleX(-1)',
|
||||
transition: 'transform 0.375s'
|
||||
} : {
|
||||
transition: 'transform 0.375s'
|
||||
};
|
||||
this.cd.markForCheck();
|
||||
}),
|
||||
delay(375),
|
||||
)
|
||||
.subscribe(() => {
|
||||
this.blockTransformation = {};
|
||||
});
|
||||
|
||||
this.timeLtrSubscription = this.stateService.timeLtr.subscribe((ltr) => {
|
||||
this.timeLtr = !!ltr;
|
||||
@ -204,6 +223,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
this.networkSubscription.unsubscribe();
|
||||
this.tabHiddenSubscription.unsubscribe();
|
||||
this.markBlockSubscription.unsubscribe();
|
||||
this.blockDisplayModeSubscription.unsubscribe();
|
||||
this.timeLtrSubscription.unsubscribe();
|
||||
clearInterval(this.interval);
|
||||
}
|
||||
|
@ -10,6 +10,7 @@
|
||||
</ng-container>
|
||||
</div>
|
||||
<div id="divider" [hidden]="pageIndex > 0">
|
||||
<button class="block-display-toggle" (click)="toggleBlockDisplayMode()"><fa-icon [icon]="['fas', 'exchange-alt']" [fixedWidth]="true"></fa-icon></button>
|
||||
<button class="time-toggle" (click)="toggleTimeDirection()"><fa-icon [icon]="['fas', 'exchange-alt']" [fixedWidth]="true"></fa-icon></button>
|
||||
</div>
|
||||
</span>
|
||||
|
@ -67,9 +67,24 @@
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.block-display-toggle {
|
||||
color: white;
|
||||
font-size: 0.8rem;
|
||||
position: absolute;
|
||||
bottom: 15.8em;
|
||||
left: 1px;
|
||||
transform: translateX(-50%) rotate(90deg);
|
||||
background: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.blockchain-wrapper.ltr-transition .blocks-wrapper,
|
||||
.blockchain-wrapper.ltr-transition .position-container,
|
||||
.blockchain-wrapper.ltr-transition .time-toggle {
|
||||
.blockchain-wrapper.ltr-transition .time-toggle,
|
||||
.blockchain-wrapper.ltr-transition .block-display-toggle {
|
||||
transition: transform 1s;
|
||||
}
|
||||
|
||||
@ -81,6 +96,10 @@
|
||||
.time-toggle {
|
||||
transform: translateX(-50%) scaleX(-1);
|
||||
}
|
||||
|
||||
.block-display-toggle {
|
||||
transform: translateX(-50%) scaleX(-1) rotate(90deg);
|
||||
}
|
||||
}
|
||||
|
||||
:host-context(.ltr-layout) {
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, Input, Output, EventEmitter, ChangeDetectorRef, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { firstValueFrom, Subscription } from 'rxjs';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { StorageService } from '../../services/storage.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-blockchain',
|
||||
@ -26,15 +27,18 @@ export class BlockchainComponent implements OnInit, OnDestroy, OnChanges {
|
||||
connectionStateSubscription: Subscription;
|
||||
loadingTip: boolean = true;
|
||||
connected: boolean = true;
|
||||
blockDisplayMode: 'size' | 'fees';
|
||||
|
||||
dividerOffset: number | null = null;
|
||||
mempoolOffset: number | null = null;
|
||||
positionStyle = {
|
||||
transform: "translateX(1280px)",
|
||||
};
|
||||
blockDisplayToggleStyle = {};
|
||||
|
||||
constructor(
|
||||
public stateService: StateService,
|
||||
public StorageService: StorageService,
|
||||
private cd: ChangeDetectorRef,
|
||||
) {}
|
||||
|
||||
@ -51,6 +55,7 @@ export class BlockchainComponent implements OnInit, OnDestroy, OnChanges {
|
||||
firstValueFrom(this.stateService.chainTip$).then(() => {
|
||||
this.loadingTip = false;
|
||||
});
|
||||
this.blockDisplayMode = this.StorageService.getValue('block-display-mode-preference') as 'size' | 'fees' || 'fees';
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
@ -84,6 +89,13 @@ export class BlockchainComponent implements OnInit, OnDestroy, OnChanges {
|
||||
}, 0);
|
||||
}
|
||||
|
||||
toggleBlockDisplayMode(): void {
|
||||
if (this.blockDisplayMode === 'size') this.blockDisplayMode = 'fees';
|
||||
else this.blockDisplayMode = 'size';
|
||||
this.StorageService.setValue('block-display-mode-preference', this.blockDisplayMode);
|
||||
this.stateService.blockDisplayMode$.next(this.blockDisplayMode);
|
||||
}
|
||||
|
||||
onMempoolWidthChange(width): void {
|
||||
if (this.flipping) {
|
||||
return;
|
||||
|
@ -0,0 +1,270 @@
|
||||
|
||||
<div class="container-xl dashboard-container">
|
||||
<div class="row row-cols-1 row-cols-md-2" *ngIf="{ value: (mempoolInfoData$ | async) } as mempoolInfoData">
|
||||
@for (widget of widgets; track widget.component) {
|
||||
@switch (widget.component) {
|
||||
@case ('fees') {
|
||||
<div class="col card-wrapper">
|
||||
<div class="main-title" i18n="fees-box.transaction-fees">Transaction Fees</div>
|
||||
<div class="card">
|
||||
<div class="card-body less-padding">
|
||||
<app-fees-box class="d-block"></app-fees-box>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@case ('difficulty') {
|
||||
<div class="col">
|
||||
<app-difficulty></app-difficulty>
|
||||
</div>
|
||||
}
|
||||
@case ('goggles') {
|
||||
<div class="col">
|
||||
<div class="card graph-card">
|
||||
<div class="card-body pl-lg-3 pr-lg-3 pl-2 pr-2">
|
||||
<a class="title-link mb-0" style="margin-top: -2px" href="" [routerLink]="['/mempool-block/0' | relativeUrl]">
|
||||
<h5 class="card-title d-inline"><span>Mempool Goggles™</span> : {{ goggleCycle[goggleIndex].name }}</h5>
|
||||
<span> </span>
|
||||
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: var(--title-fg)"></fa-icon>
|
||||
</a>
|
||||
<div class="quick-filter">
|
||||
<div class="btn-group btn-group-toggle">
|
||||
<label class="btn btn-primary btn-xs" [class.active]="filter.index === goggleIndex" *ngFor="let filter of goggleCycle">
|
||||
<input type="radio" [value]="'3m'" fragment="3m" (click)="setFilter(filter.index)" [attr.data-cy]="'3m'"> {{ filter.name }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mempool-block-wrapper" *ngIf="webGlEnabled">
|
||||
<app-mempool-block-overview
|
||||
[index]="0"
|
||||
[resolution]="goggleResolution"
|
||||
[filterFlags]="goggleFlags"
|
||||
[filterMode]="goggleMode"
|
||||
[gradientMode]="gradientMode"
|
||||
></app-mempool-block-overview>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@case ('incoming') {
|
||||
<div class="col">
|
||||
<div class="card graph-card">
|
||||
<div class="card-body">
|
||||
<ng-container *ngTemplateOutlet="mempoolTable; context: { $implicit: mempoolInfoData }"></ng-container>
|
||||
<h5 class="card-title mt-3" i18n="dashboard.incoming-transactions">Incoming Transactions</h5>
|
||||
<div class="mempool-graph" *ngIf="(mempoolStats$ | async) as mempoolStats">
|
||||
<app-incoming-transactions-graph
|
||||
[height]="incomingGraphHeight"
|
||||
[left]="50"
|
||||
[right]="20"
|
||||
[data]="mempoolStats?.weightPerSecond"
|
||||
[windowPreferenceOverride]="'2h'"
|
||||
></app-incoming-transactions-graph>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ng-template #mempoolTable let-mempoolInfoData>
|
||||
<div class="mempool-info-data">
|
||||
<div class="item">
|
||||
<h5 *ngIf="!mempoolInfoData.value || mempoolInfoData.value.memPoolInfo.mempoolminfee === mempoolInfoData.value.memPoolInfo.minrelaytxfee || (stateService.env.BASE_MODULE === 'liquid' && mempoolInfoData.value.memPoolInfo.mempoolminfee === 0.000001) else purgingText" class="card-title" i18n="dashboard.minimum-fee|Minimum mempool fee">Minimum fee</h5>
|
||||
<ng-template #purgingText><h5 class="card-title" i18n="dashboard.purging|Purgin below fee">Purging</h5></ng-template>
|
||||
<p class="card-text" *ngIf="(isLoadingWebSocket$ | async) === false && mempoolInfoData.value; else loading">
|
||||
<ng-template [ngIf]="mempoolInfoData.value.memPoolInfo.mempoolminfee !== mempoolInfoData.value.memPoolInfo.minrelaytxfee">< </ng-template><app-fee-rate [fee]="mempoolInfoData.value.memPoolInfo.mempoolminfee * 100000"></app-fee-rate>
|
||||
</p>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="dashboard.unconfirmed|Unconfirmed count">Unconfirmed</h5>
|
||||
<p class="card-text" *ngIf="(isLoadingWebSocket$ | async) === false && mempoolInfoData.value; else loading">
|
||||
{{ mempoolInfoData.value.memPoolInfo.size | number }} <span i18n="dashboard.txs">TXs</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="item bar">
|
||||
<h5 class="card-title" i18n="dashboard.memory-usage|Memory usage">Memory Usage</h5>
|
||||
<div class="card-text" *ngIf="(isLoadingWebSocket$ | async) === false && mempoolInfoData.value; else loadingbig">
|
||||
<div class="progress">
|
||||
<div class="progress-bar {{ mempoolInfoData.value.mempoolSizeProgress }}" role="progressbar" [ngStyle]="{'width': (mempoolInfoData.value.memPoolInfo.usage / mempoolInfoData.value.memPoolInfo.maxmempool * 100) + '%' }"> </div>
|
||||
<div class="progress-text">‎<span [innerHTML]="mempoolInfoData.value.memPoolInfo.usage | bytes : 2 : 'B' : null : false : 3"></span> / <span [innerHTML]="mempoolInfoData.value.memPoolInfo.maxmempool | bytes"></span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
}
|
||||
@case ('replacements') {
|
||||
<div class="col" style="max-height: 410px">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<a class="title-link" href="" [routerLink]="['/rbf' | relativeUrl]">
|
||||
<h5 class="card-title d-inline" i18n="dashboard.recent-rbf-replacements">Recent Replacements</h5>
|
||||
<span> </span>
|
||||
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: var(--title-fg)"></fa-icon>
|
||||
</a>
|
||||
<table class="table lastest-replacements-table">
|
||||
<thead>
|
||||
<th class="table-cell-txid" i18n="dashboard.latest-transactions.txid">TXID</th>
|
||||
<th class="table-cell-old-fee" i18n="dashboard.previous-transaction-fee">Previous fee</th>
|
||||
<th class="table-cell-new-fee" i18n="dashboard.new-transaction-fee">New fee</th>
|
||||
<th class="table-cell-badges" i18n="transaction.status|Transaction Status">Status</th>
|
||||
</thead>
|
||||
<tbody *ngIf="replacements$ | async as replacements; else replacementsSkeleton">
|
||||
<tr *ngFor="let replacement of replacements">
|
||||
<td class="table-cell-txid">
|
||||
<a [routerLink]="['/tx' | relativeUrl, replacement.txid]">
|
||||
<app-truncate [text]="replacement.txid" [lastChars]="5"></app-truncate>
|
||||
</a>
|
||||
</td>
|
||||
<td class="table-cell-old-fee"><app-fee-rate [fee]="replacement.oldFee" [weight]="replacement.oldVsize * 4"></app-fee-rate></td>
|
||||
<td class="table-cell-new-fee"><app-fee-rate [fee]="replacement.newFee" [weight]="replacement.newVsize * 4"></app-fee-rate></td>
|
||||
<td class="table-cell-badges">
|
||||
<span *ngIf="replacement.mined" class="badge badge-success" i18n="transaction.rbf.mined">Mined</span>
|
||||
<span *ngIf="replacement.fullRbf" class="badge badge-info" i18n="transaction.full-rbf">Full RBF</span>
|
||||
<span *ngIf="!replacement.fullRbf" class="badge badge-success" i18n="tx-features.tag.rbf|RBF">RBF</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ng-template #replacementsSkeleton>
|
||||
<tbody>
|
||||
<tr *ngFor="let i of [1,2,3,4,5,6]">
|
||||
<td class="table-cell-txid"><div class="skeleton-loader skeleton-loader-transactions"></div></td>
|
||||
<td class="table-cell-old-fee"><div class="skeleton-loader skeleton-loader-transactions"></div></td>
|
||||
<td class="table-cell-new-fee"><div class="skeleton-loader skeleton-loader-transactions"></div></td>
|
||||
<td class="table-cell-badges"><div class="skeleton-loader skeleton-loader-transactions"></div></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</ng-template>
|
||||
}
|
||||
@case ('blocks') {
|
||||
<div class="col" style="max-height: 410px">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<a class="title-link" href="" [routerLink]="['/blocks' | relativeUrl]">
|
||||
<h5 class="card-title d-inline" i18n="dashboard.recent-blocks">Recent Blocks</h5>
|
||||
<span> </span>
|
||||
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: var(--title-fg)"></fa-icon>
|
||||
</a>
|
||||
<table class="table lastest-blocks-table">
|
||||
<thead>
|
||||
<th class="table-cell-height" i18n="dashboard.latest-blocks.height">Height</th>
|
||||
<th class="table-cell-mined" i18n="dashboard.latest-blocks.mined">Mined</th>
|
||||
<th class="table-cell-transaction-count" i18n="dashboard.latest-blocks.transaction-count">TXs</th>
|
||||
<th class="table-cell-size" i18n="dashboard.latest-blocks.size">Size</th>
|
||||
</thead>
|
||||
<tbody *ngIf="blocks$ | async as blocks; else blocksSkeleton">
|
||||
<tr *ngFor="let block of blocks; let i = index; trackBy: trackByBlock">
|
||||
<td class="table-cell-height" ><a [routerLink]="['/block' | relativeUrl, block.id]" [state]="{ data: { block: block } }">{{ block.height }}</a></td>
|
||||
<td class="table-cell-mined" ><app-time kind="since" [time]="block.timestamp" [fastRender]="true"></app-time></td>
|
||||
<td class="table-cell-transaction-count">{{ block.tx_count | number }}</td>
|
||||
<td class="table-cell-size">
|
||||
<div class="progress">
|
||||
<div class="progress-bar progress-mempool {{ network$ | async }}" role="progressbar" [ngStyle]="{'width': (block.weight / stateService.env.BLOCK_WEIGHT_UNITS)*100 + '%' }"> </div>
|
||||
<div class="progress-text" [innerHTML]="block.size | bytes: 2"></div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ng-template #blocksSkeleton>
|
||||
<tbody>
|
||||
<tr *ngFor="let i of [1,2,3,4,5,6]">
|
||||
<td class="table-cell-height"><div class="skeleton-loader skeleton-loader-transactions"></div> </td>
|
||||
<td class="table-cell-mined"><div class="skeleton-loader skeleton-loader-transactions"></div></td>
|
||||
<td class="table-cell-transaction-count"><div class="skeleton-loader skeleton-loader-transactions"></div></td>
|
||||
<td class="table-cell-size"><div class="skeleton-loader skeleton-loader-transactions"></div></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</ng-template>
|
||||
}
|
||||
@case ('transactions') {
|
||||
<div class="col" style="max-height: 410px">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title" i18n="dashboard.recent-transactions">Recent Transactions</h5>
|
||||
<table class="table latest-transactions">
|
||||
<thead>
|
||||
<th class="table-cell-txid" i18n="dashboard.latest-transactions.txid">TXID</th>
|
||||
<th class="table-cell-satoshis" i18n="dashboard.latest-transactions.amount">Amount</th>
|
||||
<th class="table-cell-fiat" *ngIf="(network$ | async) === ''">{{ currency }}</th>
|
||||
<th class="table-cell-fees" i18n="transaction.fee|Transaction fee">Fee</th>
|
||||
</thead>
|
||||
<tbody *ngIf="transactions$ | async as transactions else recentTransactionsSkeleton">
|
||||
<tr *ngFor="let transaction of transactions; let i = index;">
|
||||
<td class="table-cell-txid">
|
||||
<a [routerLink]="['/tx' | relativeUrl, transaction.txid]">
|
||||
<app-truncate [text]="transaction.txid" [lastChars]="5"></app-truncate>
|
||||
</a>
|
||||
</td>
|
||||
<td class="table-cell-satoshis"><app-amount *ngIf="(network$ | async) !== 'liquidtestnet'; else liquidAmount" [satoshis]="transaction.value" digitsInfo="1.2-4" [noFiat]="true"></app-amount><ng-template #liquidAmount i18n="shared.confidential">Confidential</ng-template></td>
|
||||
<td class="table-cell-fiat" *ngIf="(network$ | async) === ''" ><app-fiat [value]="transaction.value" digitsInfo="1.0-0"></app-fiat></td>
|
||||
<td class="table-cell-fees"><app-fee-rate [fee]="transaction.fee" [weight]="transaction.vsize * 4"></app-fee-rate></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class=""> </div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ng-template #recentTransactionsSkeleton>
|
||||
<tbody>
|
||||
<tr *ngFor="let i of [1,2,3,4,5,6]">
|
||||
<td class="table-cell-txid"><div class="skeleton-loader skeleton-loader-transactions"></div> </td>
|
||||
<td class="table-cell-satoshis"><div class="skeleton-loader skeleton-loader-transactions"></div></td>
|
||||
<td class="table-cell-fiat" *ngIf="(network$ | async) === ''"><div class="skeleton-loader skeleton-loader-transactions"></div></td>
|
||||
<td class="table-cell-fees"><div class="skeleton-loader skeleton-loader-transactions"></div></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</ng-template>
|
||||
}
|
||||
@case ('balance') {
|
||||
<div class="col card-wrapper">
|
||||
<div class="main-title" i18n="dashboard.treasury">Treasury</div>
|
||||
<app-balance-widget [address]="widget.props.address" [addressSummary$]="addressSummary$" [addressInfo]="address"></app-balance-widget>
|
||||
</div>
|
||||
}
|
||||
@case ('address') {
|
||||
<div class="col" style="max-height: 410px">
|
||||
<div class="card graph-card">
|
||||
<div class="card-body">
|
||||
<a class="title-link" href="" [routerLink]="[('/address/' + widget.props.address) | relativeUrl]">
|
||||
<h5 class="card-title d-inline" i18n="dashboard.balance-history">Balance History</h5>
|
||||
<span> </span>
|
||||
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: var(--title-fg)"></fa-icon>
|
||||
</a>
|
||||
<app-address-graph [address]="widget.props.address" [addressSummary$]="addressSummary$" [period]="widget.props.period || 'all'" [stats]="address?.chain_stats" [widget]="true" [height]="graphHeight"></app-address-graph>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@case ('addressTransactions') {
|
||||
<div class="col" style="max-height: 410px">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<a class="title-link" href="" [routerLink]="[('/address/' + widget.props.address) | relativeUrl]">
|
||||
<h5 class="card-title d-inline" i18n="dashboard.treasury-transactions">Treasury Transactions</h5>
|
||||
<span> </span>
|
||||
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: var(--title-fg)"></fa-icon>
|
||||
</a>
|
||||
<app-address-transactions-widget [address]="widget.props.address" [addressSummary$]="addressSummary$" [addressInfo]="address"></app-address-transactions-widget>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-template #loading>
|
||||
<div class="skeleton-loader"></div>
|
||||
</ng-template>
|
||||
<ng-template #loadingbig>
|
||||
<span class="skeleton-loader skeleton-loader-big" ></span>
|
||||
</ng-template>
|
@ -0,0 +1,490 @@
|
||||
.dashboard-container {
|
||||
text-align: center;
|
||||
margin-top: 0.5rem;
|
||||
.col {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: var(--bg);
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
color: var(--title-fg);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.info-block {
|
||||
float: left;
|
||||
width: 350px;
|
||||
line-height: 25px;
|
||||
}
|
||||
|
||||
.progress {
|
||||
display: inline-flex;
|
||||
width: 100%;
|
||||
background-color: var(--secondary);
|
||||
height: 1.1rem;
|
||||
max-width: 180px;
|
||||
}
|
||||
|
||||
.bg-warning {
|
||||
background-color: #b58800 !important;
|
||||
}
|
||||
|
||||
.skeleton-loader {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.more-padding {
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.graph-card {
|
||||
height: 100%;
|
||||
@media (min-width: 768px) {
|
||||
height: 415px;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
height: 510px;
|
||||
}
|
||||
}
|
||||
|
||||
.mempool-info-data {
|
||||
min-height: 56px;
|
||||
display: block;
|
||||
@media (min-width: 485px) {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
&.lbtc-pegs-stats {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
h5 {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.item {
|
||||
width: 50%;
|
||||
margin: 0px auto 20px;
|
||||
display: inline-block;
|
||||
@media (min-width: 485px) {
|
||||
margin: 0px auto 10px;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
margin: 0px auto 0px;
|
||||
}
|
||||
&:last-child {
|
||||
margin: 0px auto 0px;
|
||||
}
|
||||
&:nth-child(2) {
|
||||
order: 2;
|
||||
@media (min-width: 485px) {
|
||||
order: 3;
|
||||
}
|
||||
}
|
||||
&:nth-child(3) {
|
||||
order: 3;
|
||||
@media (min-width: 485px) {
|
||||
order: 2;
|
||||
display: block;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
display: none;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
.card-text {
|
||||
font-size: 18px;
|
||||
span {
|
||||
color: var(--transparent-fg);
|
||||
font-size: 12px;
|
||||
}
|
||||
.bitcoin-color {
|
||||
color: var(--orange);
|
||||
}
|
||||
}
|
||||
.progress {
|
||||
width: 90%;
|
||||
@media (min-width: 768px) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
.bar {
|
||||
width: 93%;
|
||||
margin: 0px 5px 20px;
|
||||
@media (min-width: 485px) {
|
||||
max-width: 200px;
|
||||
margin: 0px auto 0px;
|
||||
}
|
||||
}
|
||||
.skeleton-loader {
|
||||
width: 100%;
|
||||
max-width: 100px;
|
||||
display: block;
|
||||
margin: 18px auto 0;
|
||||
}
|
||||
.skeleton-loader-big {
|
||||
max-width: 180px;
|
||||
}
|
||||
}
|
||||
|
||||
.latest-transactions {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
table-layout:fixed;
|
||||
tr, td, th {
|
||||
border: 0px;
|
||||
padding-top: 0.71rem !important;
|
||||
padding-bottom: 0.75rem !important;
|
||||
}
|
||||
td {
|
||||
overflow:hidden;
|
||||
width: 25%;
|
||||
}
|
||||
.table-cell-satoshis {
|
||||
display: none;
|
||||
text-align: right;
|
||||
@media (min-width: 576px) {
|
||||
display: table-cell;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
display: none;
|
||||
}
|
||||
@media (min-width: 1100px) {
|
||||
display: table-cell;
|
||||
}
|
||||
}
|
||||
.table-cell-fiat {
|
||||
display: none;
|
||||
text-align: right;
|
||||
@media (min-width: 485px) {
|
||||
display: table-cell;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
display: none;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
display: table-cell;
|
||||
}
|
||||
}
|
||||
.table-cell-fees {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
.skeleton-loader-transactions {
|
||||
max-width: 250px;
|
||||
position: relative;
|
||||
top: 2px;
|
||||
margin-bottom: -3px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.lastest-blocks-table {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
tr, td, th {
|
||||
border: 0px;
|
||||
padding-top: 0.65rem !important;
|
||||
padding-bottom: 0.7rem !important;
|
||||
}
|
||||
.table-cell-height {
|
||||
width: 15%;
|
||||
}
|
||||
.table-cell-mined {
|
||||
width: 35%;
|
||||
text-align: left;
|
||||
}
|
||||
.table-cell-transaction-count {
|
||||
display: none;
|
||||
text-align: right;
|
||||
width: 20%;
|
||||
display: table-cell;
|
||||
}
|
||||
.table-cell-size {
|
||||
display: none;
|
||||
text-align: center;
|
||||
width: 30%;
|
||||
@media (min-width: 485px) {
|
||||
display: table-cell;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
display: none;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
display: table-cell;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lastest-replacements-table {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
table-layout:fixed;
|
||||
tr, td, th {
|
||||
border: 0px;
|
||||
padding-top: 0.71rem !important;
|
||||
padding-bottom: 0.75rem !important;
|
||||
}
|
||||
td {
|
||||
overflow:hidden;
|
||||
width: 25%;
|
||||
}
|
||||
.table-cell-txid {
|
||||
width: 25%;
|
||||
text-align: start;
|
||||
}
|
||||
.table-cell-old-fee {
|
||||
width: 25%;
|
||||
text-align: end;
|
||||
|
||||
@media(max-width: 1080px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.table-cell-new-fee {
|
||||
width: 20%;
|
||||
text-align: end;
|
||||
}
|
||||
.table-cell-badges {
|
||||
width: 23%;
|
||||
padding-right: 0;
|
||||
padding-left: 5px;
|
||||
text-align: end;
|
||||
|
||||
.badge {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mempool-graph {
|
||||
height: 255px;
|
||||
@media (min-width: 768px) {
|
||||
height: 285px;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
height: 370px;
|
||||
}
|
||||
}
|
||||
.loadingGraphs{
|
||||
height: 250px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.inc-tx-progress-bar {
|
||||
max-width: 250px;
|
||||
.progress-bar {
|
||||
padding: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.terms-of-service {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.small-bar {
|
||||
height: 8px;
|
||||
top: -4px;
|
||||
max-width: 120px;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
min-height: 76px;
|
||||
}
|
||||
|
||||
.main-title {
|
||||
position: relative;
|
||||
color: #ffffff91;
|
||||
margin-top: -13px;
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
|
||||
.card-wrapper {
|
||||
.card {
|
||||
height: auto !important;
|
||||
}
|
||||
.card-body {
|
||||
display: flex;
|
||||
flex: inherit;
|
||||
text-align: center;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
padding: 22px 20px;
|
||||
&.liquid {
|
||||
height: 124.5px;
|
||||
}
|
||||
}
|
||||
.less-padding {
|
||||
padding: 20px 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.retarget-sign {
|
||||
margin-right: -3px;
|
||||
font-size: 14px;
|
||||
top: -2px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.previous-retarget-sign {
|
||||
margin-right: -2px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.assetIcon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.asset-title {
|
||||
text-align: left;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.asset-icon {
|
||||
width: 65px;
|
||||
height: 65px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.circulating-amount {
|
||||
text-align: right;
|
||||
width: 100%;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.clear-link {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.pool-name {
|
||||
display: inline-block;
|
||||
vertical-align: text-top;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.title-link, .title-link:hover, .title-link:focus, .title-link:active {
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.mempool-block-wrapper {
|
||||
max-height: 410px;
|
||||
max-width: 410px;
|
||||
margin: auto;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
max-height: 344px;
|
||||
max-width: 344px;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
max-height: 410px;
|
||||
max-width: 410px;
|
||||
}
|
||||
}
|
||||
|
||||
.goggle-badge {
|
||||
margin: 6px 5px 8px;
|
||||
background: none;
|
||||
border: solid 2px var(--primary);
|
||||
cursor: pointer;
|
||||
|
||||
&.active {
|
||||
background: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
.btn-xs {
|
||||
padding: 0.35rem 0.5rem;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.quick-filter {
|
||||
margin-top: 5px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.card-liquid {
|
||||
background-color: var(--bg);
|
||||
height: 418px;
|
||||
@media (min-width: 992px) {
|
||||
height: 512px;
|
||||
}
|
||||
&.smaller {
|
||||
height: 408px;
|
||||
}
|
||||
}
|
||||
|
||||
.card-title-liquid {
|
||||
padding-top: 20px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.in-progress-message {
|
||||
position: relative;
|
||||
color: #ffffff91;
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
padding-bottom: 3px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
min-height: 56px;
|
||||
display: block;
|
||||
@media (min-width: 485px) {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
h5 {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.item {
|
||||
width: 50%;
|
||||
display: inline-block;
|
||||
margin: 0px auto 20px;
|
||||
&:nth-child(2) {
|
||||
order: 2;
|
||||
@media (min-width: 485px) {
|
||||
order: 3;
|
||||
}
|
||||
}
|
||||
&:nth-child(3) {
|
||||
order: 3;
|
||||
@media (min-width: 485px) {
|
||||
order: 2;
|
||||
display: block;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
display: none;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
.card-title {
|
||||
font-size: 1rem;
|
||||
color: var(--title-fg);
|
||||
}
|
||||
.card-text {
|
||||
font-size: 18px;
|
||||
span {
|
||||
color: var(--transparent-fg);
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,372 @@
|
||||
import { AfterViewInit, ChangeDetectionStrategy, Component, HostListener, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core';
|
||||
import { combineLatest, merge, Observable, of, Subject, Subscription } from 'rxjs';
|
||||
import { catchError, filter, map, scan, share, shareReplay, startWith, switchMap, tap } from 'rxjs/operators';
|
||||
import { BlockExtended, OptimizedMempoolStats, TransactionStripped } from '../../interfaces/node-api.interface';
|
||||
import { MempoolInfo, ReplacementInfo } from '../../interfaces/websocket.interface';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { WebsocketService } from '../../services/websocket.service';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { ActiveFilter, FilterMode, GradientMode, toFlags } from '../../shared/filters.utils';
|
||||
import { detectWebGL } from '../../shared/graphs.utils';
|
||||
import { Address, AddressTxSummary } from '../../interfaces/electrs.interface';
|
||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
||||
|
||||
interface MempoolBlocksData {
|
||||
blocks: number;
|
||||
size: number;
|
||||
}
|
||||
|
||||
interface MempoolInfoData {
|
||||
memPoolInfo: MempoolInfo;
|
||||
vBytesPerSecond: number;
|
||||
progressWidth: string;
|
||||
progressColor: string;
|
||||
}
|
||||
|
||||
interface MempoolStatsData {
|
||||
mempool: OptimizedMempoolStats[];
|
||||
weightPerSecond: any;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-custom-dashboard',
|
||||
templateUrl: './custom-dashboard.component.html',
|
||||
styleUrls: ['./custom-dashboard.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class CustomDashboardComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
network$: Observable<string>;
|
||||
mempoolBlocksData$: Observable<MempoolBlocksData>;
|
||||
mempoolInfoData$: Observable<MempoolInfoData>;
|
||||
mempoolLoadingStatus$: Observable<number>;
|
||||
vBytesPerSecondLimit = 1667;
|
||||
transactions$: Observable<TransactionStripped[]>;
|
||||
blocks$: Observable<BlockExtended[]>;
|
||||
replacements$: Observable<ReplacementInfo[]>;
|
||||
latestBlockHeight: number;
|
||||
mempoolTransactionsWeightPerSecondData: any;
|
||||
mempoolStats$: Observable<MempoolStatsData>;
|
||||
transactionsWeightPerSecondOptions: any;
|
||||
isLoadingWebSocket$: Observable<boolean>;
|
||||
isLoad: boolean = true;
|
||||
filterSubscription: Subscription;
|
||||
mempoolInfoSubscription: Subscription;
|
||||
currencySubscription: Subscription;
|
||||
currency: string;
|
||||
incomingGraphHeight: number = 300;
|
||||
graphHeight: number = 300;
|
||||
webGlEnabled = true;
|
||||
|
||||
widgets;
|
||||
|
||||
addressSubscription: Subscription;
|
||||
blockTxSubscription: Subscription;
|
||||
addressSummary$: Observable<AddressTxSummary[]>;
|
||||
address: Address;
|
||||
|
||||
goggleResolution = 82;
|
||||
goggleCycle: { index: number, name: string, mode: FilterMode, filters: string[], gradient: GradientMode }[] = [
|
||||
{ index: 0, name: $localize`:@@dfc3c34e182ea73c5d784ff7c8135f087992dac1:All`, mode: 'and', filters: [], gradient: 'age' },
|
||||
{ index: 1, name: $localize`Consolidation`, mode: 'and', filters: ['consolidation'], gradient: 'fee' },
|
||||
{ index: 2, name: $localize`Coinjoin`, mode: 'and', filters: ['coinjoin'], gradient: 'fee' },
|
||||
{ index: 3, name: $localize`Data`, mode: 'or', filters: ['inscription', 'fake_pubkey', 'op_return'], gradient: 'fee' },
|
||||
];
|
||||
goggleFlags = 0n;
|
||||
goggleMode: FilterMode = 'and';
|
||||
gradientMode: GradientMode = 'age';
|
||||
goggleIndex = 0;
|
||||
|
||||
private destroy$ = new Subject();
|
||||
|
||||
constructor(
|
||||
public stateService: StateService,
|
||||
private apiService: ApiService,
|
||||
private electrsApiService: ElectrsApiService,
|
||||
private websocketService: WebsocketService,
|
||||
private seoService: SeoService,
|
||||
@Inject(PLATFORM_ID) private platformId: Object,
|
||||
) {
|
||||
this.webGlEnabled = this.stateService.isBrowser && detectWebGL();
|
||||
this.widgets = this.stateService.env.customize?.dashboard.widgets || [];
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.stateService.focusSearchInputDesktop();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.filterSubscription.unsubscribe();
|
||||
this.mempoolInfoSubscription.unsubscribe();
|
||||
this.currencySubscription.unsubscribe();
|
||||
this.websocketService.stopTrackRbfSummary();
|
||||
if (this.addressSubscription) {
|
||||
this.addressSubscription.unsubscribe();
|
||||
this.websocketService.stopTrackingAddress();
|
||||
this.address = null;
|
||||
}
|
||||
this.destroy$.next(1);
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.onResize();
|
||||
this.isLoadingWebSocket$ = this.stateService.isLoadingWebSocket$;
|
||||
this.seoService.resetTitle();
|
||||
this.seoService.resetDescription();
|
||||
this.websocketService.want(['blocks', 'stats', 'mempool-blocks', 'live-2h-chart']);
|
||||
this.websocketService.startTrackRbfSummary();
|
||||
this.network$ = merge(of(''), this.stateService.networkChanged$);
|
||||
this.mempoolLoadingStatus$ = this.stateService.loadingIndicators$
|
||||
.pipe(
|
||||
map((indicators) => indicators.mempool !== undefined ? indicators.mempool : 100)
|
||||
);
|
||||
|
||||
this.filterSubscription = this.stateService.activeGoggles$.subscribe((active: ActiveFilter) => {
|
||||
const activeFilters = active.filters.sort().join(',');
|
||||
for (const goggle of this.goggleCycle) {
|
||||
if (goggle.mode === active.mode) {
|
||||
const goggleFilters = goggle.filters.sort().join(',');
|
||||
if (goggleFilters === activeFilters) {
|
||||
this.goggleIndex = goggle.index;
|
||||
this.goggleFlags = toFlags(goggle.filters);
|
||||
this.goggleMode = goggle.mode;
|
||||
this.gradientMode = active.gradient;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.goggleCycle.push({
|
||||
index: this.goggleCycle.length,
|
||||
name: 'Custom',
|
||||
mode: active.mode,
|
||||
filters: active.filters,
|
||||
gradient: active.gradient,
|
||||
});
|
||||
this.goggleIndex = this.goggleCycle.length - 1;
|
||||
this.goggleFlags = toFlags(active.filters);
|
||||
this.goggleMode = active.mode;
|
||||
});
|
||||
|
||||
this.mempoolInfoData$ = combineLatest([
|
||||
this.stateService.mempoolInfo$,
|
||||
this.stateService.vbytesPerSecond$
|
||||
]).pipe(
|
||||
map(([mempoolInfo, vbytesPerSecond]) => {
|
||||
const percent = Math.round((Math.min(vbytesPerSecond, this.vBytesPerSecondLimit) / this.vBytesPerSecondLimit) * 100);
|
||||
|
||||
let progressColor = 'bg-success';
|
||||
if (vbytesPerSecond > 1667) {
|
||||
progressColor = 'bg-warning';
|
||||
}
|
||||
if (vbytesPerSecond > 3000) {
|
||||
progressColor = 'bg-danger';
|
||||
}
|
||||
|
||||
const mempoolSizePercentage = (mempoolInfo.usage / mempoolInfo.maxmempool * 100);
|
||||
let mempoolSizeProgress = 'bg-danger';
|
||||
if (mempoolSizePercentage <= 50) {
|
||||
mempoolSizeProgress = 'bg-success';
|
||||
} else if (mempoolSizePercentage <= 75) {
|
||||
mempoolSizeProgress = 'bg-warning';
|
||||
}
|
||||
|
||||
return {
|
||||
memPoolInfo: mempoolInfo,
|
||||
vBytesPerSecond: vbytesPerSecond,
|
||||
progressWidth: percent + '%',
|
||||
progressColor: progressColor,
|
||||
mempoolSizeProgress: mempoolSizeProgress,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
this.mempoolInfoSubscription = this.mempoolInfoData$.subscribe();
|
||||
|
||||
this.mempoolBlocksData$ = this.stateService.mempoolBlocks$
|
||||
.pipe(
|
||||
map((mempoolBlocks) => {
|
||||
const size = mempoolBlocks.map((m) => m.blockSize).reduce((a, b) => a + b, 0);
|
||||
const vsize = mempoolBlocks.map((m) => m.blockVSize).reduce((a, b) => a + b, 0);
|
||||
|
||||
return {
|
||||
size: size,
|
||||
blocks: Math.ceil(vsize / this.stateService.blockVSize)
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
this.transactions$ = this.stateService.transactions$;
|
||||
|
||||
this.blocks$ = this.stateService.blocks$
|
||||
.pipe(
|
||||
tap((blocks) => {
|
||||
this.latestBlockHeight = blocks[0].height;
|
||||
}),
|
||||
switchMap((blocks) => {
|
||||
if (this.stateService.env.MINING_DASHBOARD === true) {
|
||||
for (const block of blocks) {
|
||||
// @ts-ignore: Need to add an extra field for the template
|
||||
block.extras.pool.logo = `/resources/mining-pools/` +
|
||||
block.extras.pool.slug + '.svg';
|
||||
}
|
||||
}
|
||||
return of(blocks.slice(0, 6));
|
||||
})
|
||||
);
|
||||
|
||||
this.replacements$ = this.stateService.rbfLatestSummary$;
|
||||
|
||||
this.mempoolStats$ = this.stateService.connectionState$
|
||||
.pipe(
|
||||
filter((state) => state === 2),
|
||||
switchMap(() => this.apiService.list2HStatistics$().pipe(
|
||||
catchError((e) => {
|
||||
return of(null);
|
||||
})
|
||||
)),
|
||||
switchMap((mempoolStats) => {
|
||||
return merge(
|
||||
this.stateService.live2Chart$
|
||||
.pipe(
|
||||
scan((acc, stats) => {
|
||||
acc.unshift(stats);
|
||||
acc = acc.slice(0, 120);
|
||||
return acc;
|
||||
}, (mempoolStats || []))
|
||||
),
|
||||
of(mempoolStats)
|
||||
);
|
||||
}),
|
||||
map((mempoolStats) => {
|
||||
if (mempoolStats) {
|
||||
return {
|
||||
mempool: mempoolStats,
|
||||
weightPerSecond: this.handleNewMempoolData(mempoolStats.concat([])),
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
shareReplay(1),
|
||||
);
|
||||
|
||||
this.currencySubscription = this.stateService.fiatCurrency$.subscribe((fiat) => {
|
||||
this.currency = fiat;
|
||||
});
|
||||
|
||||
this.startAddressSubscription();
|
||||
}
|
||||
|
||||
handleNewMempoolData(mempoolStats: OptimizedMempoolStats[]) {
|
||||
mempoolStats.reverse();
|
||||
const labels = mempoolStats.map(stats => stats.added);
|
||||
|
||||
return {
|
||||
labels: labels,
|
||||
series: [mempoolStats.map((stats) => [stats.added * 1000, stats.vbytes_per_second])],
|
||||
};
|
||||
}
|
||||
|
||||
trackByBlock(index: number, block: BlockExtended) {
|
||||
return block.height;
|
||||
}
|
||||
|
||||
getArrayFromNumber(num: number): number[] {
|
||||
return Array.from({ length: num }, (_, i) => i + 1);
|
||||
}
|
||||
|
||||
setFilter(index): void {
|
||||
const selected = this.goggleCycle[index];
|
||||
this.stateService.activeGoggles$.next(selected);
|
||||
}
|
||||
|
||||
startAddressSubscription(): void {
|
||||
if (this.stateService.env.customize && this.stateService.env.customize.dashboard.widgets.some(w => w.props?.address)) {
|
||||
const address = this.stateService.env.customize.dashboard.widgets.find(w => w.props?.address).props.address;
|
||||
const addressString = (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}|04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}$/.test(address)) ? address.toLowerCase() : address;
|
||||
|
||||
this.addressSubscription = (
|
||||
addressString.match(/04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}/)
|
||||
? this.electrsApiService.getPubKeyAddress$(addressString)
|
||||
: this.electrsApiService.getAddress$(addressString)
|
||||
).pipe(
|
||||
catchError((err) => {
|
||||
console.log(err);
|
||||
return of(null);
|
||||
}),
|
||||
filter((address) => !!address),
|
||||
).subscribe((address: Address) => {
|
||||
this.websocketService.startTrackAddress(address.address);
|
||||
this.address = address;
|
||||
});
|
||||
|
||||
this.addressSummary$ = (
|
||||
addressString.match(/04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}/)
|
||||
? this.electrsApiService.getScriptHashSummary$((addressString.length === 66 ? '21' : '41') + addressString + 'ac')
|
||||
: this.electrsApiService.getAddressSummary$(addressString)).pipe(
|
||||
catchError(e => {
|
||||
return of(null);
|
||||
}),
|
||||
switchMap(initial => this.stateService.blockTransactions$.pipe(
|
||||
startWith(null),
|
||||
scan((summary, tx) => {
|
||||
if (tx && !summary.some(t => t.txid === tx.txid)) {
|
||||
let value = 0;
|
||||
let funded = 0;
|
||||
let fundedCount = 0;
|
||||
let spent = 0;
|
||||
let spentCount = 0;
|
||||
for (const vout of tx.vout) {
|
||||
if (vout.scriptpubkey_address === addressString) {
|
||||
value += vout.value;
|
||||
funded += vout.value;
|
||||
fundedCount++;
|
||||
}
|
||||
}
|
||||
for (const vin of tx.vin) {
|
||||
if (vin.prevout?.scriptpubkey_address === addressString) {
|
||||
value -= vin.prevout?.value;
|
||||
spent += vin.prevout?.value;
|
||||
spentCount++;
|
||||
}
|
||||
}
|
||||
if (this.address && this.address.address === addressString) {
|
||||
this.address.chain_stats.tx_count++;
|
||||
this.address.chain_stats.funded_txo_sum += funded;
|
||||
this.address.chain_stats.funded_txo_count += fundedCount;
|
||||
this.address.chain_stats.spent_txo_sum += spent;
|
||||
this.address.chain_stats.spent_txo_count += spentCount;
|
||||
}
|
||||
summary.unshift({
|
||||
txid: tx.txid,
|
||||
time: tx.status?.block_time,
|
||||
height: tx.status?.block_height,
|
||||
value
|
||||
});
|
||||
}
|
||||
return summary;
|
||||
}, initial)
|
||||
)),
|
||||
share(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@HostListener('window:resize', ['$event'])
|
||||
onResize(): void {
|
||||
if (window.innerWidth >= 992) {
|
||||
this.incomingGraphHeight = 300;
|
||||
this.goggleResolution = 82;
|
||||
this.graphHeight = 400;
|
||||
} else if (window.innerWidth >= 768) {
|
||||
this.incomingGraphHeight = 215;
|
||||
this.goggleResolution = 80;
|
||||
this.graphHeight = 310;
|
||||
} else {
|
||||
this.incomingGraphHeight = 180;
|
||||
this.goggleResolution = 86;
|
||||
this.graphHeight = 310;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
<div [formGroup]="languageForm" class="text-small text-center">
|
||||
<select formControlName="language" class="custom-select custom-select-sm form-control-secondary form-control mx-auto" style="width: 95px;" (change)="changeLanguage()">
|
||||
<select formControlName="language" class="custom-select custom-select-sm form-control-secondary form-control mx-auto" style="width: 120px;" (change)="changeLanguage()">
|
||||
<option *ngFor="let lang of languages" [value]="lang.code">{{ lang.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
@ -122,8 +122,8 @@ export class FederationUtxosListComponent implements OnInit {
|
||||
|
||||
getGradientColor(value: number): string {
|
||||
const distanceToGreen = Math.abs(4032 - value);
|
||||
const green = 'var(--green)';
|
||||
const red = 'var(--red)';
|
||||
const green = '#3bcc49';
|
||||
const red = '#dc3545';
|
||||
|
||||
if (value < 0) {
|
||||
return red;
|
||||
|
@ -3,7 +3,7 @@
|
||||
<h5 class="card-title" i18n="liquid.unpeg">Unpeg</h5>
|
||||
<div *ngIf="(unbackedMonths$ | async) as unbackedMonths; else loadingData" class="card-text">
|
||||
<ng-container *ngIf="unbackedMonths.historyComplete; else loadingData">
|
||||
<div class="fee-text" [ngClass]="{'danger' : unbackedMonths.total > 0, 'correct': unbackedMonths.total === 0}">
|
||||
<div class="fee-text" [ngClass]="{'danger' : unbackedMonths.total > 0, 'correct': unbackedMonths.total === 0}" i18n-ngbTooltip="liquid.unpeg-info" ngbTooltip="Number of times that the Federation's BTC holdings fall below 95% of the total L-BTC supply">
|
||||
{{ unbackedMonths.total }} <span i18n="liquid.unpeg-event">Unpeg Event</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
@ -34,7 +34,7 @@ export class ReservesRatioStatsComponent implements OnInit {
|
||||
let avg = 0;
|
||||
for (let i = 0; i < ratioSeries.length; i++) {
|
||||
avg += ratioSeries[i];
|
||||
if (ratioSeries[i] < 1) {
|
||||
if (ratioSeries[i] < 0.95) {
|
||||
total++;
|
||||
}
|
||||
}
|
||||
|
@ -85,6 +85,7 @@ export class ReservesRatioComponent implements OnInit, OnChanges {
|
||||
{
|
||||
type: 'gauge',
|
||||
startAngle: 180,
|
||||
silent: true,
|
||||
endAngle: 0,
|
||||
center: ['50%', '75%'],
|
||||
radius: '100%',
|
||||
|
@ -2,8 +2,15 @@
|
||||
<div class="preview-wrapper">
|
||||
<header>
|
||||
<span class="header-brand" style="position: relative;">
|
||||
<app-svg-images *ngIf="!officialMempoolSpace" name="mempoolSpace" viewBox="0 0 500 126" width="500" height="126" class="mempool-logo" style="width: 200px; height: 50px"></app-svg-images>
|
||||
<app-svg-images *ngIf="officialMempoolSpace" name="officialMempoolSpace" style="width: 200px; height: 50px" width="500" height="126" viewBox="0 0 500 126"></app-svg-images>
|
||||
@if (enterpriseInfo?.img) {
|
||||
<img [src]="enterpriseInfo.img" class="subdomain_logo" [class]="{'rounded': enterpriseInfo.rounded_corner}">
|
||||
}
|
||||
@if (enterpriseInfo?.header_img) {
|
||||
<img *ngIf="enterpriseInfo.header_img" [src]="enterpriseInfo?.header_img" alt="enterpriseInfo.title" height="36px">
|
||||
} @else {
|
||||
<app-svg-images *ngIf="!officialMempoolSpace" name="mempoolSpace" viewBox="0 0 500 126" width="500" height="126" class="mempool-logo" style="width: 200px; height: 50px"></app-svg-images>
|
||||
<app-svg-images *ngIf="officialMempoolSpace" name="officialMempoolSpace" style="width: 200px; height: 50px" width="500" height="126" viewBox="0 0 500 126"></app-svg-images>
|
||||
}
|
||||
</span>
|
||||
|
||||
<div [ngSwitch]="network.val">
|
||||
|
@ -18,7 +18,7 @@
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: var(--active-bg);
|
||||
background: var(--stat-box-bg);
|
||||
text-align: start;
|
||||
font-size: 1.8em;
|
||||
}
|
||||
@ -77,3 +77,15 @@
|
||||
flex-shrink: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.subdomain_logo {
|
||||
height: 35px;
|
||||
overflow: clip;
|
||||
max-width: 140px;
|
||||
margin: auto;
|
||||
align-self: center;
|
||||
margin-right: 1em;
|
||||
.rounded {
|
||||
border-radius: 5px;
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { Observable, merge, of } from 'rxjs';
|
||||
import { Observable, Subscription, merge, of } from 'rxjs';
|
||||
import { LanguageService } from '../../services/language.service';
|
||||
import { EnterpriseService } from '../../services/enterprise.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-master-page-preview',
|
||||
@ -13,15 +14,23 @@ export class MasterPagePreviewComponent implements OnInit {
|
||||
lightning$: Observable<boolean>;
|
||||
officialMempoolSpace = this.stateService.env.OFFICIAL_MEMPOOL_SPACE;
|
||||
urlLanguage: string;
|
||||
subdomain = '';
|
||||
enterpriseInfo: any;
|
||||
enterpriseInfo$: Subscription;
|
||||
|
||||
constructor(
|
||||
public stateService: StateService,
|
||||
private languageService: LanguageService,
|
||||
private enterpriseService: EnterpriseService,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.network$ = merge(of(''), this.stateService.networkChanged$);
|
||||
this.lightning$ = this.stateService.lightningChanged$;
|
||||
this.urlLanguage = this.languageService.getLanguageForUrl();
|
||||
this.subdomain = this.enterpriseService.getSubdomain();
|
||||
this.enterpriseInfo$ = this.enterpriseService.info$.subscribe(info => {
|
||||
this.enterpriseInfo = info;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -19,13 +19,17 @@
|
||||
<a class="navbar-brand d-none d-md-flex" [ngClass]="{'dual-logos': subdomain}" [routerLink]="['/' | relativeUrl]" (click)="brandClick($event)">
|
||||
<ng-template [ngIf]="subdomain && enterpriseInfo">
|
||||
<div class="subdomain_container">
|
||||
<img [src]="'/api/v1/services/enterprise/images/' + subdomain + '/logo?imageMd5=' + enterpriseInfo.imageMd5" class="subdomain_logo" [class]="{'rounded': enterpriseInfo.rounded_corner}">
|
||||
<img [src]="enterpriseInfo.img || '/api/v1/services/enterprise/images/' + subdomain + '/logo?imageMd5=' + enterpriseInfo.imageMd5" class="subdomain_logo" [class]="{'rounded': enterpriseInfo.rounded_corner}">
|
||||
</div>
|
||||
<div class="vertical-line"></div>
|
||||
</ng-template>
|
||||
<ng-container *ngIf="{ val: connectionState$ | async } as connectionState">
|
||||
<app-svg-images *ngIf="!officialMempoolSpace" name="mempoolSpace" viewBox="0 0 500 126" class="mempool-logo" [ngStyle]="{'opacity': connectionState.val === 2 ? 1 : 0.5 }"></app-svg-images>
|
||||
<app-svg-images *ngIf="officialMempoolSpace" name="officialMempoolSpace" viewBox="0 0 500 126"></app-svg-images>
|
||||
@if (enterpriseInfo?.header_img) {
|
||||
<img [src]="enterpriseInfo?.header_img" alt="enterpriseInfo.title" height="36px">
|
||||
} @else {
|
||||
<app-svg-images *ngIf="!officialMempoolSpace" name="mempoolSpace" viewBox="0 0 500 126" class="mempool-logo" [ngStyle]="{'opacity': connectionState.val === 2 ? 1 : 0.5 }"></app-svg-images>
|
||||
<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>
|
||||
@ -36,14 +40,18 @@
|
||||
<a class="navbar-brand d-flex d-md-none justify-content-center" [ngClass]="{'dual-logos': subdomain, 'mr-0': subdomain}" [routerLink]="['/' | relativeUrl]" (click)="brandClick($event)">
|
||||
<ng-template [ngIf]="subdomain && enterpriseInfo">
|
||||
<div class="subdomain_container">
|
||||
<img [src]="'/api/v1/services/enterprise/images/' + subdomain + '/logo?imageMd5=' + enterpriseInfo.imageMd5" class="subdomain_logo" [class]="{'rounded': enterpriseInfo.rounded_corner}">
|
||||
<img [src]="enterpriseInfo.img || '/api/v1/services/enterprise/images/' + subdomain + '/logo?imageMd5=' + enterpriseInfo.imageMd5" class="subdomain_logo" [class]="{'rounded': enterpriseInfo.rounded_corner}">
|
||||
</div>
|
||||
<div class="vertical-line"></div>
|
||||
</ng-template>
|
||||
<ng-container *ngIf="{ val: connectionState$ | async } as connectionState">
|
||||
<app-svg-images *ngIf="!officialMempoolSpace" name="mempoolSpace" viewBox="0 0 500 126" class="mempool-logo" [ngStyle]="{'opacity': connectionState.val === 2 ? 1 : 0.5 }"></app-svg-images>
|
||||
<app-svg-images *ngIf="officialMempoolSpace" name="officialMempoolSpace" viewBox="0 0 500 126"></app-svg-images>
|
||||
<div class="connection-badge">
|
||||
@if (enterpriseInfo?.header_img) {
|
||||
<img [src]="enterpriseInfo?.header_img" alt="enterpriseInfo.title" height="36px">
|
||||
} @else {
|
||||
<app-svg-images *ngIf="!officialMempoolSpace" name="mempoolSpace" viewBox="0 0 500 126" class="mempool-logo" [ngStyle]="{'opacity': connectionState.val === 2 ? 1 : 0.5 }"></app-svg-images>
|
||||
<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>
|
||||
</div>
|
||||
|
@ -1,11 +1,10 @@
|
||||
import { Component, ComponentRef, ViewChild, HostListener, Input, Output, EventEmitter,
|
||||
import { Component, ViewChild, Input, Output, EventEmitter,
|
||||
OnInit, OnDestroy, OnChanges, ChangeDetectionStrategy, ChangeDetectorRef, AfterViewInit } from '@angular/core';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { MempoolBlockDelta } from '../../interfaces/websocket.interface';
|
||||
import { MempoolBlockDelta, isMempoolDelta } from '../../interfaces/websocket.interface';
|
||||
import { TransactionStripped } from '../../interfaces/node-api.interface';
|
||||
import { BlockOverviewGraphComponent } from '../../components/block-overview-graph/block-overview-graph.component';
|
||||
import { Subscription, BehaviorSubject, merge, of, timer } from 'rxjs';
|
||||
import { switchMap, filter, concatMap, map } from 'rxjs/operators';
|
||||
import { Subscription, BehaviorSubject } from 'rxjs';
|
||||
import { WebsocketService } from '../../services/websocket.service';
|
||||
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
||||
import { Router } from '@angular/router';
|
||||
@ -39,10 +38,6 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
|
||||
poolDirection: string = 'left';
|
||||
|
||||
blockSub: Subscription;
|
||||
rateLimit = 1000;
|
||||
private lastEventTime = Date.now() - this.rateLimit;
|
||||
private subId = 0;
|
||||
|
||||
firstLoad: boolean = true;
|
||||
|
||||
constructor(
|
||||
@ -62,39 +57,13 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.blockSub = merge(
|
||||
this.stateService.mempoolBlockTransactions$,
|
||||
this.stateService.mempoolBlockDelta$,
|
||||
).pipe(
|
||||
concatMap(update => {
|
||||
const now = Date.now();
|
||||
const timeSinceLastEvent = now - this.lastEventTime;
|
||||
this.lastEventTime = Math.max(now, this.lastEventTime + this.rateLimit);
|
||||
|
||||
const subId = this.subId;
|
||||
|
||||
// If time since last event is less than X seconds, delay this event
|
||||
if (timeSinceLastEvent < this.rateLimit) {
|
||||
return timer(this.rateLimit - timeSinceLastEvent).pipe(
|
||||
// Emit the event after the timer
|
||||
map(() => ({ update, subId }))
|
||||
);
|
||||
} else {
|
||||
// If enough time has passed, emit the event immediately
|
||||
return of({ update, subId });
|
||||
}
|
||||
})
|
||||
).subscribe(({ update, subId }) => {
|
||||
// discard stale updates after a block transition
|
||||
if (subId !== this.subId) {
|
||||
return;
|
||||
}
|
||||
this.blockSub = this.stateService.mempoolBlockUpdate$.subscribe((update) => {
|
||||
// process update
|
||||
if (update['added']) {
|
||||
if (isMempoolDelta(update)) {
|
||||
// delta
|
||||
this.updateBlock(update as MempoolBlockDelta);
|
||||
this.updateBlock(update);
|
||||
} else {
|
||||
const transactionsStripped = update as TransactionStripped[];
|
||||
const transactionsStripped = update.transactions;
|
||||
// new transactions
|
||||
if (this.firstLoad) {
|
||||
this.replaceBlock(transactionsStripped);
|
||||
@ -137,7 +106,6 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
|
||||
|
||||
ngOnChanges(changes): void {
|
||||
if (changes.index) {
|
||||
this.subId++;
|
||||
this.firstLoad = true;
|
||||
if (this.blockGraph) {
|
||||
this.blockGraph.clear(changes.index.currentValue > changes.index.previousValue ? this.chainDirection : this.poolDirection);
|
||||
@ -173,7 +141,11 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
|
||||
const direction = (this.blockIndex == null || this.index < this.blockIndex) ? this.poolDirection : this.chainDirection;
|
||||
this.blockGraph.replace(delta.added, direction);
|
||||
} else {
|
||||
this.blockGraph.update(delta.added, delta.removed, delta.changed || [], blockMined ? this.chainDirection : this.poolDirection, blockMined);
|
||||
if (blockMined) {
|
||||
this.blockGraph.update(delta.added, delta.removed, delta.changed || [], blockMined ? this.chainDirection : this.poolDirection, blockMined);
|
||||
} else {
|
||||
this.blockGraph.queueUpdate(delta.added, delta.removed, delta.changed || [], this.poolDirection);
|
||||
}
|
||||
}
|
||||
|
||||
this.lastBlockHeight = this.stateService.latestBlockHeight;
|
||||
|
@ -7,7 +7,7 @@
|
||||
class="spotlight-bottom"
|
||||
[style.right]="mempoolBlockStyles[i].right"
|
||||
></div>
|
||||
<div @blockEntryTrigger [@.disabled]="i > 0 || !animateEntry" [attr.data-cy]="'mempool-block-' + i" class="bitcoin-block text-center mempool-block" [class.hide-block]="count && i >= count" id="mempool-block-{{ i }}" [ngStyle]="mempoolBlockStyles[i]" [class.blink-bg]="projectedBlock.blink">
|
||||
<div @blockEntryTrigger [@.disabled]="i > 0 || !animateEntry" [attr.data-cy]="'mempool-block-' + i" class="bitcoin-block text-center mempool-block" [class.hide-block]="count && i >= count" id="mempool-block-{{ i }}" [ngStyle]="mempoolBlockStyles[i]" [class.blink-bg]="projectedBlock.blink" [style]="blockTransformation">
|
||||
<a draggable="false" [routerLink]="[getHref(i) | relativeUrl]"
|
||||
class="blockLink" [ngClass]="{'disabled': (this.stateService.blockScrolling$ | async)}"> </a>
|
||||
<div class="block-body">
|
||||
@ -20,7 +20,7 @@
|
||||
-
|
||||
<app-fee-rate [fee]="projectedBlock.feeRange[projectedBlock.feeRange.length - 1]" rounding="1.0-0" unitClass=""></app-fee-rate>
|
||||
</div>
|
||||
<div *ngIf="showMiningInfo$ | async; else noMiningInfo" class="block-size">
|
||||
<div *ngIf="blockDisplayMode === 'fees'; else noMiningInfo" class="block-size">
|
||||
<app-amount [attr.data-cy]="'mempool-block-' + i + '-total-fees'" [satoshis]="projectedBlock.totalFees" digitsInfo="1.2-3" [noFiat]="true"></app-amount>
|
||||
</div>
|
||||
<ng-template #noMiningInfo>
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, HostListener, Input, OnChanges, SimpleChanges, Output, EventEmitter } from '@angular/core';
|
||||
import { Subscription, Observable, of, combineLatest, BehaviorSubject } from 'rxjs';
|
||||
import { Subscription, Observable, of, combineLatest } from 'rxjs';
|
||||
import { MempoolBlock } from '../../interfaces/websocket.interface';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { Router } from '@angular/router';
|
||||
import { map, switchMap, tap } from 'rxjs/operators';
|
||||
import { delay, filter, map, switchMap, tap } from 'rxjs/operators';
|
||||
import { feeLevels } from '../../app.constants';
|
||||
import { specialBlocks } from '../../app.constants';
|
||||
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
||||
@ -43,7 +43,10 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
mempoolBlocks$: Observable<MempoolBlock[]>;
|
||||
difficultyAdjustments$: Observable<DifficultyAdjustment>;
|
||||
loadingBlocks$: Observable<boolean>;
|
||||
showMiningInfo$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||
showMiningInfoSubscription: Subscription;
|
||||
blockDisplayModeSubscription: Subscription;
|
||||
blockDisplayMode: 'size' | 'fees';
|
||||
blockTransformation = {};
|
||||
blocksSubscription: Subscription;
|
||||
|
||||
mempoolBlocksFull: MempoolBlock[] = [];
|
||||
@ -99,9 +102,29 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
this.mempoolWidth = width;
|
||||
this.widthChange.emit(this.mempoolWidth);
|
||||
|
||||
if (['', 'testnet', 'signet'].includes(this.stateService.network)) {
|
||||
this.showMiningInfo$ = this.stateService.showMiningInfo$;
|
||||
}
|
||||
this.blockDisplayMode = this.stateService.blockDisplayMode$.value as 'size' | 'fees';
|
||||
this.blockDisplayModeSubscription = this.stateService.blockDisplayMode$
|
||||
.pipe(
|
||||
filter((mode: 'size' | 'fees') => mode !== this.blockDisplayMode),
|
||||
tap(() => {
|
||||
this.blockTransformation = {
|
||||
transform: 'rotateX(90deg)',
|
||||
transition: 'transform 0.375s'
|
||||
};
|
||||
}),
|
||||
delay(375),
|
||||
tap((mode) => {
|
||||
this.blockDisplayMode = mode;
|
||||
this.blockTransformation = {
|
||||
transition: 'transform 0.375s'
|
||||
};
|
||||
this.cd.markForCheck();
|
||||
}),
|
||||
delay(375),
|
||||
)
|
||||
.subscribe(() => {
|
||||
this.blockTransformation = {};
|
||||
});
|
||||
|
||||
this.timeLtrSubscription = this.stateService.timeLtr.subscribe((ltr) => {
|
||||
this.timeLtr = !this.forceRtl && !!ltr;
|
||||
@ -262,6 +285,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
this.markBlocksSubscription.unsubscribe();
|
||||
this.blockSubscription.unsubscribe();
|
||||
this.networkSubscription.unsubscribe();
|
||||
this.blockDisplayModeSubscription.unsubscribe();
|
||||
this.timeLtrSubscription.unsubscribe();
|
||||
this.chainTipSubscription.unsubscribe();
|
||||
this.keySubscription.unsubscribe();
|
||||
|
@ -51,7 +51,7 @@ export class RbfTimelineComponent implements OnInit, OnChanges {
|
||||
|
||||
ngOnChanges(changes): void {
|
||||
this.rows = this.buildTimelines(this.replacements);
|
||||
if (changes.txid) {
|
||||
if (changes.txid && !changes.txid.firstChange && changes.txid.previousValue !== changes.txid.currentValue) {
|
||||
setTimeout(() => { this.scrollToSelected(); });
|
||||
}
|
||||
}
|
||||
|
@ -81,6 +81,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
mempoolBlocksSubscription: Subscription;
|
||||
blocksSubscription: Subscription;
|
||||
miningSubscription: Subscription;
|
||||
auditSubscription: Subscription;
|
||||
currencyChangeSubscription: Subscription;
|
||||
fragmentParams: URLSearchParams;
|
||||
rbfTransaction: undefined | Transaction;
|
||||
@ -308,51 +309,57 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
filter((target) => target.txid === this.txId),
|
||||
tap(() => {
|
||||
this.pool = null;
|
||||
this.auditStatus = null;
|
||||
}),
|
||||
switchMap(({ hash, height, txid }) => {
|
||||
switchMap(({ hash, height }) => {
|
||||
const foundBlock = this.cacheService.getCachedBlock(height) || null;
|
||||
const auditAvailable = this.isAuditAvailable(height);
|
||||
const isCoinbase = this.tx.vin.some(v => v.is_coinbase);
|
||||
const fetchAudit = auditAvailable && !isCoinbase;
|
||||
return combineLatest([
|
||||
foundBlock ? of(foundBlock.extras.pool) : this.apiService.getBlock$(hash).pipe(
|
||||
map(block => {
|
||||
return block.extras.pool;
|
||||
}),
|
||||
retry({ count: 3, delay: 2000 }),
|
||||
catchError(() => {
|
||||
return of(null);
|
||||
})
|
||||
),
|
||||
fetchAudit ? this.apiService.getBlockAudit$(hash).pipe(
|
||||
map(audit => {
|
||||
const isAdded = audit.addedTxs.includes(txid);
|
||||
const isPrioritized = audit.prioritizedTxs.includes(txid);
|
||||
const isAccelerated = audit.acceleratedTxs.includes(txid);
|
||||
const isConflict = audit.fullrbfTxs.includes(txid);
|
||||
const isExpected = audit.template.some(tx => tx.txid === txid);
|
||||
return {
|
||||
seen: isExpected || isPrioritized || isAccelerated,
|
||||
expected: isExpected,
|
||||
added: isAdded,
|
||||
prioritized: isPrioritized,
|
||||
conflict: isConflict,
|
||||
accelerated: isAccelerated,
|
||||
};
|
||||
}),
|
||||
retry({ count: 3, delay: 2000 }),
|
||||
catchError(() => {
|
||||
return of(null);
|
||||
})
|
||||
) : of(isCoinbase ? { coinbase: true } : null)
|
||||
]);
|
||||
return foundBlock ? of(foundBlock.extras.pool) : this.apiService.getBlock$(hash).pipe(
|
||||
map(block => block.extras.pool),
|
||||
retry({ count: 3, delay: 2000 }),
|
||||
catchError(() => of(null))
|
||||
);
|
||||
}),
|
||||
catchError((e) => {
|
||||
return of(null);
|
||||
})
|
||||
).subscribe(([pool, auditStatus]) => {
|
||||
).subscribe(pool => {
|
||||
this.pool = pool;
|
||||
});
|
||||
|
||||
this.auditSubscription = this.fetchMiningInfo$.pipe(
|
||||
filter((target) => target.txid === this.txId),
|
||||
tap(() => {
|
||||
this.auditStatus = null;
|
||||
}),
|
||||
switchMap(({ hash, height, txid }) => {
|
||||
const auditAvailable = this.isAuditAvailable(height);
|
||||
const isCoinbase = this.tx.vin.some(v => v.is_coinbase);
|
||||
const fetchAudit = auditAvailable && !isCoinbase;
|
||||
return fetchAudit ? this.apiService.getBlockAudit$(hash).pipe(
|
||||
map(audit => {
|
||||
const isAdded = audit.addedTxs.includes(txid);
|
||||
const isPrioritized = audit.prioritizedTxs.includes(txid);
|
||||
const isAccelerated = audit.acceleratedTxs.includes(txid);
|
||||
const isConflict = audit.fullrbfTxs.includes(txid);
|
||||
const isExpected = audit.template.some(tx => tx.txid === txid);
|
||||
return {
|
||||
seen: isExpected || isPrioritized || isAccelerated,
|
||||
expected: isExpected,
|
||||
added: isAdded,
|
||||
prioritized: isPrioritized,
|
||||
conflict: isConflict,
|
||||
accelerated: isAccelerated,
|
||||
};
|
||||
}),
|
||||
retry({ count: 3, delay: 2000 }),
|
||||
catchError(() => {
|
||||
return of(null);
|
||||
})
|
||||
) : of(isCoinbase ? { coinbase: true } : null);
|
||||
}),
|
||||
catchError((e) => {
|
||||
return of(null);
|
||||
})
|
||||
).subscribe(auditStatus => {
|
||||
this.auditStatus = auditStatus;
|
||||
|
||||
this.setIsAccelerated();
|
||||
@ -858,6 +865,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.mempoolBlocksSubscription.unsubscribe();
|
||||
this.blocksSubscription.unsubscribe();
|
||||
this.miningSubscription?.unsubscribe();
|
||||
this.auditSubscription?.unsubscribe();
|
||||
this.currencyChangeSubscription?.unsubscribe();
|
||||
this.leaveTransaction();
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import { filter, map, tap, switchMap, shareReplay, catchError } from 'rxjs/opera
|
||||
import { BlockExtended } from '../../interfaces/node-api.interface';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { PriceService } from '../../services/price.service';
|
||||
import { StorageService } from '../../services/storage.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-transactions-list',
|
||||
@ -56,6 +57,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
||||
private assetsService: AssetsService,
|
||||
private ref: ChangeDetectorRef,
|
||||
private priceService: PriceService,
|
||||
private storageService: StorageService,
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
@ -271,8 +273,11 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
||||
if (this.network === 'liquid' || this.network === 'liquidtestnet') {
|
||||
return;
|
||||
}
|
||||
const oldvalue = !this.stateService.viewFiat$.value;
|
||||
this.stateService.viewFiat$.next(oldvalue);
|
||||
const modes = ['btc', 'sats', 'fiat'];
|
||||
const oldIndex = modes.indexOf(this.stateService.viewAmountMode$.value);
|
||||
const newIndex = (oldIndex + 1) % modes.length;
|
||||
this.stateService.viewAmountMode$.next(modes[newIndex] as 'btc' | 'sats' | 'fiat');
|
||||
this.storageService.setValue('view-amount-mode', modes[newIndex]);
|
||||
}
|
||||
|
||||
trackByFn(index: number, tx: Transaction): string {
|
||||
|
@ -63,7 +63,7 @@ export class TxBowtieGraphTooltipComponent implements OnChanges {
|
||||
this.blockConversions = {};
|
||||
this.inputStatus = {};
|
||||
});
|
||||
this.viewFiatSubscription = this.stateService.viewFiat$.subscribe(viewFiat => this.viewFiat = viewFiat);
|
||||
this.viewFiatSubscription = this.stateService.viewAmountMode$.subscribe(viewFiat => this.viewFiat = viewFiat === 'fiat');
|
||||
this.chainTipSubscription = this.stateService.chainTip$.subscribe(tip => this.chainTip = tip);
|
||||
}
|
||||
|
||||
|
@ -27,6 +27,7 @@ import { PoolRankingComponent } from '../components/pool-ranking/pool-ranking.co
|
||||
import { PoolComponent } from '../components/pool/pool.component';
|
||||
import { TelevisionComponent } from '../components/television/television.component';
|
||||
import { DashboardComponent } from '../dashboard/dashboard.component';
|
||||
import { CustomDashboardComponent } from '../components/custom-dashboard/custom-dashboard.component';
|
||||
import { MiningDashboardComponent } from '../components/mining-dashboard/mining-dashboard.component';
|
||||
import { AcceleratorDashboardComponent } from '../components/acceleration/accelerator-dashboard/accelerator-dashboard.component';
|
||||
import { HashrateChartComponent } from '../components/hashrate-chart/hashrate-chart.component';
|
||||
@ -39,6 +40,7 @@ import { CommonModule } from '@angular/common';
|
||||
@NgModule({
|
||||
declarations: [
|
||||
DashboardComponent,
|
||||
CustomDashboardComponent,
|
||||
MempoolBlockComponent,
|
||||
AddressComponent,
|
||||
|
||||
|
@ -17,10 +17,16 @@ import { StartComponent } from '../components/start/start.component';
|
||||
import { StatisticsComponent } from '../components/statistics/statistics.component';
|
||||
import { TelevisionComponent } from '../components/television/television.component';
|
||||
import { DashboardComponent } from '../dashboard/dashboard.component';
|
||||
import { CustomDashboardComponent } from '../components/custom-dashboard/custom-dashboard.component';
|
||||
import { AccelerationFeesGraphComponent } from '../components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component';
|
||||
import { AccelerationsListComponent } from '../components/acceleration/accelerations-list/accelerations-list.component';
|
||||
import { AddressComponent } from '../components/address/address.component';
|
||||
|
||||
const browserWindow = window || {};
|
||||
// @ts-ignore
|
||||
const browserWindowEnv = browserWindow.__env || {};
|
||||
const isCustomized = browserWindowEnv?.customize;
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
@ -149,7 +155,7 @@ const routes: Routes = [
|
||||
component: StartComponent,
|
||||
children: [{
|
||||
path: '',
|
||||
component: DashboardComponent,
|
||||
component: isCustomized ? CustomDashboardComponent : DashboardComponent,
|
||||
}]
|
||||
},
|
||||
]
|
||||
|
@ -75,6 +75,16 @@ export interface MempoolBlockDelta {
|
||||
removed: string[];
|
||||
changed: { txid: string, rate: number, flags: number, acc: boolean }[];
|
||||
}
|
||||
export interface MempoolBlockState {
|
||||
transactions: TransactionStripped[];
|
||||
}
|
||||
export type MempoolBlockUpdate = MempoolBlockDelta | MempoolBlockState;
|
||||
export function isMempoolState(update: MempoolBlockUpdate): update is MempoolBlockState {
|
||||
return update['transactions'] !== undefined;
|
||||
}
|
||||
export function isMempoolDelta(update: MempoolBlockUpdate): update is MempoolBlockDelta {
|
||||
return update['transactions'] === undefined;
|
||||
}
|
||||
|
||||
export interface MempoolBlockDeltaCompressed {
|
||||
added: TransactionCompressed[];
|
||||
|
@ -23,7 +23,7 @@ export class EnterpriseService {
|
||||
private stateService: StateService,
|
||||
private activatedRoute: ActivatedRoute,
|
||||
) {
|
||||
const subdomain = this.document.location.hostname.indexOf(this.exclusiveHostName) > -1
|
||||
const subdomain = this.stateService.env.customize?.enterprise || this.document.location.hostname.indexOf(this.exclusiveHostName) > -1
|
||||
&& this.document.location.hostname.split(this.exclusiveHostName)[0] || false;
|
||||
if (subdomain && subdomain.match(/^[A-z0-9-_]+$/)) {
|
||||
this.subdomain = subdomain;
|
||||
@ -47,16 +47,23 @@ export class EnterpriseService {
|
||||
}
|
||||
|
||||
fetchSubdomainInfo(): void {
|
||||
this.apiService.getEnterpriseInfo$(this.subdomain).subscribe((info) => {
|
||||
if (this.stateService.env.customize?.branding) {
|
||||
const info = this.stateService.env.customize?.branding;
|
||||
this.insertMatomo(info.site_id);
|
||||
this.seoService.setEnterpriseTitle(info.title);
|
||||
this.seoService.setEnterpriseTitle(info.title, true);
|
||||
this.info$.next(info);
|
||||
},
|
||||
(error) => {
|
||||
if (error.status === 404) {
|
||||
window.location.href = 'https://mempool.space' + window.location.pathname;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.apiService.getEnterpriseInfo$(this.subdomain).subscribe((info) => {
|
||||
this.insertMatomo(info.site_id);
|
||||
this.seoService.setEnterpriseTitle(info.title);
|
||||
this.info$.next(info);
|
||||
},
|
||||
(error) => {
|
||||
if (error.status === 404) {
|
||||
window.location.href = 'https://mempool.space' + window.location.pathname;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
insertMatomo(siteId?: number): void {
|
||||
|
@ -50,8 +50,12 @@ export class SeoService {
|
||||
this.metaService.updateTag({ property: 'og:meta:ready', content: 'ready'});
|
||||
}
|
||||
|
||||
setEnterpriseTitle(title: string) {
|
||||
this.baseTitle = title + ' - ' + this.baseTitle;
|
||||
setEnterpriseTitle(title: string, override: boolean = false) {
|
||||
if (override) {
|
||||
this.baseTitle = title;
|
||||
} else {
|
||||
this.baseTitle = title + ' - ' + this.baseTitle;
|
||||
}
|
||||
this.resetTitle();
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Inject, Injectable, PLATFORM_ID, LOCALE_ID } from '@angular/core';
|
||||
import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable, merge } from 'rxjs';
|
||||
import { Transaction } from '../interfaces/electrs.interface';
|
||||
import { HealthCheckHost, IBackendInfo, MempoolBlock, MempoolBlockDelta, MempoolInfo, Recommendedfees, ReplacedTransaction, ReplacementInfo } from '../interfaces/websocket.interface';
|
||||
import { HealthCheckHost, IBackendInfo, MempoolBlock, MempoolBlockDelta, MempoolBlockUpdate, MempoolInfo, Recommendedfees, ReplacedTransaction, ReplacementInfo, isMempoolState } from '../interfaces/websocket.interface';
|
||||
import { BlockExtended, CpfpInfo, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, RbfTree, TransactionStripped } from '../interfaces/node-api.interface';
|
||||
import { Router, NavigationStart } from '@angular/router';
|
||||
import { isPlatformBrowser } from '@angular/common';
|
||||
@ -20,6 +20,24 @@ export interface MarkBlockState {
|
||||
|
||||
export interface ILoadingIndicators { [name: string]: number; }
|
||||
|
||||
export interface Customization {
|
||||
theme: string;
|
||||
enterprise?: string;
|
||||
branding: {
|
||||
name: string;
|
||||
site_id?: number;
|
||||
title: string;
|
||||
img: string;
|
||||
rounded_corner: boolean;
|
||||
},
|
||||
dashboard: {
|
||||
widgets: {
|
||||
component: string;
|
||||
props: { [key: string]: any };
|
||||
}[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface Env {
|
||||
TESTNET_ENABLED: boolean;
|
||||
SIGNET_ENABLED: boolean;
|
||||
@ -50,6 +68,7 @@ export interface Env {
|
||||
ADDITIONAL_CURRENCIES: boolean;
|
||||
GIT_COMMIT_HASH_MEMPOOL_SPACE?: string;
|
||||
PACKAGE_JSON_VERSION_MEMPOOL_SPACE?: string;
|
||||
customize?: Customization;
|
||||
}
|
||||
|
||||
const defaultEnv: Env = {
|
||||
@ -108,8 +127,7 @@ export class StateService {
|
||||
bsqPrice$ = new ReplaySubject<number>(1);
|
||||
mempoolInfo$ = new ReplaySubject<MempoolInfo>(1);
|
||||
mempoolBlocks$ = new ReplaySubject<MempoolBlock[]>(1);
|
||||
mempoolBlockTransactions$ = new Subject<TransactionStripped[]>();
|
||||
mempoolBlockDelta$ = new Subject<MempoolBlockDelta>();
|
||||
mempoolBlockUpdate$ = new Subject<MempoolBlockUpdate>();
|
||||
liveMempoolBlockTransactions$: Observable<{ [txid: string]: TransactionStripped}>;
|
||||
txConfirmed$ = new Subject<[string, BlockExtended]>();
|
||||
txReplaced$ = new Subject<ReplacedTransaction>();
|
||||
@ -136,7 +154,7 @@ export class StateService {
|
||||
|
||||
live2Chart$ = new Subject<OptimizedMempoolStats>();
|
||||
|
||||
viewFiat$ = new BehaviorSubject<boolean>(false);
|
||||
viewAmountMode$: BehaviorSubject<'btc' | 'sats' | 'fiat'>;
|
||||
connectionState$ = new BehaviorSubject<0 | 1 | 2>(2);
|
||||
isTabHidden$: Observable<boolean>;
|
||||
|
||||
@ -151,7 +169,7 @@ export class StateService {
|
||||
hideAudit: BehaviorSubject<boolean>;
|
||||
fiatCurrency$: BehaviorSubject<string>;
|
||||
rateUnits$: BehaviorSubject<string>;
|
||||
showMiningInfo$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||
blockDisplayMode$: BehaviorSubject<string>;
|
||||
|
||||
searchFocus$: Subject<boolean> = new Subject<boolean>();
|
||||
menuOpen$: BehaviorSubject<boolean> = new BehaviorSubject(false);
|
||||
@ -196,25 +214,25 @@ export class StateService {
|
||||
this.router.navigate(['/tracker/' + window.location.pathname.slice(4)]);
|
||||
}
|
||||
|
||||
this.liveMempoolBlockTransactions$ = merge(
|
||||
this.mempoolBlockTransactions$.pipe(map(transactions => { return { transactions }; })),
|
||||
this.mempoolBlockDelta$.pipe(map(delta => { return { delta }; })),
|
||||
).pipe(scan((transactions: { [txid: string]: TransactionStripped }, change: any): { [txid: string]: TransactionStripped } => {
|
||||
if (change.transactions) {
|
||||
const txMap = {}
|
||||
this.liveMempoolBlockTransactions$ = this.mempoolBlockUpdate$.pipe(scan((transactions: { [txid: string]: TransactionStripped }, change: MempoolBlockUpdate): { [txid: string]: TransactionStripped } => {
|
||||
if (isMempoolState(change)) {
|
||||
const txMap = {};
|
||||
change.transactions.forEach(tx => {
|
||||
txMap[tx.txid] = tx;
|
||||
})
|
||||
});
|
||||
return txMap;
|
||||
} else {
|
||||
change.delta.changed.forEach(tx => {
|
||||
transactions[tx.txid].rate = tx.rate;
|
||||
})
|
||||
change.delta.removed.forEach(txid => {
|
||||
change.added.forEach(tx => {
|
||||
transactions[tx.txid] = tx;
|
||||
});
|
||||
change.removed.forEach(txid => {
|
||||
delete transactions[txid];
|
||||
});
|
||||
change.delta.added.forEach(tx => {
|
||||
transactions[tx.txid] = tx;
|
||||
change.changed.forEach(tx => {
|
||||
if (transactions[tx.txid]) {
|
||||
transactions[tx.txid].rate = tx.rate;
|
||||
transactions[tx.txid].acc = tx.acc;
|
||||
}
|
||||
});
|
||||
return transactions;
|
||||
}
|
||||
@ -259,6 +277,12 @@ export class StateService {
|
||||
const rateUnitPreference = this.storageService.getValue('rate-unit-preference');
|
||||
this.rateUnits$ = new BehaviorSubject<string>(rateUnitPreference || 'vb');
|
||||
|
||||
const blockDisplayModePreference = this.storageService.getValue('block-display-mode-preference');
|
||||
this.blockDisplayMode$ = new BehaviorSubject<string>(blockDisplayModePreference || 'fees');
|
||||
|
||||
const viewAmountModePreference = this.storageService.getValue('view-amount-mode') as 'btc' | 'sats' | 'fiat';
|
||||
this.viewAmountMode$ = new BehaviorSubject<'btc' | 'sats' | 'fiat'>(viewAmountModePreference || 'btc');
|
||||
|
||||
this.backend$.subscribe(backend => {
|
||||
this.backend = backend;
|
||||
});
|
||||
|
@ -2,6 +2,7 @@ import { Injectable } from '@angular/core';
|
||||
import { Subject } from 'rxjs';
|
||||
import { defaultMempoolFeeColors, contrastMempoolFeeColors } from '../app.constants';
|
||||
import { StorageService } from './storage.service';
|
||||
import { StateService } from './state.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
@ -14,8 +15,9 @@ export class ThemeService {
|
||||
|
||||
constructor(
|
||||
private storageService: StorageService,
|
||||
private stateService: StateService,
|
||||
) {
|
||||
const theme = this.storageService.getValue('theme-preference') || 'default';
|
||||
const theme = this.storageService.getValue('theme-preference') || this.stateService.env.customize?.theme || 'default';
|
||||
this.apply(theme);
|
||||
}
|
||||
|
||||
|
@ -401,14 +401,16 @@ export class WebsocketService {
|
||||
if (response['projected-block-transactions'].index == this.trackingMempoolBlock) {
|
||||
if (response['projected-block-transactions'].blockTransactions) {
|
||||
this.stateService.mempoolSequence = response['projected-block-transactions'].sequence;
|
||||
this.stateService.mempoolBlockTransactions$.next(response['projected-block-transactions'].blockTransactions.map(uncompressTx));
|
||||
this.stateService.mempoolBlockUpdate$.next({
|
||||
transactions: response['projected-block-transactions'].blockTransactions.map(uncompressTx),
|
||||
});
|
||||
} else if (response['projected-block-transactions'].delta) {
|
||||
if (this.stateService.mempoolSequence && response['projected-block-transactions'].sequence !== this.stateService.mempoolSequence + 1) {
|
||||
this.stateService.mempoolSequence = 0;
|
||||
this.startTrackMempoolBlock(this.trackingMempoolBlock, true);
|
||||
} else {
|
||||
this.stateService.mempoolSequence = response['projected-block-transactions'].sequence;
|
||||
this.stateService.mempoolBlockDelta$.next(uncompressDeltaChange(response['projected-block-transactions'].delta));
|
||||
this.stateService.mempoolBlockUpdate$.next(uncompressDeltaChange(response['projected-block-transactions'].delta));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -45,7 +45,7 @@
|
||||
<div class="row col-md-12 link-tree" [class]="{'services': isServicesPage}">
|
||||
<div class="links">
|
||||
<p class="category" i18n="footer.explore">Explore</p>
|
||||
<p><a [routerLink]="['/mining' | relativeUrl]" i18n="mining.mining-dashboard">Mining Dashboard</a></p>
|
||||
<p><a *ngIf="env.MINING_DASHBOARD" [routerLink]="['/mining' | relativeUrl]" i18n="mining.mining-dashboard">Mining Dashboard</a></p>
|
||||
<p><a *ngIf="env.LIGHTNING" [routerLink]="['/lightning' | relativeUrl]" i18n="master-page.lightning">Lightning Explorer</a></p>
|
||||
<p><a [routerLink]="['/blocks' | relativeUrl]" i18n="dashboard.recent-blocks">Recent Blocks</a></p>
|
||||
<p><a [routerLink]="['/tx/push' | relativeUrl]" i18n="shared.broadcast-transaction|Broadcast Transaction">Broadcast Transaction</a></p>
|
||||
|
@ -65,6 +65,8 @@ import { FeesBoxComponent } from '../components/fees-box/fees-box.component';
|
||||
import { DifficultyComponent } from '../components/difficulty/difficulty.component';
|
||||
import { DifficultyTooltipComponent } from '../components/difficulty/difficulty-tooltip.component';
|
||||
import { DifficultyMiningComponent } from '../components/difficulty-mining/difficulty-mining.component';
|
||||
import { BalanceWidgetComponent } from '../components/balance-widget/balance-widget.component';
|
||||
import { AddressTransactionsWidgetComponent } from '../components/address-transactions-widget/address-transactions-widget.component';
|
||||
import { RbfTimelineComponent } from '../components/rbf-timeline/rbf-timeline.component';
|
||||
import { RbfTimelineTooltipComponent } from '../components/rbf-timeline/rbf-timeline-tooltip.component';
|
||||
import { PushTransactionComponent } from '../components/push-transaction/push-transaction.component';
|
||||
@ -174,6 +176,8 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
|
||||
DifficultyComponent,
|
||||
DifficultyMiningComponent,
|
||||
DifficultyTooltipComponent,
|
||||
BalanceWidgetComponent,
|
||||
AddressTransactionsWidgetComponent,
|
||||
RbfTimelineComponent,
|
||||
RbfTimelineTooltipComponent,
|
||||
PushTransactionComponent,
|
||||
@ -311,6 +315,8 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
|
||||
DifficultyComponent,
|
||||
DifficultyMiningComponent,
|
||||
DifficultyTooltipComponent,
|
||||
BalanceWidgetComponent,
|
||||
AddressTransactionsWidgetComponent,
|
||||
RbfTimelineComponent,
|
||||
RbfTimelineTooltipComponent,
|
||||
PushTransactionComponent,
|
||||
|
@ -5,6 +5,7 @@
|
||||
<meta charset="utf-8">
|
||||
<title>mempool - Bitcoin Explorer</title>
|
||||
<script src="/resources/config.js"></script>
|
||||
<script src="/resources/customize.js"></script>
|
||||
<base href="/">
|
||||
|
||||
<meta name="description" content="Explore the full Bitcoin ecosystem with The Mempool Open Source Project®. See the real-time status of your transactions, get network info, and more." />
|
||||
|
45
frontend/src/index.sv.html
Normal file
@ -0,0 +1,45 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Oficina Nacional del Bitcoin - Bitcoin Explorer</title>
|
||||
<script src="/resources/config.js"></script>
|
||||
<script src="/resources/customize.js"></script>
|
||||
<base href="/">
|
||||
|
||||
<meta name="description" content="Explore the full Bitcoin ecosystem with The Mempool Open Source Project®. See the real-time status of your transactions, get network info, and more." />
|
||||
<meta property="og:image" content="https://mempool.space/resources/sv/onbtc-preview.jpg" />
|
||||
<meta property="og:image:type" content="image/jpeg" />
|
||||
<meta property="og:image:width" content="2000" />
|
||||
<meta property="og:image:height" content="1000" />
|
||||
<meta property="og:description" content="Explore the full Bitcoin ecosystem with The Mempool Open Source Project®. See the real-time status of your transactions, get network info, and more." />
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:site" content="@mempool">
|
||||
<meta name="twitter:creator" content="@mempool">
|
||||
<meta name="twitter:title" content="Oficina Nacional del Bitcoin - Bitcoin Explorer">
|
||||
<meta name="twitter:description" content="Explore the full Bitcoin ecosystem with The Mempool Open Source Project®. See the real-time status of your transactions, get network info, and more." />
|
||||
<meta name="twitter:image" content="https://mempool.space/resources/sv/onbtc-preview.jpg" />
|
||||
<meta name="twitter:domain" content="bitcoin.gob.sv">
|
||||
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/resources/sv/favicons/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/resources/sv/favicons/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/resources/sv/favicons/favicon-16x16.png">
|
||||
<link rel="manifest" href="/resources/sv/favicons/site.webmanifest">
|
||||
<link rel="shortcut icon" href="/resources/sv/favicons/favicon.ico">
|
||||
<link id="canonical" rel="canonical" href="https://bitcoin.gob.sv">
|
||||
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||
<meta name="msapplication-TileColor" content="#000000">
|
||||
<meta name="msapplication-config" content="/resources/favicons/browserconfig.xml">
|
||||
<meta name="theme-color" content="#1d1f31">
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
|
||||
</html>
|
1135
frontend/src/resources/elsalvador.svg
Normal file
After Width: | Height: | Size: 80 KiB |
118
frontend/src/resources/onbtc.svg
Normal file
@ -0,0 +1,118 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
|
||||
<svg
|
||||
width="166.66856mm"
|
||||
height="27.910412mm"
|
||||
viewBox="0 0 166.66856 27.910412"
|
||||
version="1.1"
|
||||
id="svg5"
|
||||
sodipodi:docname="onbtc.svg"
|
||||
inkscape:version="1.2.2 (b0a84865, 2022-12-01)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview188"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="mm"
|
||||
showgrid="false"
|
||||
inkscape:zoom="1.9192657"
|
||||
inkscape:cx="243.84326"
|
||||
inkscape:cy="52.88481"
|
||||
inkscape:window-width="1728"
|
||||
inkscape:window-height="1051"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="38"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="text1855" />
|
||||
<defs
|
||||
id="defs2" />
|
||||
<g
|
||||
aria-label="OFICINA NACIONAL DEL BITCOIN"
|
||||
id="text1855"
|
||||
style="fill:#d2d2d2;stroke-width:3.37609;stroke-linecap:square"
|
||||
transform="translate(-15.744428,-40.187266)">
|
||||
<path
|
||||
d="m 28.492368,45.40464 c 0,-3.039246 -2.735322,-5.217373 -5.92653,-5.217373 -3.596442,0 -6.179801,2.380743 -6.179801,5.740798 0,3.157439 2.785976,5.284912 6.095377,5.284912 3.275632,0 6.010954,-2.397628 6.010954,-5.808337 z m -1.587162,0.557195 c 0,2.397627 -1.317007,4.829024 -4.069213,4.829024 -2.718437,0 -4.862794,-2.22878 -4.862794,-5.386219 0,-2.026164 1.181929,-4.795255 4.153636,-4.795255 2.785976,0 4.778371,2.211895 4.778371,5.35245 z"
|
||||
id="path415" />
|
||||
<path
|
||||
d="m 37.390604,43.935671 h -0.28704 c -0.05065,0.726042 -0.354579,1.080621 -0.979313,1.080621 h -3.34317 v -3.967905 h 3.208093 c 1.046851,0 1.384545,0.405233 1.40143,1.384545 h 0.28704 v -1.992394 h -7.564346 v 0.320809 c 1.181929,0.01689 1.266352,0.506541 1.266352,1.587162 v 6.618803 c 0,1.36766 -0.185731,1.587161 -1.232583,1.6547 v 0.32081 h 4.102982 v -0.32081 c -1.266352,0 -1.468968,-0.354579 -1.468968,-1.300122 v -3.697749 h 3.123669 c 0.810466,0 1.165045,0.303924 1.198814,1.283237 h 0.28704 z"
|
||||
id="path417" />
|
||||
<path
|
||||
d="m 43.215796,50.622012 c -1.165044,0 -1.435199,-0.337694 -1.435199,-1.333891 v -7.159113 c 0,-1.131275 0.371463,-1.367661 1.435199,-1.367661 v -0.320809 h -4.102982 v 0.320809 c 0.945543,0.05065 1.266353,0.303925 1.266353,1.215698 v 7.192883 c 0,1.131275 -0.219501,1.40143 -1.266353,1.452084 v 0.32081 h 4.102982 z"
|
||||
id="path419" />
|
||||
<path
|
||||
d="m 55.18702,43.209629 -0.151963,-2.448282 h -0.25327 c -0.03377,0.05065 -0.118193,0.101308 -0.303925,0.101308 -0.523426,0 -1.435199,-0.675388 -3.562672,-0.675388 -3.528902,0 -6.061607,2.363858 -6.061607,5.63949 0,3.12367 2.465166,5.38622 5.89276,5.38622 1.992395,0 2.735322,-0.60785 3.815943,-0.455887 0.405233,-0.658504 0.861119,-1.705355 1.080621,-2.465167 h -0.32081 c -1.148159,2.043049 -2.785975,2.498936 -4.119867,2.498936 -2.566474,0 -4.761485,-2.245665 -4.761485,-5.301796 0,-2.83663 1.756009,-4.879678 4.575754,-4.879678 1.840432,0 3.292516,1.029966 3.849711,2.600244 z"
|
||||
id="path421" />
|
||||
<path
|
||||
d="m 61.147288,50.622012 c -1.165044,0 -1.435199,-0.337694 -1.435199,-1.333891 v -7.159113 c 0,-1.131275 0.371463,-1.367661 1.435199,-1.367661 v -0.320809 h -4.102982 v 0.320809 c 0.945543,0.05065 1.266352,0.303925 1.266352,1.215698 v 7.192883 c 0,1.131275 -0.219501,1.40143 -1.266352,1.452084 v 0.32081 h 4.102982 z"
|
||||
id="path423" />
|
||||
<path
|
||||
d="m 73.810786,51.061014 v -9.421663 c 0,-0.658503 0.270155,-0.878004 1.536508,-0.878004 v -0.320809 h -3.95102 v 0.320809 c 1.350776,0.03377 1.654701,0.219501 1.654701,1.249468 v 6.720111 l -7.952694,-8.290388 h -2.650899 v 0.320809 c 0.590965,0.01689 1.029967,0.151962 1.418315,0.422117 v 8.307273 c 0,0.776696 -0.337694,1.131275 -1.384545,1.131275 h -0.06754 v 0.32081 h 3.917251 v -0.32081 c -1.4352,0 -1.705355,-0.25327 -1.705355,-1.468969 v -7.243536 l 8.746275,9.151507 z"
|
||||
id="path425" />
|
||||
<path
|
||||
d="m 82.776557,46.282644 1.131275,3.073016 c 0.06754,0.168847 0.185732,0.422117 0.185732,0.692272 0,0.472772 -0.354579,0.57408 -1.317007,0.57408 h -0.439002 v 0.32081 h 4.643293 v -0.32081 c -0.844235,0 -1.198814,-0.168847 -1.536508,-1.063736 l -3.528902,-9.371009 h -1.468969 l 0.151962,0.371464 -3.073016,8.391696 c -0.54031,1.468969 -0.726042,1.6547 -1.823547,1.671585 v 0.32081 h 3.900366 v -0.32081 h -0.270156 c -0.928658,0 -1.36766,-0.168847 -1.36766,-0.624734 0,-0.236386 0.219501,-0.742927 0.337694,-1.063736 l 0.979312,-2.650898 z m -0.28704,-0.742927 h -2.921053 l 1.4352,-3.934135 z"
|
||||
id="path427" />
|
||||
<path
|
||||
d="m 103.6629,51.061014 v -9.421663 c 0,-0.658503 0.27015,-0.878004 1.53651,-0.878004 v -0.320809 h -3.95102 v 0.320809 c 1.35077,0.03377 1.6547,0.219501 1.6547,1.249468 v 6.720111 l -7.952698,-8.290388 h -2.650898 v 0.320809 c 0.590965,0.01689 1.029967,0.151962 1.418315,0.422117 v 8.307273 c 0,0.776696 -0.337694,1.131275 -1.384545,1.131275 h -0.06754 v 0.32081 h 3.91725 v -0.32081 c -1.435199,0 -1.705354,-0.25327 -1.705354,-1.468969 v -7.243536 l 8.74628,9.151507 z"
|
||||
id="path429" />
|
||||
<path
|
||||
d="m 112.62867,46.282644 1.13127,3.073016 c 0.0675,0.168847 0.18574,0.422117 0.18574,0.692272 0,0.472772 -0.35458,0.57408 -1.31701,0.57408 h -0.439 v 0.32081 h 4.64329 v -0.32081 c -0.84424,0 -1.19881,-0.168847 -1.53651,-1.063736 l -3.5289,-9.371009 h -1.46897 l 0.15196,0.371464 -3.07301,8.391696 c -0.54031,1.468969 -0.72604,1.6547 -1.82355,1.671585 v 0.32081 h 3.90037 v -0.32081 h -0.27016 c -0.92866,0 -1.36766,-0.168847 -1.36766,-0.624734 0,-0.236386 0.2195,-0.742927 0.33769,-1.063736 l 0.97932,-2.650898 z m -0.28704,-0.742927 h -2.92105 l 1.4352,-3.934135 z"
|
||||
id="path431" />
|
||||
<path
|
||||
d="m 127.45339,43.209629 -0.15196,-2.448282 h -0.25327 c -0.0338,0.05065 -0.1182,0.101308 -0.30393,0.101308 -0.52342,0 -1.4352,-0.675388 -3.56267,-0.675388 -3.5289,0 -6.06161,2.363858 -6.06161,5.63949 0,3.12367 2.46517,5.38622 5.89276,5.38622 1.9924,0 2.73533,-0.60785 3.81595,-0.455887 0.40523,-0.658504 0.86112,-1.705355 1.08062,-2.465167 h -0.32081 c -1.14816,2.043049 -2.78598,2.498936 -4.11987,2.498936 -2.56647,0 -4.76148,-2.245665 -4.76148,-5.301796 0,-2.83663 1.756,-4.879678 4.57575,-4.879678 1.84043,0 3.29252,1.029966 3.84971,2.600244 z"
|
||||
id="path433" />
|
||||
<path
|
||||
d="m 133.41366,50.622012 c -1.16505,0 -1.4352,-0.337694 -1.4352,-1.333891 v -7.159113 c 0,-1.131275 0.37146,-1.367661 1.4352,-1.367661 v -0.320809 h -4.10299 v 0.320809 c 0.94555,0.05065 1.26636,0.303925 1.26636,1.215698 v 7.192883 c 0,1.131275 -0.2195,1.40143 -1.26636,1.452084 v 0.32081 h 4.10299 z"
|
||||
id="path435" />
|
||||
<path
|
||||
d="m 147.17466,45.40464 c 0,-3.039246 -2.73532,-5.217373 -5.92653,-5.217373 -3.59644,0 -6.1798,2.380743 -6.1798,5.740798 0,3.157439 2.78597,5.284912 6.09537,5.284912 3.27564,0 6.01096,-2.397628 6.01096,-5.808337 z m -1.58717,0.557195 c 0,2.397627 -1.317,4.829024 -4.06921,4.829024 -2.71843,0 -4.86279,-2.22878 -4.86279,-5.386219 0,-2.026164 1.18193,-4.795255 4.15363,-4.795255 2.78598,0 4.77837,2.211895 4.77837,5.35245 z"
|
||||
id="path437" />
|
||||
<path
|
||||
d="m 159.97326,51.061014 v -9.421663 c 0,-0.658503 0.27016,-0.878004 1.53651,-0.878004 v -0.320809 h -3.95102 v 0.320809 c 1.35078,0.03377 1.6547,0.219501 1.6547,1.249468 v 6.720111 l -7.95269,-8.290388 h -2.6509 v 0.320809 c 0.59096,0.01689 1.02996,0.151962 1.41831,0.422117 v 8.307273 c 0,0.776696 -0.33769,1.131275 -1.38454,1.131275 h -0.0675 v 0.32081 h 3.91725 v -0.32081 c -1.4352,0 -1.70536,-0.25327 -1.70536,-1.468969 v -7.243536 l 8.74628,9.151507 z"
|
||||
id="path439" />
|
||||
<path
|
||||
d="m 168.93903,46.282644 1.13127,3.073016 c 0.0675,0.168847 0.18573,0.422117 0.18573,0.692272 0,0.472772 -0.35458,0.57408 -1.317,0.57408 h -0.43901 v 0.32081 h 4.6433 v -0.32081 c -0.84424,0 -1.19882,-0.168847 -1.53651,-1.063736 l -3.5289,-9.371009 h -1.46897 l 0.15196,0.371464 -3.07302,8.391696 c -0.54031,1.468969 -0.72604,1.6547 -1.82354,1.671585 v 0.32081 h 3.90036 v -0.32081 h -0.27015 c -0.92866,0 -1.36766,-0.168847 -1.36766,-0.624734 0,-0.236386 0.2195,-0.742927 0.33769,-1.063736 l 0.97931,-2.650898 z m -0.28704,-0.742927 h -2.92106 l 1.4352,-3.934135 z"
|
||||
id="path441" />
|
||||
<path
|
||||
d="m 182.413,48.477655 h -0.30393 c -1.06373,1.468969 -1.36766,1.857317 -2.36386,1.857317 h -2.48205 c -0.48965,0 -0.67538,-0.168847 -0.67538,-0.624734 v -7.783847 c 0,-1.046851 0.28704,-1.165044 1.38454,-1.165044 h 0.62474 v -0.320809 h -4.98099 v 0.320809 h 0.23638 c 1.06374,0 1.3339,0.371463 1.3339,1.266352 v 7.479923 c 0,0.86112 -0.28704,1.11439 -1.06374,1.11439 h -0.5572 v 0.32081 h 7.54747 z"
|
||||
id="path443" />
|
||||
<path
|
||||
d="m 17.36535,66.27413 c 0,0.979312 -0.422118,1.232583 -1.333892,1.232583 h -0.287039 v 0.320809 h 5.403104 c 4.271829,0 6.247339,-3.056131 6.247339,-5.60572 0,-2.785976 -1.97551,-5.048526 -6.433071,-5.048526 -0.878004,0 -1.907971,0.01689 -2.83663,0.05065 -0.928658,0.03377 -1.756009,0.06754 -2.380742,0.101308 v 0.32081 h 0.337694 c 0.996197,0 1.283237,0.270155 1.283237,1.36766 z m 1.40143,-8.712506 c 0.202616,-0.01688 0.658503,-0.06754 1.350776,-0.06754 2.363858,0 3.562672,0.590965 4.339368,1.367661 0.996197,0.996198 1.350776,2.380743 1.350776,3.630211 0,1.739124 -0.776696,3.039246 -1.40143,3.647095 -1.046851,1.046852 -1.992395,1.367661 -4.254945,1.367661 -1.333891,0 -1.384545,-0.236386 -1.384545,-0.928659 z"
|
||||
id="path445" />
|
||||
<path
|
||||
d="m 36.157993,60.820371 h -0.320809 c -0.08442,0.979313 -0.371464,1.266353 -0.979313,1.266353 H 31.83551 v -4.153636 h 3.562672 c 0.607849,0 1.029966,0.303924 1.249468,1.739124 h 0.303924 L 36.816497,57.325238 H 28.71184 v 0.32081 h 0.371464 c 1.131275,0 1.350776,0.405233 1.350776,1.350776 v 7.125343 c 0,1.013083 -0.236386,1.384546 -1.587162,1.384546 h -0.472772 v 0.320809 h 8.965776 l 0.270155,-2.51582 h -0.303924 c -0.422118,1.452084 -0.776696,1.907971 -1.958625,1.907971 h -2.600244 c -0.624734,0 -0.911774,-0.151962 -0.911774,-0.928658 v -3.596442 h 2.853514 c 0.692273,0 1.013082,0.135078 1.14816,1.333892 h 0.320809 z"
|
||||
id="path447" />
|
||||
<path
|
||||
d="m 47.53828,65.362356 h -0.303925 c -1.063736,1.468969 -1.367661,1.857317 -2.363858,1.857317 h -2.482051 c -0.489656,0 -0.675388,-0.168847 -0.675388,-0.624734 v -7.783847 c 0,-1.046851 0.28704,-1.165044 1.384545,-1.165044 h 0.624734 v -0.32081 h -4.980986 v 0.32081 h 0.236385 c 1.063737,0 1.333892,0.371463 1.333892,1.266352 v 7.479923 c 0,0.861119 -0.28704,1.11439 -1.063736,1.11439 h -0.557195 v 0.320809 h 7.547461 z"
|
||||
id="path449" />
|
||||
<path
|
||||
d="m 54.410318,66.122167 c 0,1.063737 -0.270155,1.384546 -1.6547,1.384546 H 52.36727 v 0.320809 h 6.1798 c 2.718437,0 3.647095,-1.874202 3.647095,-3.208093 0,-1.317007 -0.928658,-2.346973 -2.51582,-2.819745 v -0.03377 c 1.131275,-0.202617 1.924856,-1.097506 1.924856,-2.059934 0,-0.810465 -0.337694,-1.384545 -1.029967,-1.874202 -0.607849,-0.439002 -1.907971,-0.658503 -3.393825,-0.658503 -0.320809,0 -1.604046,0.01689 -2.532705,0.05065 -0.489656,0.01688 -1.654701,0.08442 -2.127472,0.101308 v 0.32081 h 0.590964 c 1.181929,0 1.300122,0.506541 1.300122,1.468969 z m 1.40143,-8.577427 c 0.422118,-0.01688 1.114391,-0.05065 1.485854,-0.05065 2.076818,0 2.785976,1.080621 2.785976,2.26255 0,1.317007 -0.810466,1.958626 -2.431397,1.958626 h -1.840433 z m 0,4.49133 h 1.823548 c 1.97551,0 2.971707,1.333891 2.971707,2.634013 0,1.317007 -0.641618,2.701552 -2.870399,2.701552 -1.418315,0 -1.924856,-0.455887 -1.924856,-1.739124 z"
|
||||
id="path451" />
|
||||
<path
|
||||
d="m 67.951819,67.506713 c -1.165044,0 -1.435199,-0.337694 -1.435199,-1.333891 v -7.159114 c 0,-1.131274 0.371463,-1.36766 1.435199,-1.36766 v -0.32081 h -4.102982 v 0.32081 c 0.945543,0.05065 1.266353,0.303924 1.266353,1.215698 v 7.192883 c 0,1.131275 -0.219502,1.40143 -1.266353,1.452084 v 0.320809 h 4.102982 z"
|
||||
id="path453" />
|
||||
<path
|
||||
d="m 75.127789,57.933088 h 2.937938 c 0.675388,0 0.979313,0.388348 1.232584,1.874201 h 0.303924 L 79.33208,57.071968 h -0.320809 c -0.03377,0.202616 -0.28704,0.25327 -0.523426,0.25327 h -8.121541 c -0.118193,0 -0.236386,-0.01688 -0.337694,-0.05065 -0.101308,-0.03377 -0.168847,-0.101308 -0.185732,-0.202616 h -0.320809 l -0.270155,2.735321 h 0.303924 c 0.253271,-1.485853 0.557195,-1.874201 1.232583,-1.874201 h 2.937938 v 8.341042 c 0,0.911774 -0.303924,1.232583 -1.333891,1.232583 h -0.624734 v 0.320809 h 5.318681 v -0.320809 h -0.624734 c -1.029967,0 -1.333892,-0.320809 -1.333892,-1.232583 z"
|
||||
id="path455" />
|
||||
<path
|
||||
d="m 91.353956,60.094329 -0.151962,-2.448281 h -0.25327 c -0.03377,0.05065 -0.118193,0.101308 -0.303925,0.101308 -0.523426,0 -1.4352,-0.675388 -3.562672,-0.675388 -3.528902,0 -6.061607,2.363858 -6.061607,5.63949 0,3.12367 2.465166,5.386219 5.89276,5.386219 1.992395,0 2.735322,-0.607849 3.815943,-0.455886 0.405232,-0.658504 0.861119,-1.705355 1.08062,-2.465167 h -0.320809 c -1.14816,2.043049 -2.785976,2.498936 -4.119867,2.498936 -2.566474,0 -4.761486,-2.245665 -4.761486,-5.301796 0,-2.83663 1.756009,-4.879679 4.575754,-4.879679 1.840433,0 3.292517,1.029967 3.849712,2.600244 z"
|
||||
id="path457" />
|
||||
<path
|
||||
d="m 105.45265,62.28934 c 0,-3.039246 -2.73532,-5.217372 -5.926531,-5.217372 -3.596441,0 -6.1798,2.380743 -6.1798,5.740798 0,3.157439 2.785975,5.284911 6.095376,5.284911 3.275635,0 6.010955,-2.397627 6.010955,-5.808337 z m -1.58716,0.557196 c 0,2.397627 -1.31701,4.829024 -4.069216,4.829024 -2.718437,0 -4.862794,-2.228781 -4.862794,-5.38622 0,-2.026164 1.181929,-4.795255 4.153637,-4.795255 2.785973,0 4.778373,2.211896 4.778373,5.352451 z"
|
||||
id="path459" />
|
||||
<path
|
||||
d="m 111.21033,67.506713 c -1.16504,0 -1.4352,-0.337694 -1.4352,-1.333891 v -7.159114 c 0,-1.131274 0.37147,-1.36766 1.4352,-1.36766 v -0.32081 h -4.10298 v 0.32081 c 0.94554,0.05065 1.26635,0.303924 1.26635,1.215698 v 7.192883 c 0,1.131275 -0.2195,1.40143 -1.26635,1.452084 v 0.320809 h 4.10298 z"
|
||||
id="path461" />
|
||||
<path
|
||||
d="m 123.87383,67.945715 v -9.421663 c 0,-0.658503 0.27015,-0.878004 1.53651,-0.878004 v -0.32081 h -3.95102 v 0.32081 c 1.35077,0.03377 1.6547,0.219501 1.6547,1.249468 v 6.72011 l -7.9527,-8.290388 h -2.6509 v 0.32081 c 0.59097,0.01688 1.02997,0.151962 1.41832,0.422117 v 8.307273 c 0,0.776696 -0.33769,1.131275 -1.38455,1.131275 h -0.0675 v 0.320809 h 3.91726 v -0.320809 c -1.4352,0 -1.70536,-0.253271 -1.70536,-1.468969 v -7.243537 l 8.74628,9.151508 z"
|
||||
id="path463" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 14 KiB |
BIN
frontend/src/resources/sv/favicons/android-chrome-192x192.png
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
frontend/src/resources/sv/favicons/android-chrome-512x512.png
Normal file
After Width: | Height: | Size: 66 KiB |
BIN
frontend/src/resources/sv/favicons/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
frontend/src/resources/sv/favicons/favicon-16x16.png
Normal file
After Width: | Height: | Size: 756 B |
BIN
frontend/src/resources/sv/favicons/favicon-32x32.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
frontend/src/resources/sv/favicons/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
1
frontend/src/resources/sv/favicons/site.webmanifest
Normal file
@ -0,0 +1 @@
|
||||
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
|
BIN
frontend/src/resources/sv/onbtc-preview.jpg
Normal file
After Width: | Height: | Size: 80 KiB |
@ -129,8 +129,8 @@ export NVM_DIR="${HOME}/.nvm"
|
||||
source "${NVM_DIR}/nvm.sh"
|
||||
|
||||
# what to look for
|
||||
frontends=(mainnet liquid)
|
||||
backends=(mainnet testnet signet liquid liquidtestnet)
|
||||
frontends=(mainnet liquid onbtc)
|
||||
backends=(mainnet testnet signet liquid liquidtestnet onbtc)
|
||||
frontend_repos=()
|
||||
backend_repos=()
|
||||
|
||||
@ -151,7 +151,7 @@ for repo in $backend_repos;do
|
||||
done
|
||||
|
||||
# build unfurlers
|
||||
for repo in mainnet liquid;do
|
||||
for repo in $frontend_repos;do
|
||||
build_unfurler "${repo}"
|
||||
done
|
||||
|
||||
@ -166,7 +166,7 @@ for repo in $frontend_repos;do
|
||||
done
|
||||
|
||||
# ship frontend dist folders to public_html
|
||||
for target in mainnet liquid;do
|
||||
for target in $frontend_repos;do
|
||||
ship_frontend "${target}"
|
||||
done
|
||||
|
||||
|
@ -1,5 +1,4 @@
|
||||
{
|
||||
"BASE_MODULE": "bisq",
|
||||
"OFFICIAL_MEMPOOL_SPACE": true,
|
||||
"TESTNET_ENABLED": true,
|
||||
"LIQUID_ENABLED": true,
|
||||
@ -10,5 +9,10 @@
|
||||
"MEMPOOL_WEBSITE_URL": "https://mempool.space",
|
||||
"LIQUID_WEBSITE_URL": "https://liquid.network",
|
||||
"BISQ_WEBSITE_URL": "https://bisq.markets",
|
||||
"ITEMS_PER_PAGE": 25
|
||||
"ITEMS_PER_PAGE": 25,
|
||||
"LIGHTNING": true,
|
||||
"ACCELERATOR": true,
|
||||
"PUBLIC_ACCELERATIONS": true,
|
||||
"AUDIT": true,
|
||||
"CUSTOMIZATION": "custom-sv-config.json"
|
||||
}
|
@ -10,7 +10,7 @@ killall sh 2>/dev/null
|
||||
killall chrome 2>/dev/null
|
||||
|
||||
# kill xorg
|
||||
killall xinit 2>/dev/null
|
||||
killall xinit Xorg 2>/dev/null
|
||||
|
||||
# kill dbus
|
||||
killall dbus-daemon 2>/dev/null
|
||||
|
@ -15,7 +15,7 @@ screen -dmS x startx
|
||||
sleep 3
|
||||
|
||||
# start unfurlers for each frontend
|
||||
for site in mainnet liquid ;do
|
||||
for site in mainnet liquid onbtc;do
|
||||
cd "$HOME/${site}/unfurler" && \
|
||||
echo "starting mempool unfurler: ${site}" && \
|
||||
screen -dmS "unfurler-${site}" sh -c 'while true;do npm run unfurler;sleep 2;done'
|
||||
|
@ -1,12 +1,12 @@
|
||||
{
|
||||
"SERVER": {
|
||||
"HOST": "https://bisq.fra.mempool.space",
|
||||
"HTTP_PORT": 8002
|
||||
"HOST": "https://onbtc.tk7.mempool.space",
|
||||
"HTTP_PORT": 8004
|
||||
},
|
||||
"MEMPOOL": {
|
||||
"HTTP_HOST": "http://127.0.0.1",
|
||||
"HTTP_PORT": 82,
|
||||
"NETWORK": "bisq"
|
||||
"HTTP_PORT": 83,
|
||||
"NETWORK": "onbtc"
|
||||
},
|
||||
"PUPPETEER": {
|
||||
"CLUSTER_SIZE": 8,
|
14
unfurler/package-lock.json
generated
@ -9,7 +9,7 @@
|
||||
"version": "3.0.0-dev",
|
||||
"dependencies": {
|
||||
"@types/node": "^16.11.41",
|
||||
"ejs": "^3.1.9",
|
||||
"ejs": "^3.1.10",
|
||||
"express": "^4.19.2",
|
||||
"node-fetch-commonjs": "^3.3.1",
|
||||
"puppeteer": "^15.3.2",
|
||||
@ -772,9 +772,9 @@
|
||||
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
|
||||
},
|
||||
"node_modules/ejs": {
|
||||
"version": "3.1.9",
|
||||
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz",
|
||||
"integrity": "sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==",
|
||||
"version": "3.1.10",
|
||||
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
|
||||
"integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==",
|
||||
"dependencies": {
|
||||
"jake": "^10.8.5"
|
||||
},
|
||||
@ -3306,9 +3306,9 @@
|
||||
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
|
||||
},
|
||||
"ejs": {
|
||||
"version": "3.1.9",
|
||||
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz",
|
||||
"integrity": "sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==",
|
||||
"version": "3.1.10",
|
||||
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
|
||||
"integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==",
|
||||
"requires": {
|
||||
"jake": "^10.8.5"
|
||||
}
|
||||
|
@ -19,7 +19,7 @@
|
||||
"dependencies": {
|
||||
"@types/node": "^16.11.41",
|
||||
"express": "^4.19.2",
|
||||
"ejs": "^3.1.9",
|
||||
"ejs": "^3.1.10",
|
||||
"node-fetch-commonjs": "^3.3.1",
|
||||
"puppeteer": "^15.3.2",
|
||||
"puppeteer-cluster": "^0.23.0",
|
||||
|
@ -50,6 +50,9 @@ class Server {
|
||||
case "bisq":
|
||||
canonical = "https://bisq.markets"
|
||||
break;
|
||||
case "onbtc":
|
||||
canonical = "https://bitcoin.gob.sv"
|
||||
break;
|
||||
default:
|
||||
canonical = "https://mempool.space"
|
||||
}
|
||||
|
@ -252,6 +252,24 @@ const networks = {
|
||||
bisq: {
|
||||
fallbackImg: '/resources/bisq/bisq-markets-preview.png',
|
||||
routes: {} // no routes supported
|
||||
},
|
||||
onbtc: {
|
||||
fallbackImg: '/resources/onbtc/onbtc-preview.png',
|
||||
routes: { // only dynamic routes supported
|
||||
block: routes.block,
|
||||
address: routes.address,
|
||||
tx: routes.tx,
|
||||
mining: {
|
||||
title: "Mining",
|
||||
routes: {
|
||||
pool: routes.mining.routes.pool,
|
||||
}
|
||||
},
|
||||
lightning: {
|
||||
title: "Lightning",
|
||||
routes: routes.lightning.routes,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|