mempool/frontend/src/app/services/state.service.ts

446 lines
16 KiB
TypeScript
Raw Normal View History

import { Inject, Injectable, PLATFORM_ID, LOCALE_ID } from '@angular/core';
import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable } from 'rxjs';
import { Transaction } from '../interfaces/electrs.interface';
import { AccelerationDelta, HealthCheckHost, IBackendInfo, MempoolBlock, MempoolBlockUpdate, MempoolInfo, Recommendedfees, ReplacedTransaction, ReplacementInfo, isMempoolState } from '../interfaces/websocket.interface';
import { Acceleration, AccelerationPosition, BlockExtended, CpfpInfo, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, RbfTree, TransactionStripped } from '../interfaces/node-api.interface';
import { Router, NavigationStart } from '@angular/router';
2020-11-07 04:30:52 +07:00
import { isPlatformBrowser } from '@angular/common';
2023-07-11 16:35:00 +09:00
import { filter, map, scan, shareReplay } from 'rxjs/operators';
import { StorageService } from './storage.service';
import { hasTouchScreen } from '../shared/pipes/bytes-pipe/utils';
import { ActiveFilter } from '../shared/filters.utils';
export interface MarkBlockState {
blockHeight?: number;
txid?: string;
mempoolBlockIndex?: number;
txFeePerVSize?: number;
mempoolPosition?: MempoolPosition;
accelerationPositions?: AccelerationPosition[];
}
export interface ILoadingIndicators { [name: string]: number; }
2024-04-25 20:18:37 +00:00
export interface Customization {
theme: string;
enterprise?: string;
2024-04-25 20:39:57 +00:00
branding: {
name: string;
site_id?: number;
title: string;
2024-05-07 22:42:06 +00:00
img?: string;
header_img?: string;
footer_img?: string;
2024-04-25 20:39:57 +00:00
rounded_corner: boolean;
},
2024-04-25 20:18:37 +00:00
dashboard: {
widgets: {
component: string;
2024-05-01 23:40:39 +00:00
mobileOrder?: number;
2024-04-25 20:18:37 +00:00
props: { [key: string]: any };
}[];
};
}
export interface Env {
2024-06-15 00:22:33 +02:00
MAINNET_ENABLED: boolean;
TESTNET_ENABLED: boolean;
2024-05-06 15:40:32 +00:00
TESTNET4_ENABLED: boolean;
2021-02-20 23:12:22 +07:00
SIGNET_ENABLED: boolean;
LIQUID_ENABLED: boolean;
LIQUID_TESTNET_ENABLED: boolean;
ITEMS_PER_PAGE: number;
KEEP_BLOCKS_AMOUNT: number;
OFFICIAL_MEMPOOL_SPACE: boolean;
BASE_MODULE: string;
2024-06-16 10:50:31 +02:00
ROOT_NETWORK: string;
NGINX_PROTOCOL?: string;
NGINX_HOSTNAME?: string;
NGINX_PORT?: string;
BLOCK_WEIGHT_UNITS: number;
MEMPOOL_BLOCKS_AMOUNT: number;
GIT_COMMIT_HASH: string;
PACKAGE_JSON_VERSION: string;
MEMPOOL_WEBSITE_URL: string;
LIQUID_WEBSITE_URL: string;
MINING_DASHBOARD: boolean;
LIGHTNING: boolean;
AUDIT: boolean;
MAINNET_BLOCK_AUDIT_START_HEIGHT: number;
TESTNET_BLOCK_AUDIT_START_HEIGHT: number;
SIGNET_BLOCK_AUDIT_START_HEIGHT: number;
HISTORICAL_PRICE: boolean;
2023-08-03 15:18:04 +09:00
ACCELERATOR: boolean;
PUBLIC_ACCELERATIONS: boolean;
ADDITIONAL_CURRENCIES: boolean;
GIT_COMMIT_HASH_MEMPOOL_SPACE?: string;
PACKAGE_JSON_VERSION_MEMPOOL_SPACE?: string;
2024-04-25 20:18:37 +00:00
customize?: Customization;
}
const defaultEnv: Env = {
2024-06-15 00:22:33 +02:00
'MAINNET_ENABLED': true,
'TESTNET_ENABLED': false,
2024-05-06 15:40:32 +00:00
'TESTNET4_ENABLED': false,
2021-02-20 23:12:22 +07:00
'SIGNET_ENABLED': false,
'LIQUID_ENABLED': false,
'LIQUID_TESTNET_ENABLED': false,
'BASE_MODULE': 'mempool',
2024-06-16 10:50:31 +02:00
'ROOT_NETWORK': '',
'ITEMS_PER_PAGE': 10,
'KEEP_BLOCKS_AMOUNT': 8,
'OFFICIAL_MEMPOOL_SPACE': false,
'NGINX_PROTOCOL': 'http',
'NGINX_HOSTNAME': '127.0.0.1',
'NGINX_PORT': '80',
'BLOCK_WEIGHT_UNITS': 4000000,
'MEMPOOL_BLOCKS_AMOUNT': 8,
'GIT_COMMIT_HASH': '',
'PACKAGE_JSON_VERSION': '',
'MEMPOOL_WEBSITE_URL': 'https://mempool.space',
'LIQUID_WEBSITE_URL': 'https://liquid.network',
'MINING_DASHBOARD': true,
'LIGHTNING': false,
'AUDIT': false,
'MAINNET_BLOCK_AUDIT_START_HEIGHT': 0,
'TESTNET_BLOCK_AUDIT_START_HEIGHT': 0,
'SIGNET_BLOCK_AUDIT_START_HEIGHT': 0,
'HISTORICAL_PRICE': true,
2023-08-03 15:18:04 +09:00
'ACCELERATOR': false,
'PUBLIC_ACCELERATIONS': false,
'ADDITIONAL_CURRENCIES': false,
};
@Injectable({
providedIn: 'root'
})
export class StateService {
2024-04-16 16:01:52 +09:00
referrer: string = '';
2020-11-07 04:30:52 +07:00
isBrowser: boolean = isPlatformBrowser(this.platformId);
isMempoolSpaceBuild = window['isMempoolSpaceBuild'] ?? false;
backend: 'esplora' | 'electrum' | 'none' = 'esplora';
network = '';
lightningNetworks = ['', 'mainnet', 'bitcoin', 'testnet', 'signet'];
2022-08-11 17:19:12 +00:00
lightning = false;
blockVSize: number;
env: Env;
2022-01-19 17:11:35 +01:00
latestBlockHeight = -1;
2023-07-08 01:07:06 -04:00
blocks: BlockExtended[] = [];
mempoolSequence: number;
backend$ = new BehaviorSubject<'esplora' | 'electrum' | 'none'>('esplora');
networkChanged$ = new ReplaySubject<string>(1);
2022-08-11 17:19:12 +00:00
lightningChanged$ = new ReplaySubject<boolean>(1);
2023-07-11 16:35:00 +09:00
blocksSubject$ = new BehaviorSubject<BlockExtended[]>([]);
blocks$: Observable<BlockExtended[]>;
transactions$ = new BehaviorSubject<TransactionStripped[]>(null);
conversions$ = new ReplaySubject<any>(1);
2020-07-14 21:26:02 +07:00
bsqPrice$ = new ReplaySubject<number>(1);
mempoolInfo$ = new ReplaySubject<MempoolInfo>(1);
2020-07-24 14:11:49 +07:00
mempoolBlocks$ = new ReplaySubject<MempoolBlock[]>(1);
mempoolBlockUpdate$ = new Subject<MempoolBlockUpdate>();
liveMempoolBlockTransactions$: Observable<{ [txid: string]: TransactionStripped}>;
accelerations$ = new Subject<AccelerationDelta>();
liveAccelerations$: Observable<Acceleration[]>;
2023-07-08 01:07:06 -04:00
txConfirmed$ = new Subject<[string, BlockExtended]>();
2022-03-08 14:49:25 +01:00
txReplaced$ = new Subject<ReplacedTransaction>();
2022-12-17 09:39:06 -06:00
txRbfInfo$ = new Subject<RbfTree>();
rbfLatest$ = new Subject<RbfTree[]>();
2023-07-14 16:08:57 +09:00
rbfLatestSummary$ = new Subject<ReplacementInfo[]>();
utxoSpent$ = new Subject<object>();
difficultyAdjustment$ = new ReplaySubject<DifficultyAdjustment>(1);
2020-02-23 19:16:50 +07:00
mempoolTransactions$ = new Subject<Transaction>();
mempoolTxPosition$ = new BehaviorSubject<{ txid: string, position: MempoolPosition, cpfp: CpfpInfo | null, accelerationPositions?: AccelerationPosition[] }>(null);
mempoolRemovedTransactions$ = new Subject<Transaction>();
2024-02-16 02:30:51 +00:00
multiAddressTransactions$ = new Subject<{ [address: string]: { mempool: Transaction[], confirmed: Transaction[], removed: Transaction[] }}>();
2020-02-23 19:16:50 +07:00
blockTransactions$ = new Subject<Transaction>();
isLoadingWebSocket$ = new ReplaySubject<boolean>(1);
2023-07-24 16:22:35 +09:00
isLoadingMempool$ = new BehaviorSubject<boolean>(true);
vbytesPerSecond$ = new ReplaySubject<number>(1);
previousRetarget$ = new ReplaySubject<number>(1);
backendInfo$ = new ReplaySubject<IBackendInfo>(1);
servicesBackendInfo$ = new ReplaySubject<IBackendInfo>(1);
loadingIndicators$ = new ReplaySubject<ILoadingIndicators>(1);
recommendedFees$ = new ReplaySubject<Recommendedfees>(1);
chainTip$ = new ReplaySubject<number>(-1);
2024-03-03 20:31:02 +00:00
serverHealth$ = new Subject<HealthCheckHost[]>();
2020-02-23 19:16:50 +07:00
2020-02-17 00:26:57 +07:00
live2Chart$ = new Subject<OptimizedMempoolStats>();
viewAmountMode$: BehaviorSubject<'btc' | 'sats' | 'fiat'>;
connectionState$ = new BehaviorSubject<0 | 1 | 2>(2);
2020-11-07 04:30:52 +07:00
isTabHidden$: Observable<boolean>;
markBlock$ = new BehaviorSubject<MarkBlockState>({});
2020-05-13 13:03:57 +07:00
keyNavigation$ = new Subject<KeyboardEvent>();
searchText$ = new BehaviorSubject<string>('');
blockScrolling$: Subject<boolean> = new Subject<boolean>();
2023-06-09 19:03:47 -04:00
resetScroll$: Subject<boolean> = new Subject<boolean>();
timeLtr: BehaviorSubject<boolean>;
hideFlow: BehaviorSubject<boolean>;
hideAudit: BehaviorSubject<boolean>;
2023-01-03 11:56:54 -06:00
fiatCurrency$: BehaviorSubject<string>;
rateUnits$: BehaviorSubject<string>;
blockDisplayMode$: BehaviorSubject<string>;
searchFocus$: Subject<boolean> = new Subject<boolean>();
menuOpen$: BehaviorSubject<boolean> = new BehaviorSubject(false);
2024-04-04 11:22:57 +00:00
activeGoggles$: BehaviorSubject<ActiveFilter> = new BehaviorSubject({ mode: 'and', filters: [], gradient: 'age' });
constructor(
2020-11-07 04:30:52 +07:00
@Inject(PLATFORM_ID) private platformId: any,
@Inject(LOCALE_ID) private locale: string,
private router: Router,
private storageService: StorageService,
) {
2024-04-16 16:01:52 +09:00
this.referrer = window.document.referrer;
const browserWindow = window || {};
// @ts-ignore
const browserWindowEnv = browserWindow.__env || {};
this.env = Object.assign(defaultEnv, browserWindowEnv);
2020-11-07 04:30:52 +07:00
if (defaultEnv.BASE_MODULE !== 'mempool') {
this.env.MINING_DASHBOARD = false;
}
2020-11-07 04:30:52 +07:00
if (this.isBrowser) {
this.setNetworkBasedonUrl(window.location.pathname);
2022-08-11 17:19:12 +00:00
this.setLightningBasedonUrl(window.location.pathname);
2020-11-07 04:30:52 +07:00
this.isTabHidden$ = fromEvent(document, 'visibilitychange').pipe(map(() => this.isHidden()), shareReplay());
} else {
this.setNetworkBasedonUrl('/');
2022-08-11 17:19:12 +00:00
this.setLightningBasedonUrl('/');
2020-11-07 04:30:52 +07:00
this.isTabHidden$ = new BehaviorSubject(false);
}
this.router.events.subscribe((event) => {
if (event instanceof NavigationStart) {
this.setNetworkBasedonUrl(event.url);
2022-08-11 17:19:12 +00:00
this.setLightningBasedonUrl(event.url);
}
});
2024-04-16 16:01:52 +09:00
if (this.referrer === 'https://cash.app/' && window.innerWidth < 850 && window.location.pathname.startsWith('/tx/')) {
2024-04-13 09:24:52 +00:00
this.router.navigate(['/tracker/' + window.location.pathname.slice(4)]);
}
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.added.forEach(tx => {
transactions[tx.txid] = tx;
});
change.removed.forEach(txid => {
delete transactions[txid];
});
change.changed.forEach(tx => {
if (transactions[tx.txid]) {
transactions[tx.txid].rate = tx.rate;
transactions[tx.txid].acc = tx.acc;
}
});
return transactions;
}
}, {}));
// Emits the full list of pending accelerations each time it changes
this.liveAccelerations$ = this.accelerations$.pipe(
scan((accelerations: { [txid: string]: Acceleration }, delta: AccelerationDelta) => {
if (delta.reset) {
accelerations = {};
} else {
for (const txid of delta.removed) {
delete accelerations[txid];
}
}
for (const acc of delta.added) {
accelerations[acc.txid] = acc;
}
return accelerations;
}, {}),
map((accMap) => Object.values(accMap).sort((a,b) => b.added - a.added))
);
this.networkChanged$.subscribe((network) => {
this.transactions$ = new BehaviorSubject<TransactionStripped[]>(null);
2023-07-13 11:03:44 +09:00
this.blocksSubject$.next([]);
});
this.blockVSize = this.env.BLOCK_WEIGHT_UNITS / 4;
2023-07-11 16:35:00 +09:00
this.blocks$ = this.blocksSubject$.pipe(filter(blocks => blocks != null && blocks.length > 0));
const savedTimePreference = this.storageService.getValue('time-preference-ltr');
const rtlLanguage = (this.locale.startsWith('ar') || this.locale.startsWith('fa') || this.locale.startsWith('he'));
// default time direction is right-to-left, unless locale is a RTL language
this.timeLtr = new BehaviorSubject<boolean>(savedTimePreference === 'true' || (savedTimePreference == null && rtlLanguage));
this.timeLtr.subscribe((ltr) => {
this.storageService.setValue('time-preference-ltr', ltr ? 'true' : 'false');
});
const savedFlowPreference = this.storageService.getValue('flow-preference');
this.hideFlow = new BehaviorSubject<boolean>(savedFlowPreference === 'hide');
this.hideFlow.subscribe((hide) => {
if (hide) {
this.storageService.setValue('flow-preference', hide ? 'hide' : 'show');
} else {
this.storageService.removeItem('flow-preference');
}
});
const savedAuditPreference = this.storageService.getValue('audit-preference');
this.hideAudit = new BehaviorSubject<boolean>(savedAuditPreference === 'hide');
this.hideAudit.subscribe((hide) => {
this.storageService.setValue('audit-preference', hide ? 'hide' : 'show');
});
2023-01-03 11:56:54 -06:00
const fiatPreference = this.storageService.getValue('fiat-preference');
this.fiatCurrency$ = new BehaviorSubject<string>(fiatPreference || 'USD');
const rateUnitPreference = this.storageService.getValue('rate-unit-preference');
this.rateUnits$ = new BehaviorSubject<string>(rateUnitPreference || 'vb');
2024-04-21 14:54:50 +02:00
const blockDisplayModePreference = this.storageService.getValue('block-display-mode-preference');
this.blockDisplayMode$ = new BehaviorSubject<string>(blockDisplayModePreference || 'fees');
2024-04-21 14:54:50 +02:00
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;
});
}
2020-05-10 12:31:57 +07:00
setNetworkBasedonUrl(url: string) {
if (this.env.BASE_MODULE !== 'mempool' && this.env.BASE_MODULE !== 'liquid') {
return;
}
// horrible network regex breakdown:
// /^\/ starts with a forward slash...
// (?:[a-z]{2}(?:-[A-Z]{2})?\/)? optional locale prefix (non-capturing)
// (?:preview\/)? optional "preview" prefix (non-capturing)
2024-04-03 16:09:30 +09:00
// (testnet|signet)/ network string (captured as networkMatches[1])
// ($|\/) network string must end or end with a slash
2024-06-21 19:32:25 +09:00
let networkMatches: object = url.match(/^\/(?:[a-z]{2}(?:-[A-Z]{2})?\/)?(?:preview\/)?(testnet4?|signet)($|\/)/);
if (!networkMatches && this.env.ROOT_NETWORK) {
networkMatches = { 1: this.env.ROOT_NETWORK };
}
switch (networkMatches && networkMatches[1]) {
2021-02-20 23:12:22 +07:00
case 'signet':
if (this.network !== 'signet') {
this.network = 'signet';
this.networkChanged$.next('signet');
}
return;
2020-05-10 12:31:57 +07:00
case 'testnet':
2024-04-03 16:09:30 +09:00
if (this.network !== 'testnet' && this.network !== 'liquidtestnet') {
if (this.env.BASE_MODULE === 'liquid') {
this.network = 'liquidtestnet';
this.networkChanged$.next('liquidtestnet');
} else {
this.network = 'testnet';
this.networkChanged$.next('testnet');
}
2020-05-10 12:31:57 +07:00
}
return;
2024-05-06 15:40:32 +00:00
case 'testnet4':
if (this.network !== 'testnet4') {
this.network = 'testnet4';
this.networkChanged$.next('testnet4');
}
return;
2020-05-10 12:31:57 +07:00
default:
if (this.env.BASE_MODULE !== 'mempool') {
if (this.network !== this.env.BASE_MODULE) {
this.network = this.env.BASE_MODULE;
this.networkChanged$.next(this.env.BASE_MODULE);
}
} else if (this.network !== '') {
2024-06-21 19:32:25 +09:00
this.network = '';
this.networkChanged$.next('');
2020-05-10 12:31:57 +07:00
}
}
}
2022-08-11 17:19:12 +00:00
setLightningBasedonUrl(url: string) {
if (this.env.BASE_MODULE !== 'mempool') {
return;
}
const networkMatches = url.match(/\/lightning\//);
this.lightning = !!networkMatches;
this.lightningChanged$.next(this.lightning);
}
networkSupportsLightning() {
return this.env.LIGHTNING && this.lightningNetworks.includes(this.network);
}
getHiddenProp(){
const prefixes = ['webkit', 'moz', 'ms', 'o'];
if ('hidden' in document) { return 'hidden'; }
for (const prefix of prefixes) {
if ((prefix + 'Hidden') in document) {
return prefix + 'Hidden';
}
}
return null;
}
isHidden() {
const prop = this.getHiddenProp();
if (!prop) { return false; }
return document[prop];
}
setBlockScrollingInProgress(value: boolean) {
this.blockScrolling$.next(value);
}
isLiquid() {
return this.network === 'liquid' || this.network === 'liquidtestnet';
}
isAnyTestnet(): boolean {
2024-05-06 15:40:32 +00:00
return ['testnet', 'testnet4', 'signet', 'liquidtestnet'].includes(this.network);
}
resetChainTip() {
this.latestBlockHeight = -1;
this.chainTip$.next(-1);
}
updateChainTip(height) {
if (height > this.latestBlockHeight) {
this.latestBlockHeight = height;
this.chainTip$.next(height);
}
}
2023-07-08 01:07:06 -04:00
resetBlocks(blocks: BlockExtended[]): void {
this.blocks = blocks.reverse();
2023-07-11 16:35:00 +09:00
this.blocksSubject$.next(blocks);
2023-07-08 01:07:06 -04:00
}
addBlock(block: BlockExtended): void {
this.blocks.unshift(block);
this.blocks = this.blocks.slice(0, this.env.KEEP_BLOCKS_AMOUNT);
2023-07-11 16:35:00 +09:00
this.blocksSubject$.next(this.blocks);
2023-07-08 01:07:06 -04:00
}
focusSearchInputDesktop() {
if (!hasTouchScreen()) {
this.searchFocus$.next(true);
}
}
}