-
+
+
+
+ P2PK
+
+
+
+
diff --git a/frontend/src/app/components/transactions-list/transactions-list.component.scss b/frontend/src/app/components/transactions-list/transactions-list.component.scss
index 08d7d7486..7356bad0b 100644
--- a/frontend/src/app/components/transactions-list/transactions-list.component.scss
+++ b/frontend/src/app/components/transactions-list/transactions-list.component.scss
@@ -140,6 +140,12 @@ h2 {
font-family: monospace;
}
+.p2pk-address {
+ display: inline-block;
+ margin-left: 1em;
+ max-width: 140px;
+}
+
.grey-info-text {
color:#6c757d;
font-style: italic;
diff --git a/frontend/src/app/interfaces/electrs.interface.ts b/frontend/src/app/interfaces/electrs.interface.ts
index 2739d2b06..df19f7491 100644
--- a/frontend/src/app/interfaces/electrs.interface.ts
+++ b/frontend/src/app/interfaces/electrs.interface.ts
@@ -129,6 +129,22 @@ export interface Address {
address: string;
chain_stats: ChainStats;
mempool_stats: MempoolStats;
+ is_pubkey?: boolean;
+}
+
+export interface ScriptHash {
+ electrum?: boolean;
+ scripthash: string;
+ chain_stats: ChainStats;
+ mempool_stats: MempoolStats;
+}
+
+export interface AddressOrScriptHash {
+ electrum?: boolean;
+ address?: string;
+ scripthash?: string;
+ chain_stats: ChainStats;
+ mempool_stats: MempoolStats;
}
export interface ChainStats {
diff --git a/frontend/src/app/services/electrs-api.service.ts b/frontend/src/app/services/electrs-api.service.ts
index c87018741..f866eb23d 100644
--- a/frontend/src/app/services/electrs-api.service.ts
+++ b/frontend/src/app/services/electrs-api.service.ts
@@ -1,9 +1,10 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
-import { Observable } from 'rxjs';
-import { Transaction, Address, Outspend, Recent, Asset } from '../interfaces/electrs.interface';
+import { Observable, from, of, switchMap } from 'rxjs';
+import { Transaction, Address, Outspend, Recent, Asset, ScriptHash } from '../interfaces/electrs.interface';
import { StateService } from './state.service';
import { BlockExtended } from '../interfaces/node-api.interface';
+import { calcScriptHash$ } from '../bitcoin.utils';
@Injectable({
providedIn: 'root'
@@ -65,6 +66,24 @@ export class ElectrsApiService {
return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/address/' + address);
}
+ getPubKeyAddress$(pubkey: string): Observable {
+ return this.getScriptHash$('41' + pubkey + 'ac').pipe(
+ switchMap((scripthash: ScriptHash) => {
+ return of({
+ ...scripthash,
+ address: pubkey,
+ is_pubkey: true,
+ });
+ })
+ );
+ }
+
+ getScriptHash$(script: string): Observable {
+ return from(calcScriptHash$(script)).pipe(
+ switchMap(scriptHash => this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/scripthash/' + scriptHash))
+ );
+ }
+
getAddressTransactions$(address: string, txid?: string): Observable {
let params = new HttpParams();
if (txid) {
@@ -73,6 +92,16 @@ export class ElectrsApiService {
return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/address/' + address + '/txs', { params });
}
+ getScriptHashTransactions$(script: string, txid?: string): Observable {
+ let params = new HttpParams();
+ if (txid) {
+ params = params.append('after_txid', txid);
+ }
+ return from(calcScriptHash$(script)).pipe(
+ switchMap(scriptHash => this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/scripthash/' + scriptHash + '/txs', { params })),
+ );
+ }
+
getAsset$(assetId: string): Observable {
return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/asset/' + assetId);
}
From 0ce043cca9f63c51e3b9bf4685ce5fffc33f200d Mon Sep 17 00:00:00 2001
From: Mononaut
Date: Sun, 23 Jul 2023 13:55:27 +0900
Subject: [PATCH 026/105] Fix esplora error messages
---
backend/src/api/bitcoin/esplora-api.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/backend/src/api/bitcoin/esplora-api.ts b/backend/src/api/bitcoin/esplora-api.ts
index 01294cc01..5bfff5730 100644
--- a/backend/src/api/bitcoin/esplora-api.ts
+++ b/backend/src/api/bitcoin/esplora-api.ts
@@ -111,11 +111,11 @@ class ElectrsApi implements AbstractBitcoinApi {
}
$getScriptHash(scripthash: string): Promise {
- throw new Error('Method getAddress not implemented.');
+ throw new Error('Method getScriptHash not implemented.');
}
$getScriptHashTransactions(scripthash: string, txId?: string): Promise {
- throw new Error('Method getAddressTransactions not implemented.');
+ throw new Error('Method getScriptHashTransactions not implemented.');
}
$getAddressPrefix(prefix: string): string[] {
From 48b55eed468d9515e82518ebb31d5c41fc1080a4 Mon Sep 17 00:00:00 2001
From: Mononaut
Date: Sun, 23 Jul 2023 13:55:52 +0900
Subject: [PATCH 027/105] improve script hex parsing validation
---
frontend/src/app/bitcoin.utils.ts | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/frontend/src/app/bitcoin.utils.ts b/frontend/src/app/bitcoin.utils.ts
index 7ff0d9570..c4af730f6 100644
--- a/frontend/src/app/bitcoin.utils.ts
+++ b/frontend/src/app/bitcoin.utils.ts
@@ -283,7 +283,10 @@ export function isFeatureActive(network: string, height: number, feature: 'rbf'
}
export async function calcScriptHash$(script: string): Promise {
- const buf = Uint8Array.from(script.match(/.{1,2}/g).map((byte) => parseInt(byte, 16)));
+ if (!/^[0-9a-fA-F]*$/.test(script) || script.length % 2 !== 0) {
+ throw new Error('script is not a valid hex string');
+ }
+ const buf = Uint8Array.from(script.match(/.{2}/g).map((byte) => parseInt(byte, 16)));
const hashBuffer = await crypto.subtle.digest('SHA-256', buf);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray
From 0376467e6c9c211b7357cf61f0e1903cc631dbaa Mon Sep 17 00:00:00 2001
From: Mononaut
Date: Sun, 23 Jul 2023 14:00:39 +0900
Subject: [PATCH 028/105] highlight matching P2PK inputs
---
.../transactions-list/transactions-list.component.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frontend/src/app/components/transactions-list/transactions-list.component.html b/frontend/src/app/components/transactions-list/transactions-list.component.html
index 3f88c61b0..d1d0673fe 100644
--- a/frontend/src/app/components/transactions-list/transactions-list.component.html
+++ b/frontend/src/app/components/transactions-list/transactions-list.component.html
@@ -23,7 +23,7 @@
From 56127dce6a19039c126858953a28fcb2eb475ae3 Mon Sep 17 00:00:00 2001
From: Mononaut
Date: Sun, 23 Jul 2023 14:05:04 +0900
Subject: [PATCH 029/105] Add P2PK support to search bar
---
.../src/app/components/search-form/search-form.component.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frontend/src/app/components/search-form/search-form.component.ts b/frontend/src/app/components/search-form/search-form.component.ts
index ab42fe1f7..2361f8873 100644
--- a/frontend/src/app/components/search-form/search-form.component.ts
+++ b/frontend/src/app/components/search-form/search-form.component.ts
@@ -34,7 +34,7 @@ export class SearchFormComponent implements OnInit {
}
}
- regexAddress = /^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[A-z]{2,5}1[a-zA-HJ-NP-Z0-9]{39,59})$/;
+ regexAddress = /^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[A-z]{2,5}1[a-zA-HJ-NP-Z0-9]{39,59}|[0-9a-fA-F]{130})$/;
regexBlockhash = /^[0]{8}[a-fA-F0-9]{56}$/;
regexTransaction = /^([a-fA-F0-9]{64})(:\d+)?$/;
regexBlockheight = /^[0-9]{1,9}$/;
From ae183210e036b0c9455d21abb5d922cd5e70f083 Mon Sep 17 00:00:00 2001
From: softsimon
Date: Sun, 23 Jul 2023 14:43:43 +0900
Subject: [PATCH 030/105] Updating pubkey width on mobile and desktop
---
.../transactions-list/transactions-list.component.scss | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/frontend/src/app/components/transactions-list/transactions-list.component.scss b/frontend/src/app/components/transactions-list/transactions-list.component.scss
index 7356bad0b..14559089a 100644
--- a/frontend/src/app/components/transactions-list/transactions-list.component.scss
+++ b/frontend/src/app/components/transactions-list/transactions-list.component.scss
@@ -143,7 +143,10 @@ h2 {
.p2pk-address {
display: inline-block;
margin-left: 1em;
- max-width: 140px;
+ max-width: 100px;
+ @media (min-width: 576px) {
+ max-width: 200px
+ }
}
.grey-info-text {
From 05affa5ad4320f0b2bc4475f4c483a497ab22f15 Mon Sep 17 00:00:00 2001
From: Mononaut
Date: Sun, 23 Jul 2023 16:19:48 +0900
Subject: [PATCH 031/105] Fix difficulty tooltip position
---
.../app/components/difficulty/difficulty.component.ts | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/frontend/src/app/components/difficulty/difficulty.component.ts b/frontend/src/app/components/difficulty/difficulty.component.ts
index d3983c939..a2c03dc56 100644
--- a/frontend/src/app/components/difficulty/difficulty.component.ts
+++ b/frontend/src/app/components/difficulty/difficulty.component.ts
@@ -1,4 +1,4 @@
-import { ChangeDetectionStrategy, Component, HostListener, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
+import { ChangeDetectionStrategy, ChangeDetectorRef, Component, HostListener, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
import { combineLatest, Observable, timer } from 'rxjs';
import { map, switchMap } from 'rxjs/operators';
import { StateService } from '../..//services/state.service';
@@ -61,6 +61,7 @@ export class DifficultyComponent implements OnInit {
constructor(
public stateService: StateService,
+ private cd: ChangeDetectorRef,
@Inject(LOCALE_ID) private locale: string,
) { }
@@ -189,9 +190,15 @@ export class DifficultyComponent implements OnInit {
return shapes;
}
+ @HostListener('pointerdown', ['$event'])
+ onPointerDown(event) {
+ this.onPointerMove(event);
+ }
+
@HostListener('pointermove', ['$event'])
onPointerMove(event) {
this.tooltipPosition = { x: event.clientX, y: event.clientY };
+ this.cd.markForCheck();
}
onHover(event, rect): void {
From a1e05c0c37bdf848c51b502cd1286bc9117120fa Mon Sep 17 00:00:00 2001
From: Mononaut
Date: Sun, 23 Jul 2023 17:45:01 +0900
Subject: [PATCH 032/105] Lightning channel balance progress bars
---
.../channel-close-box.component.html | 58 ++++++++----
.../channel-close-box.component.scss | 94 +++++++++++++++++++
.../channel-close-box.component.ts | 70 +++++++++++---
.../lightning/channel/channel.component.html | 4 +-
4 files changed, 195 insertions(+), 31 deletions(-)
diff --git a/frontend/src/app/lightning/channel/channel-close-box/channel-close-box.component.html b/frontend/src/app/lightning/channel/channel-close-box/channel-close-box.component.html
index b5615324b..08a341de4 100644
--- a/frontend/src/app/lightning/channel/channel-close-box/channel-close-box.component.html
+++ b/frontend/src/app/lightning/channel/channel-close-box/channel-close-box.component.html
@@ -1,19 +1,43 @@
-
-
-
-
- | Starting balance |
- |
- {{ minStartingBalance | number : '1.0-0' }} - {{ maxStartingBalance | number : '1.0-0' }} |
- ? |
-
-
- | Closing balance |
- |
- {{ minClosingBalance | number : '1.0-0' }} - {{ maxClosingBalance | number : '1.0-0' }} |
- ? |
-
-
-
+
+ Starting balance
+
+ {{ left.alias }}
+ {{ right.alias }}
+
+
+
+ {{ minStartingBalance | number : '1.0-0' }} - {{ maxStartingBalance | number : '1.0-0' }}
+ {{ minStartingBalance | number : '1.0-0' }}
+
+
+ {{ channel.capacity - maxStartingBalance | number : '1.0-0' }} - {{ channel.capacity - minStartingBalance | number : '1.0-0' }}
+ {{ channel.capacity - maxStartingBalance | number : '1.0-0' }}
+
+
+
+
+
+
+ Closing balance
+
+
+ {{ minClosingBalance | number : '1.0-0' }} - {{ maxClosingBalance | number : '1.0-0' }}
+ {{ minClosingBalance | number : '1.0-0' }}
+
+
+ {{ channel.capacity - maxClosingBalance | number : '1.0-0' }} - {{ channel.capacity - minClosingBalance | number : '1.0-0' }}
+ {{ channel.capacity - maxClosingBalance | number : '1.0-0' }}
+
+
+
+
\ No newline at end of file
diff --git a/frontend/src/app/lightning/channel/channel-close-box/channel-close-box.component.scss b/frontend/src/app/lightning/channel/channel-close-box/channel-close-box.component.scss
index a42871308..f55550eb3 100644
--- a/frontend/src/app/lightning/channel/channel-close-box/channel-close-box.component.scss
+++ b/frontend/src/app/lightning/channel/channel-close-box/channel-close-box.component.scss
@@ -6,4 +6,98 @@
.box {
margin-bottom: 20px;
}
+}
+
+.starting-balance, .closing-balance {
+ width: 100%;
+
+ h5 {
+ text-align: center;
+ }
+}
+
+.nodes {
+ display: none;
+ flex-direction: row;
+ align-items: baseline;
+ justify-content: space-between;
+
+ @media (max-width: 768px) {
+ display: flex;
+ }
+}
+
+.balances {
+ display: flex;
+ flex-direction: row;
+ align-items: baseline;
+ justify-content: space-between;
+ margin-bottom: 8px;
+
+ .balance {
+ &.left {
+ text-align: start;
+ }
+ &.right {
+ text-align: end;
+ }
+ }
+}
+
+.balance-bar {
+ width: 100%;
+ height: 2em;
+ position: relative;
+
+ .bar {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ height: 100%;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: center;
+
+ &.left {
+ background: #105fb0;
+ }
+ &.center {
+ background: repeating-linear-gradient(
+ 60deg,
+ #105fb0 0,
+ #105fb0 12px,
+ #1a9436 12px,
+ #1a9436 24px
+ );
+ }
+ &.right {
+ background: #1a9436;
+ }
+
+ .value {
+ flex: 0;
+ white-space: nowrap;
+ }
+
+ &.hide-value {
+ .value {
+ display: none;
+ }
+ }
+ }
+
+ @media (max-width: 768px) {
+ height: 1em;
+
+ .bar.center {
+ background: repeating-linear-gradient(
+ 60deg,
+ #105fb0 0,
+ #105fb0 8px,
+ #1a9436 8px,
+ #1a9436 16px
+ )
+ }
+ }
}
\ No newline at end of file
diff --git a/frontend/src/app/lightning/channel/channel-close-box/channel-close-box.component.ts b/frontend/src/app/lightning/channel/channel-close-box/channel-close-box.component.ts
index 05cc31434..ef42464eb 100644
--- a/frontend/src/app/lightning/channel/channel-close-box/channel-close-box.component.ts
+++ b/frontend/src/app/lightning/channel/channel-close-box/channel-close-box.component.ts
@@ -8,8 +8,8 @@ import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } f
})
export class ChannelCloseBoxComponent implements OnChanges {
@Input() channel: any;
- @Input() local: any;
- @Input() remote: any;
+ @Input() left: any;
+ @Input() right: any;
showStartingBalance: boolean = false;
showClosingBalance: boolean = false;
@@ -18,29 +18,55 @@ export class ChannelCloseBoxComponent implements OnChanges {
minClosingBalance: number;
maxClosingBalance: number;
+ startingBalanceStyle: {
+ left: string,
+ center: string,
+ right: string,
+ } = {
+ left: '',
+ center: '',
+ right: '',
+ };
+
+ closingBalanceStyle: {
+ left: string,
+ center: string,
+ right: string,
+ } = {
+ left: '',
+ center: '',
+ right: '',
+ };
+
+ hideStartingLeft: boolean = false;
+ hideStartingRight: boolean = false;
+ hideClosingLeft: boolean = false;
+ hideClosingRight: boolean = false;
+
constructor() { }
ngOnChanges(changes: SimpleChanges): void {
- if (this.channel && this.local && this.remote) {
- this.showStartingBalance = (this.local.funding_balance || this.remote.funding_balance) && this.channel.funding_ratio;
- this.showClosingBalance = this.local.closing_balance || this.remote.closing_balance;
+ let closingCapacity;
+ if (this.channel && this.left && this.right) {
+ this.showStartingBalance = (this.left.funding_balance || this.right.funding_balance) && this.channel.funding_ratio;
+ this.showClosingBalance = this.left.closing_balance || this.right.closing_balance;
if (this.channel.single_funded) {
- if (this.local.funding_balance) {
+ if (this.left.funding_balance) {
this.minStartingBalance = this.channel.capacity;
this.maxStartingBalance = this.channel.capacity;
- } else if (this.remote.funding_balance) {
+ } else if (this.right.funding_balance) {
this.minStartingBalance = 0;
this.maxStartingBalance = 0;
}
} else {
- this.minStartingBalance = clampRound(0, this.channel.capacity, this.local.funding_balance * this.channel.funding_ratio);
- this.maxStartingBalance = clampRound(0, this.channel.capacity, this.channel.capacity - (this.remote.funding_balance * this.channel.funding_ratio));
+ this.minStartingBalance = clampRound(0, this.channel.capacity, this.left.funding_balance * this.channel.funding_ratio);
+ this.maxStartingBalance = clampRound(0, this.channel.capacity, this.channel.capacity - (this.right.funding_balance * this.channel.funding_ratio));
}
- const closingCapacity = this.channel.capacity - this.channel.closing_fee;
- this.minClosingBalance = clampRound(0, closingCapacity, this.local.closing_balance);
- this.maxClosingBalance = clampRound(0, closingCapacity, closingCapacity - this.remote.closing_balance);
+ closingCapacity = this.channel.capacity - this.channel.closing_fee;
+ this.minClosingBalance = clampRound(0, closingCapacity, this.left.closing_balance);
+ this.maxClosingBalance = clampRound(0, closingCapacity, closingCapacity - this.right.closing_balance);
// margin of error to account for 2 x 330 sat anchor outputs
if (Math.abs(this.minClosingBalance - this.maxClosingBalance) <= 660) {
@@ -50,6 +76,26 @@ export class ChannelCloseBoxComponent implements OnChanges {
this.showStartingBalance = false;
this.showClosingBalance = false;
}
+
+ const startingMinPc = (this.minStartingBalance / this.channel.capacity) * 100;
+ const startingMaxPc = (this.maxStartingBalance / this.channel.capacity) * 100;
+ this.startingBalanceStyle = {
+ left: `left: 0%; right: ${100 - startingMinPc}%;`,
+ center: `left: ${startingMinPc}%; right: ${100 -startingMaxPc}%;`,
+ right: `left: ${startingMaxPc}%; right: 0%;`,
+ };
+ this.hideStartingLeft = startingMinPc < 15;
+ this.hideStartingRight = startingMaxPc > 85;
+
+ const closingMinPc = (this.minClosingBalance / closingCapacity) * 100;
+ const closingMaxPc = (this.maxClosingBalance / closingCapacity) * 100;
+ this.closingBalanceStyle = {
+ left: `left: 0%; right: ${100 - closingMinPc}%;`,
+ center: `left: ${closingMinPc}%; right: ${100 - closingMaxPc}%;`,
+ right: `left: ${closingMaxPc}%; right: 0%;`,
+ };
+ this.hideClosingLeft = closingMinPc < 15;
+ this.hideClosingRight = closingMaxPc > 85;
}
}
diff --git a/frontend/src/app/lightning/channel/channel.component.html b/frontend/src/app/lightning/channel/channel.component.html
index 2766f1d15..b9d9e09a4 100644
--- a/frontend/src/app/lightning/channel/channel.component.html
+++ b/frontend/src/app/lightning/channel/channel.component.html
@@ -75,14 +75,14 @@
+
+
From 02f361af7334ec8e0df6206c15d328fa7807f5cf Mon Sep 17 00:00:00 2001
From: wiz
Date: Sun, 23 Jul 2023 22:21:53 +0900
Subject: [PATCH 033/105] Hotfix for CLN crash
---
backend/src/api/lightning/clightning/clightning-convert.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/backend/src/api/lightning/clightning/clightning-convert.ts b/backend/src/api/lightning/clightning/clightning-convert.ts
index 771dabcd7..02854a79b 100644
--- a/backend/src/api/lightning/clightning/clightning-convert.ts
+++ b/backend/src/api/lightning/clightning/clightning-convert.ts
@@ -257,8 +257,8 @@ async function buildIncompleteChannel(clChannel: any): Promise
Date: Sun, 23 Jul 2023 22:35:32 +0900
Subject: [PATCH 034/105] Another hotfix for CLN crash
---
backend/src/api/lightning/clightning/clightning-convert.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/backend/src/api/lightning/clightning/clightning-convert.ts b/backend/src/api/lightning/clightning/clightning-convert.ts
index 02854a79b..55e4bd213 100644
--- a/backend/src/api/lightning/clightning/clightning-convert.ts
+++ b/backend/src/api/lightning/clightning/clightning-convert.ts
@@ -217,7 +217,7 @@ async function buildFullChannel(clChannelA: any, clChannelB: any): Promise
Date: Mon, 24 Jul 2023 10:18:00 +0900
Subject: [PATCH 035/105] [search bar] auto focus only in dashboards
---
.../mining-dashboard.component.ts | 10 +++++++--
.../search-form/search-form.component.html | 2 +-
.../search-form/search-form.component.ts | 21 ++++++++++++++++---
.../src/app/dashboard/dashboard.component.ts | 10 ++++++---
frontend/src/app/services/state.service.ts | 2 ++
5 files changed, 36 insertions(+), 9 deletions(-)
diff --git a/frontend/src/app/components/mining-dashboard/mining-dashboard.component.ts b/frontend/src/app/components/mining-dashboard/mining-dashboard.component.ts
index df4713374..22d0e11fe 100644
--- a/frontend/src/app/components/mining-dashboard/mining-dashboard.component.ts
+++ b/frontend/src/app/components/mining-dashboard/mining-dashboard.component.ts
@@ -1,6 +1,7 @@
-import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
+import { AfterViewChecked, ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { SeoService } from '../../services/seo.service';
import { WebsocketService } from '../../services/websocket.service';
+import { StateService } from '../../services/state.service';
@Component({
selector: 'app-mining-dashboard',
@@ -8,10 +9,11 @@ import { WebsocketService } from '../../services/websocket.service';
styleUrls: ['./mining-dashboard.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
-export class MiningDashboardComponent implements OnInit {
+export class MiningDashboardComponent implements OnInit, AfterViewChecked {
constructor(
private seoService: SeoService,
private websocketService: WebsocketService,
+ private stateService: StateService
) {
this.seoService.setTitle($localize`:@@a681a4e2011bb28157689dbaa387de0dd0aa0c11:Mining Dashboard`);
}
@@ -19,4 +21,8 @@ export class MiningDashboardComponent implements OnInit {
ngOnInit(): void {
this.websocketService.want(['blocks', 'mempool-blocks', 'stats']);
}
+
+ ngAfterViewChecked(): void {
+ this.stateService.searchFocus$.next(true);
+ }
}
diff --git a/frontend/src/app/components/search-form/search-form.component.html b/frontend/src/app/components/search-form/search-form.component.html
index cdfcfe015..3fc03c83a 100644
--- a/frontend/src/app/components/search-form/search-form.component.html
+++ b/frontend/src/app/components/search-form/search-form.component.html
@@ -1,7 +1,7 @@
|
diff --git a/frontend/src/app/components/block/block.component.ts b/frontend/src/app/components/block/block.component.ts
index ce3317255..ec9a49504 100644
--- a/frontend/src/app/components/block/block.component.ts
+++ b/frontend/src/app/components/block/block.component.ts
@@ -339,7 +339,7 @@ export class BlockComponent implements OnInit, OnDestroy {
const isSelected = {};
const isFresh = {};
const isSigop = {};
- const isFullRbf = {};
+ const isRbf = {};
this.numMissing = 0;
this.numUnexpected = 0;
@@ -363,7 +363,7 @@ export class BlockComponent implements OnInit, OnDestroy {
isSigop[txid] = true;
}
for (const txid of blockAudit.fullrbfTxs || []) {
- isFullRbf[txid] = true;
+ isRbf[txid] = true;
}
// set transaction statuses
for (const tx of blockAudit.template) {
@@ -381,8 +381,8 @@ export class BlockComponent implements OnInit, OnDestroy {
}
} else if (isSigop[tx.txid]) {
tx.status = 'sigop';
- } else if (isFullRbf[tx.txid]) {
- tx.status = 'fullrbf';
+ } else if (isRbf[tx.txid]) {
+ tx.status = 'rbf';
} else {
tx.status = 'missing';
}
@@ -398,8 +398,8 @@ export class BlockComponent implements OnInit, OnDestroy {
tx.status = 'added';
} else if (inTemplate[tx.txid]) {
tx.status = 'found';
- } else if (isFullRbf[tx.txid]) {
- tx.status = 'fullrbf';
+ } else if (isRbf[tx.txid]) {
+ tx.status = 'rbf';
} else {
tx.status = 'selected';
isSelected[tx.txid] = true;
diff --git a/frontend/src/app/interfaces/node-api.interface.ts b/frontend/src/app/interfaces/node-api.interface.ts
index 2b434c44d..4249fd9db 100644
--- a/frontend/src/app/interfaces/node-api.interface.ts
+++ b/frontend/src/app/interfaces/node-api.interface.ts
@@ -174,7 +174,7 @@ export interface TransactionStripped {
vsize: number;
value: number;
rate?: number; // effective fee rate
- status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'fullrbf';
+ status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'rbf';
context?: 'projected' | 'actual';
}
diff --git a/frontend/src/app/interfaces/websocket.interface.ts b/frontend/src/app/interfaces/websocket.interface.ts
index 15d97fa8d..e0ecdfeda 100644
--- a/frontend/src/app/interfaces/websocket.interface.ts
+++ b/frontend/src/app/interfaces/websocket.interface.ts
@@ -89,7 +89,7 @@ export interface TransactionStripped {
vsize: number;
value: number;
rate?: number; // effective fee rate
- status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'fullrbf';
+ status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'rbf';
context?: 'projected' | 'actual';
}
From 07b0f24cf15d2e72a3b0458969278733addcb9dc Mon Sep 17 00:00:00 2001
From: softsimon
Date: Tue, 25 Jul 2023 14:26:43 +0900
Subject: [PATCH 050/105] Update
frontend/src/app/shared/pipes/bytes-pipe/utils.ts
---
frontend/src/app/shared/pipes/bytes-pipe/utils.ts | 1 -
1 file changed, 1 deletion(-)
diff --git a/frontend/src/app/shared/pipes/bytes-pipe/utils.ts b/frontend/src/app/shared/pipes/bytes-pipe/utils.ts
index 86a1e1a1d..2700be45d 100644
--- a/frontend/src/app/shared/pipes/bytes-pipe/utils.ts
+++ b/frontend/src/app/shared/pipes/bytes-pipe/utils.ts
@@ -332,6 +332,5 @@ export function hasTouchScreen(): boolean {
/\b(Android|Windows Phone|iPad|iPod)\b/i.test(UA);
}
}
- console.log(hasTouchScreen);
return hasTouchScreen;
}
\ No newline at end of file
From 6d5be78dd06d2b13a6636201eb1d1cb960e50a2f Mon Sep 17 00:00:00 2001
From: nymkappa <1612910616@pm.me>
Date: Tue, 25 Jul 2023 15:03:39 +0900
Subject: [PATCH 051/105] [search bar] use afterviewinit instead of
afterviewchecked
---
.../mining-dashboard.component.ts | 17 +++++++++++++----
.../search-form/search-form.component.ts | 8 +++++---
.../src/app/dashboard/dashboard.component.ts | 6 +++---
.../lightning-dashboard.component.ts | 6 +++---
4 files changed, 24 insertions(+), 13 deletions(-)
diff --git a/frontend/src/app/components/mining-dashboard/mining-dashboard.component.ts b/frontend/src/app/components/mining-dashboard/mining-dashboard.component.ts
index c7670bc1e..6353ab8b8 100644
--- a/frontend/src/app/components/mining-dashboard/mining-dashboard.component.ts
+++ b/frontend/src/app/components/mining-dashboard/mining-dashboard.component.ts
@@ -1,7 +1,8 @@
-import { AfterViewChecked, ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
+import { AfterViewInit, ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { SeoService } from '../../services/seo.service';
import { WebsocketService } from '../../services/websocket.service';
import { StateService } from '../../services/state.service';
+import { EventType, NavigationStart, Router } from '@angular/router';
@Component({
selector: 'app-mining-dashboard',
@@ -9,11 +10,12 @@ import { StateService } from '../../services/state.service';
styleUrls: ['./mining-dashboard.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
-export class MiningDashboardComponent implements OnInit, AfterViewChecked {
+export class MiningDashboardComponent implements OnInit, AfterViewInit {
constructor(
private seoService: SeoService,
private websocketService: WebsocketService,
- private stateService: StateService
+ private stateService: StateService,
+ private router: Router
) {
this.seoService.setTitle($localize`:@@a681a4e2011bb28157689dbaa387de0dd0aa0c11:Mining Dashboard`);
}
@@ -22,7 +24,14 @@ export class MiningDashboardComponent implements OnInit, AfterViewChecked {
this.websocketService.want(['blocks', 'mempool-blocks', 'stats']);
}
- ngAfterViewChecked(): void {
+ ngAfterViewInit(): void {
this.stateService.focusSearchInputDesktop();
+ this.router.events.subscribe((e: NavigationStart) => {
+ if (e.type === EventType.NavigationStart) {
+ if (e.url.indexOf('graphs') === -1) { // The mining dashboard and the graph component are part of the same module so we can't use ngAfterViewInit in graphs.component.ts to blur the input
+ this.stateService.focusSearchInputDesktop();
+ }
+ }
+ });
}
}
diff --git a/frontend/src/app/components/search-form/search-form.component.ts b/frontend/src/app/components/search-form/search-form.component.ts
index 2fc25748e..61b3351b7 100644
--- a/frontend/src/app/components/search-form/search-form.component.ts
+++ b/frontend/src/app/components/search-form/search-form.component.ts
@@ -65,13 +65,15 @@ export class SearchFormComponent implements OnInit {
this.stateService.networkChanged$.subscribe((network) => this.network = network);
this.router.events.subscribe((e: NavigationStart) => { // Reset search focus when changing page
- if (e.type === EventType.NavigationStart) {
+ if (this.searchInput && e.type === EventType.NavigationStart) {
this.searchInput.nativeElement.blur();
}
});
- this.stateService.searchFocus$.subscribe(focus => {
- if (this.searchInput && focus === true) {
+ this.stateService.searchFocus$.subscribe(() => {
+ if (!this.searchInput) { // Try again a bit later once the view is properly initialized
+ setTimeout(() => this.searchInput.nativeElement.focus(), 100);
+ } else if (this.searchInput) {
this.searchInput.nativeElement.focus();
}
});
diff --git a/frontend/src/app/dashboard/dashboard.component.ts b/frontend/src/app/dashboard/dashboard.component.ts
index 6d61953cf..05381453d 100644
--- a/frontend/src/app/dashboard/dashboard.component.ts
+++ b/frontend/src/app/dashboard/dashboard.component.ts
@@ -1,4 +1,4 @@
-import { AfterViewChecked, ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
+import { AfterViewInit, ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
import { combineLatest, merge, Observable, of, Subscription } from 'rxjs';
import { filter, map, scan, share, switchMap, tap } from 'rxjs/operators';
import { BlockExtended, OptimizedMempoolStats } from '../interfaces/node-api.interface';
@@ -31,7 +31,7 @@ interface MempoolStatsData {
styleUrls: ['./dashboard.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
-export class DashboardComponent implements OnInit, OnDestroy, AfterViewChecked {
+export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
featuredAssets$: Observable;
network$: Observable;
mempoolBlocksData$: Observable;
@@ -57,7 +57,7 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewChecked {
private seoService: SeoService
) { }
- ngAfterViewChecked(): void {
+ ngAfterViewInit(): void {
this.stateService.focusSearchInputDesktop();
}
diff --git a/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.ts b/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.ts
index adaa8d115..e58d5f124 100644
--- a/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.ts
+++ b/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.ts
@@ -1,4 +1,4 @@
-import { AfterViewChecked, ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
+import { AfterViewInit, ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { share } from 'rxjs/operators';
import { INodesRanking } from '../../interfaces/node-api.interface';
@@ -12,7 +12,7 @@ import { LightningApiService } from '../lightning-api.service';
styleUrls: ['./lightning-dashboard.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
-export class LightningDashboardComponent implements OnInit, AfterViewChecked {
+export class LightningDashboardComponent implements OnInit, AfterViewInit {
statistics$: Observable;
nodesRanking$: Observable;
officialMempoolSpace = this.stateService.env.OFFICIAL_MEMPOOL_SPACE;
@@ -30,7 +30,7 @@ export class LightningDashboardComponent implements OnInit, AfterViewChecked {
this.statistics$ = this.lightningApiService.getLatestStatistics$().pipe(share());
}
- ngAfterViewChecked(): void {
+ ngAfterViewInit(): void {
this.stateService.focusSearchInputDesktop();
}
}
From e15c0c6c7a62b9d85bd6c0e5d6c02e64d8509d36 Mon Sep 17 00:00:00 2001
From: Mononaut
Date: Tue, 25 Jul 2023 21:18:19 +0900
Subject: [PATCH 052/105] Fix key navigation subscription leak
---
.../mempool-blocks.component.ts | 21 +++++++++++--------
1 file changed, 12 insertions(+), 9 deletions(-)
diff --git a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts
index 71075b261..cedcf03f4 100644
--- a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts
+++ b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts
@@ -50,6 +50,8 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
blockSubscription: Subscription;
networkSubscription: Subscription;
chainTipSubscription: Subscription;
+ keySubscription: Subscription;
+ isTabHiddenSubscription: Subscription;
network = '';
now = new Date().getTime();
timeOffset = 0;
@@ -116,7 +118,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
this.calculateTransactionPosition();
});
this.reduceMempoolBlocksToFitScreen(this.mempoolBlocks);
- this.stateService.isTabHidden$.subscribe((tabHidden) => this.tabHidden = tabHidden);
+ this.isTabHiddenSubscription = this.stateService.isTabHidden$.subscribe((tabHidden) => this.tabHidden = tabHidden);
this.loadingBlocks$ = combineLatest([
this.stateService.isLoadingWebSocket$,
this.stateService.isLoadingMempool$
@@ -224,7 +226,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
this.networkSubscription = this.stateService.networkChanged$
.subscribe((network) => this.network = network);
- this.stateService.keyNavigation$.subscribe((event) => {
+ this.keySubscription = this.stateService.keyNavigation$.subscribe((event) => {
if (this.markIndex === undefined) {
return;
}
@@ -235,13 +237,12 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
if (this.mempoolBlocks[this.markIndex - 1]) {
this.router.navigate([this.relativeUrlPipe.transform('mempool-block/'), this.markIndex - 1]);
} else {
- this.stateService.blocks$
- .pipe(map((blocks) => blocks[0]))
- .subscribe((block) => {
- if (this.stateService.latestBlockHeight === block.height) {
- this.router.navigate([this.relativeUrlPipe.transform('/block/'), block.id], { state: { data: { block } }});
- }
- });
+ const blocks = this.stateService.blocksSubject$.getValue();
+ for (const block of (blocks || [])) {
+ if (this.stateService.latestBlockHeight === block.height) {
+ this.router.navigate([this.relativeUrlPipe.transform('/block/'), block.id], { state: { data: { block } }});
+ }
+ }
}
} else if (event.key === nextKey) {
if (this.mempoolBlocks[this.markIndex + 1]) {
@@ -265,6 +266,8 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
this.networkSubscription.unsubscribe();
this.timeLtrSubscription.unsubscribe();
this.chainTipSubscription.unsubscribe();
+ this.keySubscription.unsubscribe();
+ this.isTabHiddenSubscription.unsubscribe();
clearTimeout(this.resetTransitionTimeout);
}
From 1fd5b975f152c085e21a64f86ccc205a560e2884 Mon Sep 17 00:00:00 2001
From: Mononaut
Date: Thu, 27 Jul 2023 11:45:16 +0900
Subject: [PATCH 053/105] Handle failures while fetching block transactions
---
backend/src/api/blocks.ts | 64 +++++++++++++++++++---------
backend/src/api/transaction-utils.ts | 20 ++++++++-
2 files changed, 62 insertions(+), 22 deletions(-)
diff --git a/backend/src/api/blocks.ts b/backend/src/api/blocks.ts
index 4dbf4305e..9ad9278d0 100644
--- a/backend/src/api/blocks.ts
+++ b/backend/src/api/blocks.ts
@@ -105,11 +105,16 @@ class Blocks {
}
}
- // Skip expensive lookups while mempool has priority
if (onlyCoinbase) {
try {
- const coinbase = await transactionUtils.$getTransactionExtended(txIds[0], false, false, false, addMempoolData);
- return [coinbase];
+ const coinbase = await transactionUtils.$getTransactionExtendedRetry(txIds[0], false, false, false, addMempoolData);
+ if (coinbase && coinbase.vin[0].is_coinbase) {
+ return [coinbase];
+ } else {
+ const msg = `Expected a coinbase tx, but the backend API returned something else`;
+ logger.err(msg);
+ throw new Error(msg);
+ }
} catch (e) {
const msg = `Cannot fetch coinbase tx ${txIds[0]}. Reason: ` + (e instanceof Error ? e.message : e);
logger.err(msg);
@@ -134,17 +139,17 @@ class Blocks {
// Fetch remaining txs individually
for (const txid of txIds.filter(txid => !transactionMap[txid])) {
- if (!transactionMap[txid]) {
- if (!quiet && (totalFound % (Math.round((txIds.length) / 10)) === 0 || totalFound + 1 === txIds.length)) { // Avoid log spam
- logger.debug(`Indexing tx ${totalFound + 1} of ${txIds.length} in block #${blockHeight}`);
- }
- try {
- const tx = await transactionUtils.$getTransactionExtended(txid, false, false, false, addMempoolData);
- transactionMap[txid] = tx;
- totalFound++;
- } catch (e) {
- logger.err(`Cannot fetch tx ${txid}. Reason: ` + (e instanceof Error ? e.message : e));
- }
+ if (!quiet && (totalFound % (Math.round((txIds.length) / 10)) === 0 || totalFound + 1 === txIds.length)) { // Avoid log spam
+ logger.debug(`Indexing tx ${totalFound + 1} of ${txIds.length} in block #${blockHeight}`);
+ }
+ try {
+ const tx = await transactionUtils.$getTransactionExtendedRetry(txid, false, false, false, addMempoolData);
+ transactionMap[txid] = tx;
+ totalFound++;
+ } catch (e) {
+ const msg = `Cannot fetch tx ${txid}. Reason: ` + (e instanceof Error ? e.message : e);
+ logger.err(msg);
+ throw new Error(msg);
}
}
@@ -152,8 +157,25 @@ class Blocks {
logger.debug(`${foundInMempool} of ${txIds.length} found in mempool. ${totalFound - foundInMempool} fetched through backend service.`);
}
+ // Require the first transaction to be a coinbase
+ const coinbase = transactionMap[txIds[0]];
+ if (!coinbase || !coinbase.vin[0].is_coinbase) {
+ console.log(coinbase);
+ const msg = `Expected first tx in a block to be a coinbase, but found something else`;
+ logger.err(msg);
+ throw new Error(msg);
+ }
+
+ // Require all transactions to be present
+ // (we should have thrown an error already if a tx request failed)
+ if (txIds.some(txid => !transactionMap[txid])) {
+ const msg = `Failed to fetch ${txIds.length - totalFound} transactions from block`;
+ logger.err(msg);
+ throw new Error(msg);
+ }
+
// Return list of transactions, preserving block order
- return txIds.map(txid => transactionMap[txid]).filter(tx => tx != null);
+ return txIds.map(txid => transactionMap[txid]);
}
/**
@@ -667,14 +689,14 @@ class Blocks {
const block = BitcoinApi.convertBlock(verboseBlock);
const txIds: string[] = verboseBlock.tx.map(tx => tx.txid);
const transactions = await this.$getTransactionsExtended(blockHash, block.height, false, txIds, false, true) as MempoolTransactionExtended[];
- if (config.MEMPOOL.BACKEND !== 'esplora') {
- // fill in missing transaction fee data from verboseBlock
- for (let i = 0; i < transactions.length; i++) {
- if (!transactions[i].fee && transactions[i].txid === verboseBlock.tx[i].txid) {
- transactions[i].fee = verboseBlock.tx[i].fee * 100_000_000;
- }
+
+ // fill in missing transaction fee data from verboseBlock
+ for (let i = 0; i < transactions.length; i++) {
+ if (!transactions[i].fee && transactions[i].txid === verboseBlock.tx[i].txid) {
+ transactions[i].fee = verboseBlock.tx[i].fee * 100_000_000;
}
}
+
const cpfpSummary: CpfpSummary = Common.calculateCpfp(block.height, transactions);
const blockExtended: BlockExtended = await this.$getBlockExtended(block, cpfpSummary.transactions);
const blockSummary: BlockSummary = this.summarizeBlockTransactions(block.id, cpfpSummary.transactions);
diff --git a/backend/src/api/transaction-utils.ts b/backend/src/api/transaction-utils.ts
index 0b10afdfb..009fe1dde 100644
--- a/backend/src/api/transaction-utils.ts
+++ b/backend/src/api/transaction-utils.ts
@@ -3,6 +3,7 @@ import { IEsploraApi } from './bitcoin/esplora-api.interface';
import { Common } from './common';
import bitcoinApi, { bitcoinCoreApi } from './bitcoin/bitcoin-api-factory';
import * as bitcoinjs from 'bitcoinjs-lib';
+import logger from '../logger';
class TransactionUtils {
constructor() { }
@@ -22,6 +23,23 @@ class TransactionUtils {
};
}
+ // Wrapper for $getTransactionExtended with an automatic retry direct to Core if the first API request fails.
+ // Propagates any error from the retry request.
+ public async $getTransactionExtendedRetry(txid: string, addPrevouts = false, lazyPrevouts = false, forceCore = false, addMempoolData = false): Promise {
+ try {
+ const result = await this.$getTransactionExtended(txid, addPrevouts, lazyPrevouts, forceCore, addMempoolData);
+ if (result) {
+ return result;
+ } else {
+ logger.err(`Cannot fetch tx ${txid}. Reason: backend returned null data`);
+ }
+ } catch (e) {
+ logger.err(`Cannot fetch tx ${txid}. Reason: ` + (e instanceof Error ? e.message : e));
+ }
+ // retry direct from Core if first request failed
+ return this.$getTransactionExtended(txid, addPrevouts, lazyPrevouts, true, addMempoolData);
+ }
+
/**
* @param txId
* @param addPrevouts
@@ -31,7 +49,7 @@ class TransactionUtils {
public async $getTransactionExtended(txId: string, addPrevouts = false, lazyPrevouts = false, forceCore = false, addMempoolData = false): Promise {
let transaction: IEsploraApi.Transaction;
if (forceCore === true) {
- transaction = await bitcoinCoreApi.$getRawTransaction(txId, true);
+ transaction = await bitcoinCoreApi.$getRawTransaction(txId, false, addPrevouts, lazyPrevouts);
} else {
transaction = await bitcoinApi.$getRawTransaction(txId, false, addPrevouts, lazyPrevouts);
}
From 589adb95c304bad01a9d32c12264325f341a2954 Mon Sep 17 00:00:00 2001
From: Mononaut
Date: Thu, 27 Jul 2023 14:49:21 +0900
Subject: [PATCH 054/105] remove stray debugging log
---
backend/src/api/blocks.ts | 1 -
1 file changed, 1 deletion(-)
diff --git a/backend/src/api/blocks.ts b/backend/src/api/blocks.ts
index 9ad9278d0..ba7a55149 100644
--- a/backend/src/api/blocks.ts
+++ b/backend/src/api/blocks.ts
@@ -160,7 +160,6 @@ class Blocks {
// Require the first transaction to be a coinbase
const coinbase = transactionMap[txIds[0]];
if (!coinbase || !coinbase.vin[0].is_coinbase) {
- console.log(coinbase);
const msg = `Expected first tx in a block to be a coinbase, but found something else`;
logger.err(msg);
throw new Error(msg);
From 3f3f0db2f2631a977d4817c0cadf256f15543a62 Mon Sep 17 00:00:00 2001
From: nymkappa <1612910616@pm.me>
Date: Fri, 28 Jul 2023 13:45:04 +0900
Subject: [PATCH 055/105] [mining] use .slug to load pool logo
---
.../src/app/components/blocks-list/blocks-list.component.ts | 4 ++--
frontend/src/app/components/pool/pool-preview.component.ts | 2 +-
frontend/src/app/components/pool/pool.component.ts | 2 +-
frontend/src/app/dashboard/dashboard.component.ts | 2 +-
frontend/src/app/interfaces/node-api.interface.ts | 1 +
frontend/src/app/services/mining.service.ts | 2 +-
6 files changed, 7 insertions(+), 6 deletions(-)
diff --git a/frontend/src/app/components/blocks-list/blocks-list.component.ts b/frontend/src/app/components/blocks-list/blocks-list.component.ts
index 1af6572fc..cec925270 100644
--- a/frontend/src/app/components/blocks-list/blocks-list.component.ts
+++ b/frontend/src/app/components/blocks-list/blocks-list.component.ts
@@ -68,7 +68,7 @@ export class BlocksList implements OnInit {
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.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg';
+ block.extras.pool.slug + '.svg';
}
}
if (this.widget) {
@@ -102,7 +102,7 @@ export class BlocksList implements OnInit {
if (this.stateService.env.MINING_DASHBOARD) {
// @ts-ignore: Need to add an extra field for the template
blocks[1][0].extras.pool.logo = `/resources/mining-pools/` +
- blocks[1][0].extras.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg';
+ blocks[1][0].extras.pool.slug + '.svg';
}
acc.unshift(blocks[1][0]);
acc = acc.slice(0, this.widget ? 6 : 15);
diff --git a/frontend/src/app/components/pool/pool-preview.component.ts b/frontend/src/app/components/pool/pool-preview.component.ts
index 0431686d6..e03b73665 100644
--- a/frontend/src/app/components/pool/pool-preview.component.ts
+++ b/frontend/src/app/components/pool/pool-preview.component.ts
@@ -89,7 +89,7 @@ export class PoolPreviewComponent implements OnInit {
this.openGraphService.waitOver('pool-stats-' + this.slug);
- const logoSrc = `/resources/mining-pools/` + poolStats.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg';
+ const logoSrc = `/resources/mining-pools/` + poolStats.pool.slug + '.svg';
if (logoSrc === this.lastImgSrc) {
this.openGraphService.waitOver('pool-img-' + this.slug);
}
diff --git a/frontend/src/app/components/pool/pool.component.ts b/frontend/src/app/components/pool/pool.component.ts
index f2fc79ff2..edd5801fe 100644
--- a/frontend/src/app/components/pool/pool.component.ts
+++ b/frontend/src/app/components/pool/pool.component.ts
@@ -79,7 +79,7 @@ export class PoolComponent implements OnInit {
poolStats.pool.regexes = regexes.slice(0, -3);
return Object.assign({
- logo: `/resources/mining-pools/` + poolStats.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg'
+ logo: `/resources/mining-pools/` + poolStats.pool.slug + '.svg'
}, poolStats);
})
);
diff --git a/frontend/src/app/dashboard/dashboard.component.ts b/frontend/src/app/dashboard/dashboard.component.ts
index 05381453d..645ccc8d8 100644
--- a/frontend/src/app/dashboard/dashboard.component.ts
+++ b/frontend/src/app/dashboard/dashboard.component.ts
@@ -159,7 +159,7 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
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.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg';
+ block.extras.pool.slug + '.svg';
}
}
return of(blocks.slice(0, 6));
diff --git a/frontend/src/app/interfaces/node-api.interface.ts b/frontend/src/app/interfaces/node-api.interface.ts
index 4249fd9db..59dff8e90 100644
--- a/frontend/src/app/interfaces/node-api.interface.ts
+++ b/frontend/src/app/interfaces/node-api.interface.ts
@@ -110,6 +110,7 @@ export interface PoolInfo {
regexes: string; // JSON array
addresses: string; // JSON array
emptyBlocks: number;
+ slug: string;
}
export interface PoolStat {
pool: PoolInfo;
diff --git a/frontend/src/app/services/mining.service.ts b/frontend/src/app/services/mining.service.ts
index 63257fa74..96723921e 100644
--- a/frontend/src/app/services/mining.service.ts
+++ b/frontend/src/app/services/mining.service.ts
@@ -96,7 +96,7 @@ export class MiningService {
share: parseFloat((poolStat.blockCount / stats.blockCount * 100).toFixed(2)),
lastEstimatedHashrate: (poolStat.blockCount / stats.blockCount * stats.lastEstimatedHashrate / hashrateDivider).toFixed(2),
emptyBlockRatio: (poolStat.emptyBlocks / poolStat.blockCount * 100).toFixed(2),
- logo: `/resources/mining-pools/` + poolStat.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg',
+ logo: `/resources/mining-pools/` + poolStat.slug + '.svg',
...poolStat
};
});
From 9b65fbd98c7fac134e68705fa6be558de19699d5 Mon Sep 17 00:00:00 2001
From: Mononaut
Date: Fri, 28 Jul 2023 15:53:52 +0900
Subject: [PATCH 056/105] Show new mined transactions on the address page
---
.../components/address/address.component.ts | 62 +++++++++++--------
1 file changed, 37 insertions(+), 25 deletions(-)
diff --git a/frontend/src/app/components/address/address.component.ts b/frontend/src/app/components/address/address.component.ts
index ae1f6dbbe..64d3a4143 100644
--- a/frontend/src/app/components/address/address.component.ts
+++ b/frontend/src/app/components/address/address.component.ts
@@ -166,31 +166,8 @@ export class AddressComponent implements OnInit, OnDestroy {
});
this.stateService.mempoolTransactions$
- .subscribe((transaction) => {
- if (this.transactions.some((t) => t.txid === transaction.txid)) {
- return;
- }
-
- this.transactions.unshift(transaction);
- this.transactions = this.transactions.slice();
- this.txCount++;
-
- if (transaction.vout.some((vout) => vout.scriptpubkey_address === this.address.address)) {
- this.audioService.playSound('cha-ching');
- } else {
- this.audioService.playSound('chime');
- }
-
- transaction.vin.forEach((vin) => {
- if (vin.prevout.scriptpubkey_address === this.address.address) {
- this.sent += vin.prevout.value;
- }
- });
- transaction.vout.forEach((vout) => {
- if (vout.scriptpubkey_address === this.address.address) {
- this.received += vout.value;
- }
- });
+ .subscribe(tx => {
+ this.addTransaction(tx);
});
this.stateService.blockTransactions$
@@ -200,12 +177,47 @@ export class AddressComponent implements OnInit, OnDestroy {
tx.status = transaction.status;
this.transactions = this.transactions.slice();
this.audioService.playSound('magic');
+ } else {
+ if (this.addTransaction(transaction, false)) {
+ this.audioService.playSound('magic');
+ }
}
this.totalConfirmedTxCount++;
this.loadedConfirmedTxCount++;
});
}
+ addTransaction(transaction: Transaction, playSound: boolean = true): boolean {
+ if (this.transactions.some((t) => t.txid === transaction.txid)) {
+ return false;
+ }
+
+ this.transactions.unshift(transaction);
+ this.transactions = this.transactions.slice();
+ this.txCount++;
+
+ if (playSound) {
+ if (transaction.vout.some((vout) => vout?.scriptpubkey_address === this.address.address)) {
+ this.audioService.playSound('cha-ching');
+ } else {
+ this.audioService.playSound('chime');
+ }
+ }
+
+ transaction.vin.forEach((vin) => {
+ if (vin?.prevout?.scriptpubkey_address === this.address.address) {
+ this.sent += vin.prevout.value;
+ }
+ });
+ transaction.vout.forEach((vout) => {
+ if (vout?.scriptpubkey_address === this.address.address) {
+ this.received += vout.value;
+ }
+ });
+
+ return true;
+ }
+
loadMore() {
if (this.isLoadingTransactions || !this.totalConfirmedTxCount || this.loadedConfirmedTxCount >= this.totalConfirmedTxCount) {
return;
From 74b87b60065b617b93cbcec6219a0f2281f26387 Mon Sep 17 00:00:00 2001
From: Mononaut
Date: Wed, 26 Jul 2023 10:47:59 +0900
Subject: [PATCH 057/105] Support p2pk track-address websocket subscriptions
---
backend/src/api/transaction-utils.ts | 9 ++++
backend/src/api/websocket-handler.ts | 76 +++++++++++++++++++++++++++-
2 files changed, 83 insertions(+), 2 deletions(-)
diff --git a/backend/src/api/transaction-utils.ts b/backend/src/api/transaction-utils.ts
index 0b10afdfb..b8a9a108a 100644
--- a/backend/src/api/transaction-utils.ts
+++ b/backend/src/api/transaction-utils.ts
@@ -3,6 +3,7 @@ import { IEsploraApi } from './bitcoin/esplora-api.interface';
import { Common } from './common';
import bitcoinApi, { bitcoinCoreApi } from './bitcoin/bitcoin-api-factory';
import * as bitcoinjs from 'bitcoinjs-lib';
+import crypto from 'node:crypto';
class TransactionUtils {
constructor() { }
@@ -170,6 +171,14 @@ class TransactionUtils {
16
);
}
+
+ public calcScriptHash(script: string): string {
+ if (!/^[0-9a-fA-F]*$/.test(script) || script.length % 2 !== 0) {
+ throw new Error('script is not a valid hex string');
+ }
+ const buf = Buffer.from(script, 'hex');
+ return crypto.createHash('sha256').update(buf).digest('hex');
+ }
}
export default new TransactionUtils();
diff --git a/backend/src/api/websocket-handler.ts b/backend/src/api/websocket-handler.ts
index 56c8513cd..3438e0e0c 100644
--- a/backend/src/api/websocket-handler.ts
+++ b/backend/src/api/websocket-handler.ts
@@ -183,15 +183,22 @@ class WebsocketHandler {
}
if (parsedMessage && parsedMessage['track-address']) {
- if (/^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[a-z]{2,5}1[ac-hj-np-z02-9]{8,100}|[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100})$/
+ if (/^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[a-z]{2,5}1[ac-hj-np-z02-9]{8,100}|[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}|[0-9a-fA-F]{130})$/
.test(parsedMessage['track-address'])) {
let matchedAddress = parsedMessage['track-address'];
if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}$/.test(parsedMessage['track-address'])) {
matchedAddress = matchedAddress.toLowerCase();
}
- client['track-address'] = matchedAddress;
+ if (/^[0-9a-fA-F]{130}$/.test(parsedMessage['track-address'])) {
+ client['track-address'] = null;
+ client['track-scripthash'] = transactionUtils.calcScriptHash('41' + matchedAddress + 'ac');
+ } else {
+ client['track-address'] = matchedAddress;
+ client['track-scripthash'] = null;
+ }
} else {
client['track-address'] = null;
+ client['track-scripthash'] = null;
}
}
@@ -546,6 +553,44 @@ class WebsocketHandler {
}
}
+ if (client['track-scripthash']) {
+ const foundTransactions: TransactionExtended[] = [];
+
+ for (const tx of newTransactions) {
+ const someVin = tx.vin.some((vin) => !!vin.prevout && vin.prevout.scriptpubkey_type === 'p2pk' && vin.prevout.scriptpubkey === client['track-scripthash']);
+ if (someVin) {
+ if (config.MEMPOOL.BACKEND !== 'esplora') {
+ try {
+ const fullTx = await transactionUtils.$getMempoolTransactionExtended(tx.txid, true);
+ foundTransactions.push(fullTx);
+ } catch (e) {
+ logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e));
+ }
+ } else {
+ foundTransactions.push(tx);
+ }
+ return;
+ }
+ const someVout = tx.vout.some((vout) => vout.scriptpubkey_type === 'p2pk' && vout.scriptpubkey === client['track-scripthash']);
+ if (someVout) {
+ if (config.MEMPOOL.BACKEND !== 'esplora') {
+ try {
+ const fullTx = await transactionUtils.$getMempoolTransactionExtended(tx.txid, true);
+ foundTransactions.push(fullTx);
+ } catch (e) {
+ logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e));
+ }
+ } else {
+ foundTransactions.push(tx);
+ }
+ }
+ }
+
+ if (foundTransactions.length) {
+ response['address-transactions'] = JSON.stringify(foundTransactions);
+ }
+ }
+
if (client['track-asset']) {
const foundTransactions: TransactionExtended[] = [];
@@ -821,6 +866,33 @@ class WebsocketHandler {
}
}
+ if (client['track-scripthash']) {
+ const foundTransactions: TransactionExtended[] = [];
+
+ transactions.forEach((tx) => {
+ if (tx.vin && tx.vin.some((vin) => !!vin.prevout && vin.prevout.scriptpubkey_type === 'p2pk' && vin.prevout.scriptpubkey === client['track-scripthash'])) {
+ foundTransactions.push(tx);
+ return;
+ }
+ if (tx.vout && tx.vout.some((vout) => vout.scriptpubkey_type === 'p2pk' && vout.scriptpubkey === client['track-scripthash'])) {
+ foundTransactions.push(tx);
+ }
+ });
+
+ if (foundTransactions.length) {
+ foundTransactions.forEach((tx) => {
+ tx.status = {
+ confirmed: true,
+ block_height: block.height,
+ block_hash: block.id,
+ block_time: block.timestamp,
+ };
+ });
+
+ response['block-transactions'] = JSON.stringify(foundTransactions);
+ }
+ }
+
if (client['track-asset']) {
const foundTransactions: TransactionExtended[] = [];
From 5b2470955d480656f2f3e636bc257a9da3f7bf0d Mon Sep 17 00:00:00 2001
From: Mononaut
Date: Fri, 28 Jul 2023 16:04:03 +0900
Subject: [PATCH 058/105] track p2pk addresses by scriptpubkey not scripthash
---
backend/src/api/websocket-handler.ts | 18 +++++++++---------
1 file changed, 9 insertions(+), 9 deletions(-)
diff --git a/backend/src/api/websocket-handler.ts b/backend/src/api/websocket-handler.ts
index 3438e0e0c..74c4ed832 100644
--- a/backend/src/api/websocket-handler.ts
+++ b/backend/src/api/websocket-handler.ts
@@ -191,14 +191,14 @@ class WebsocketHandler {
}
if (/^[0-9a-fA-F]{130}$/.test(parsedMessage['track-address'])) {
client['track-address'] = null;
- client['track-scripthash'] = transactionUtils.calcScriptHash('41' + matchedAddress + 'ac');
+ client['track-scriptpubkey'] = '41' + matchedAddress + 'ac';
} else {
client['track-address'] = matchedAddress;
- client['track-scripthash'] = null;
+ client['track-scriptpubkey'] = null;
}
} else {
client['track-address'] = null;
- client['track-scripthash'] = null;
+ client['track-scriptpubkey'] = null;
}
}
@@ -553,11 +553,11 @@ class WebsocketHandler {
}
}
- if (client['track-scripthash']) {
+ if (client['track-scriptpubkey']) {
const foundTransactions: TransactionExtended[] = [];
for (const tx of newTransactions) {
- const someVin = tx.vin.some((vin) => !!vin.prevout && vin.prevout.scriptpubkey_type === 'p2pk' && vin.prevout.scriptpubkey === client['track-scripthash']);
+ const someVin = tx.vin.some((vin) => !!vin.prevout && vin.prevout.scriptpubkey_type === 'p2pk' && vin.prevout.scriptpubkey === client['track-scriptpubkey']);
if (someVin) {
if (config.MEMPOOL.BACKEND !== 'esplora') {
try {
@@ -571,7 +571,7 @@ class WebsocketHandler {
}
return;
}
- const someVout = tx.vout.some((vout) => vout.scriptpubkey_type === 'p2pk' && vout.scriptpubkey === client['track-scripthash']);
+ const someVout = tx.vout.some((vout) => vout.scriptpubkey_type === 'p2pk' && vout.scriptpubkey === client['track-scriptpubkey']);
if (someVout) {
if (config.MEMPOOL.BACKEND !== 'esplora') {
try {
@@ -866,15 +866,15 @@ class WebsocketHandler {
}
}
- if (client['track-scripthash']) {
+ if (client['track-scriptpubkey']) {
const foundTransactions: TransactionExtended[] = [];
transactions.forEach((tx) => {
- if (tx.vin && tx.vin.some((vin) => !!vin.prevout && vin.prevout.scriptpubkey_type === 'p2pk' && vin.prevout.scriptpubkey === client['track-scripthash'])) {
+ if (tx.vin && tx.vin.some((vin) => !!vin.prevout && vin.prevout.scriptpubkey_type === 'p2pk' && vin.prevout.scriptpubkey === client['track-scriptpubkey'])) {
foundTransactions.push(tx);
return;
}
- if (tx.vout && tx.vout.some((vout) => vout.scriptpubkey_type === 'p2pk' && vout.scriptpubkey === client['track-scripthash'])) {
+ if (tx.vout && tx.vout.some((vout) => vout.scriptpubkey_type === 'p2pk' && vout.scriptpubkey === client['track-scriptpubkey'])) {
foundTransactions.push(tx);
}
});
From 63ccecf4107823194787e0aca8d7309f0dfde9df Mon Sep 17 00:00:00 2001
From: Mononaut
Date: Fri, 28 Jul 2023 16:14:28 +0900
Subject: [PATCH 059/105] remove unused calcScriptHash function
---
backend/src/api/transaction-utils.ts | 9 ---------
1 file changed, 9 deletions(-)
diff --git a/backend/src/api/transaction-utils.ts b/backend/src/api/transaction-utils.ts
index b8a9a108a..0b10afdfb 100644
--- a/backend/src/api/transaction-utils.ts
+++ b/backend/src/api/transaction-utils.ts
@@ -3,7 +3,6 @@ import { IEsploraApi } from './bitcoin/esplora-api.interface';
import { Common } from './common';
import bitcoinApi, { bitcoinCoreApi } from './bitcoin/bitcoin-api-factory';
import * as bitcoinjs from 'bitcoinjs-lib';
-import crypto from 'node:crypto';
class TransactionUtils {
constructor() { }
@@ -171,14 +170,6 @@ class TransactionUtils {
16
);
}
-
- public calcScriptHash(script: string): string {
- if (!/^[0-9a-fA-F]*$/.test(script) || script.length % 2 !== 0) {
- throw new Error('script is not a valid hex string');
- }
- const buf = Buffer.from(script, 'hex');
- return crypto.createHash('sha256').update(buf).digest('hex');
- }
}
export default new TransactionUtils();
From 2c613195cce70672f2312ce02f8a4bc1771227a0 Mon Sep 17 00:00:00 2001
From: Mononaut
Date: Fri, 28 Jul 2023 16:35:42 +0900
Subject: [PATCH 060/105] Add support for compressed p2pk addresses
---
backend/src/api/websocket-handler.ts | 7 +++++--
.../components/address/address-preview.component.ts | 4 ++--
.../src/app/components/address/address.component.ts | 6 +++---
.../components/search-form/search-form.component.ts | 2 +-
.../transactions-list.component.html | 12 ++++++------
frontend/src/app/services/electrs-api.service.ts | 3 ++-
6 files changed, 19 insertions(+), 15 deletions(-)
diff --git a/backend/src/api/websocket-handler.ts b/backend/src/api/websocket-handler.ts
index 74c4ed832..0d0332523 100644
--- a/backend/src/api/websocket-handler.ts
+++ b/backend/src/api/websocket-handler.ts
@@ -183,15 +183,18 @@ class WebsocketHandler {
}
if (parsedMessage && parsedMessage['track-address']) {
- if (/^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[a-z]{2,5}1[ac-hj-np-z02-9]{8,100}|[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}|[0-9a-fA-F]{130})$/
+ if (/^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[a-z]{2,5}1[ac-hj-np-z02-9]{8,100}|[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(parsedMessage['track-address'])) {
let matchedAddress = parsedMessage['track-address'];
if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}$/.test(parsedMessage['track-address'])) {
matchedAddress = matchedAddress.toLowerCase();
}
- if (/^[0-9a-fA-F]{130}$/.test(parsedMessage['track-address'])) {
+ if (/^04[a-fA-F0-9]{128}$/.test(parsedMessage['track-address'])) {
client['track-address'] = null;
client['track-scriptpubkey'] = '41' + matchedAddress + 'ac';
+ } else if (/^|(02|03)[a-fA-F0-9]{64}$/.test(parsedMessage['track-address'])) {
+ client['track-address'] = null;
+ client['track-scriptpubkey'] = '21' + matchedAddress + 'ac';
} else {
client['track-address'] = matchedAddress;
client['track-scriptpubkey'] = null;
diff --git a/frontend/src/app/components/address/address-preview.component.ts b/frontend/src/app/components/address/address-preview.component.ts
index 07ead8baa..844def9fd 100644
--- a/frontend/src/app/components/address/address-preview.component.ts
+++ b/frontend/src/app/components/address/address-preview.component.ts
@@ -64,12 +64,12 @@ export class AddressPreviewComponent implements OnInit, OnDestroy {
this.address = null;
this.addressInfo = null;
this.addressString = params.get('id') || '';
- if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}|[A-F0-9]{130}$/.test(this.addressString)) {
+ if (/^[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(this.addressString)) {
this.addressString = this.addressString.toLowerCase();
}
this.seoService.setTitle($localize`:@@address.component.browser-title:Address: ${this.addressString}:INTERPOLATION:`);
- return (this.addressString.match(/[a-f0-9]{130}/)
+ return (this.addressString.match(/04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}/)
? this.electrsApiService.getPubKeyAddress$(this.addressString)
: this.electrsApiService.getAddress$(this.addressString)
).pipe(
diff --git a/frontend/src/app/components/address/address.component.ts b/frontend/src/app/components/address/address.component.ts
index ae1f6dbbe..a6cbb9617 100644
--- a/frontend/src/app/components/address/address.component.ts
+++ b/frontend/src/app/components/address/address.component.ts
@@ -72,7 +72,7 @@ export class AddressComponent implements OnInit, OnDestroy {
this.addressInfo = null;
document.body.scrollTo(0, 0);
this.addressString = params.get('id') || '';
- if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}|[A-F0-9]{130}$/.test(this.addressString)) {
+ if (/^[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(this.addressString)) {
this.addressString = this.addressString.toLowerCase();
}
this.seoService.setTitle($localize`:@@address.component.browser-title:Address: ${this.addressString}:INTERPOLATION:`);
@@ -84,7 +84,7 @@ export class AddressComponent implements OnInit, OnDestroy {
)
.pipe(
switchMap(() => (
- this.addressString.match(/[a-f0-9]{130}/)
+ this.addressString.match(/04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}/)
? this.electrsApiService.getPubKeyAddress$(this.addressString)
: this.electrsApiService.getAddress$(this.addressString)
).pipe(
@@ -118,7 +118,7 @@ export class AddressComponent implements OnInit, OnDestroy {
this.isLoadingAddress = false;
this.isLoadingTransactions = true;
return address.is_pubkey
- ? this.electrsApiService.getScriptHashTransactions$('41' + address.address + 'ac')
+ ? this.electrsApiService.getScriptHashTransactions$((address.address.length === 66 ? '21' : '41') + address.address + 'ac')
: this.electrsApiService.getAddressTransactions$(address.address);
}),
switchMap((transactions) => {
diff --git a/frontend/src/app/components/search-form/search-form.component.ts b/frontend/src/app/components/search-form/search-form.component.ts
index 61b3351b7..0a794d1f5 100644
--- a/frontend/src/app/components/search-form/search-form.component.ts
+++ b/frontend/src/app/components/search-form/search-form.component.ts
@@ -34,7 +34,7 @@ export class SearchFormComponent implements OnInit {
}
}
- regexAddress = /^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[A-z]{2,5}1[a-zA-HJ-NP-Z0-9]{39,59}|[0-9a-fA-F]{130})$/;
+ regexAddress = /^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[A-z]{2,5}1[a-zA-HJ-NP-Z0-9]{39,59}|04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64})$/;
regexBlockhash = /^[0]{8}[a-fA-F0-9]{56}$/;
regexTransaction = /^([a-fA-F0-9]{64})(:\d+)?$/;
regexBlockheight = /^[0-9]{1,9}$/;
diff --git a/frontend/src/app/components/transactions-list/transactions-list.component.html b/frontend/src/app/components/transactions-list/transactions-list.component.html
index d1d0673fe..22486d320 100644
--- a/frontend/src/app/components/transactions-list/transactions-list.component.html
+++ b/frontend/src/app/components/transactions-list/transactions-list.component.html
@@ -23,7 +23,7 @@
@@ -56,8 +56,8 @@
Peg-in
- P2PK
-
+ P2PK
+
@@ -184,7 +184,7 @@
@@ -192,8 +192,8 @@
- P2PK
-
+ P2PK
+
diff --git a/frontend/src/app/services/electrs-api.service.ts b/frontend/src/app/services/electrs-api.service.ts
index f866eb23d..d63d49f68 100644
--- a/frontend/src/app/services/electrs-api.service.ts
+++ b/frontend/src/app/services/electrs-api.service.ts
@@ -67,7 +67,8 @@ export class ElectrsApiService {
}
getPubKeyAddress$(pubkey: string): Observable {
- return this.getScriptHash$('41' + pubkey + 'ac').pipe(
+ const scriptpubkey = (pubkey.length === 130 ? '41' : '21') + pubkey + 'ac';
+ return this.getScriptHash$(scriptpubkey).pipe(
switchMap((scripthash: ScriptHash) => {
return of({
...scripthash,
From b1bdb528512c3276c2351397e29615b4dda71aea Mon Sep 17 00:00:00 2001
From: wiz
Date: Fri, 28 Jul 2023 23:39:33 +0900
Subject: [PATCH 061/105] ops: Fix a classic typo in mempool clear protection
log print
---
backend/src/api/mempool.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/backend/src/api/mempool.ts b/backend/src/api/mempool.ts
index 945b78738..e822ba329 100644
--- a/backend/src/api/mempool.ts
+++ b/backend/src/api/mempool.ts
@@ -274,7 +274,7 @@ class Mempool {
logger.warn(`Mempool clear protection triggered because transactions.length: ${transactions.length} and currentMempoolSize: ${currentMempoolSize}.`);
setTimeout(() => {
this.mempoolProtection = 2;
- logger.warn('Mempool clear protection resumed.');
+ logger.warn('Mempool clear protection ended, normal operation resumed.');
}, 1000 * 60 * config.MEMPOOL.CLEAR_PROTECTION_MINUTES);
}
From cc27c0159e393270bc5e1dda8cab0d3c107c9203 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?dni=20=E2=9A=A1?=
Date: Fri, 28 Jul 2023 23:04:44 +0200
Subject: [PATCH 062/105] [BUG]: Update frontend entrypoint.sh
Typo of variable LIQUID_ENABLED
---
docker/frontend/entrypoint.sh | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docker/frontend/entrypoint.sh b/docker/frontend/entrypoint.sh
index b6946578b..7d5ee313d 100644
--- a/docker/frontend/entrypoint.sh
+++ b/docker/frontend/entrypoint.sh
@@ -18,7 +18,7 @@ fi
__TESTNET_ENABLED__=${TESTNET_ENABLED:=false}
__SIGNET_ENABLED__=${SIGNET_ENABLED:=false}
-__LIQUID_ENABLED__=${LIQUID_EANBLED:=false}
+__LIQUID_ENABLED__=${LIQUID_ENABLED:=false}
__LIQUID_TESTNET_ENABLED__=${LIQUID_TESTNET_ENABLED:=false}
__BISQ_ENABLED__=${BISQ_ENABLED:=false}
__BISQ_SEPARATE_BACKEND__=${BISQ_SEPARATE_BACKEND:=false}
From 354c119e9990fe30788c6cc09e607d8847702533 Mon Sep 17 00:00:00 2001
From: Mononaut
Date: Sat, 29 Jul 2023 15:16:28 +0900
Subject: [PATCH 063/105] fix websocket connection state observable
---
.../src/app/dashboard/dashboard.component.ts | 20 +++++++++++++------
.../src/app/services/websocket.service.ts | 2 +-
2 files changed, 15 insertions(+), 7 deletions(-)
diff --git a/frontend/src/app/dashboard/dashboard.component.ts b/frontend/src/app/dashboard/dashboard.component.ts
index 05381453d..38ee5f436 100644
--- a/frontend/src/app/dashboard/dashboard.component.ts
+++ b/frontend/src/app/dashboard/dashboard.component.ts
@@ -1,6 +1,6 @@
import { AfterViewInit, ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
import { combineLatest, merge, Observable, of, Subscription } from 'rxjs';
-import { filter, map, scan, share, switchMap, tap } from 'rxjs/operators';
+import { catchError, filter, map, scan, share, switchMap, tap } from 'rxjs/operators';
import { BlockExtended, OptimizedMempoolStats } from '../interfaces/node-api.interface';
import { MempoolInfo, TransactionStripped, ReplacementInfo } from '../interfaces/websocket.interface';
import { ApiService } from '../services/api.service';
@@ -171,7 +171,11 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
this.mempoolStats$ = this.stateService.connectionState$
.pipe(
filter((state) => state === 2),
- switchMap(() => this.apiService.list2HStatistics$()),
+ switchMap(() => this.apiService.list2HStatistics$().pipe(
+ catchError((e) => {
+ return of(null);
+ })
+ )),
switchMap((mempoolStats) => {
return merge(
this.stateService.live2Chart$
@@ -186,10 +190,14 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
);
}),
map((mempoolStats) => {
- return {
- mempool: mempoolStats,
- weightPerSecond: this.handleNewMempoolData(mempoolStats.concat([])),
- };
+ if (mempoolStats) {
+ return {
+ mempool: mempoolStats,
+ weightPerSecond: this.handleNewMempoolData(mempoolStats.concat([])),
+ };
+ } else {
+ return null;
+ }
}),
share(),
);
diff --git a/frontend/src/app/services/websocket.service.ts b/frontend/src/app/services/websocket.service.ts
index e70424cdc..4bd20e987 100644
--- a/frontend/src/app/services/websocket.service.ts
+++ b/frontend/src/app/services/websocket.service.ts
@@ -113,7 +113,7 @@ export class WebsocketService {
this.stateService.connectionState$.next(2);
}
- if (this.stateService.connectionState$.value === 1) {
+ if (this.stateService.connectionState$.value !== 2) {
this.stateService.connectionState$.next(2);
}
From 2719be90751fb2309abc11dc7c9d7f6df14f422c Mon Sep 17 00:00:00 2001
From: Czino
Date: Sat, 29 Jul 2023 09:53:47 +0200
Subject: [PATCH 064/105] Add contributor license agreement
---
contributors/Czino.txt | 3 +++
1 file changed, 3 insertions(+)
create mode 100644 contributors/Czino.txt
diff --git a/contributors/Czino.txt b/contributors/Czino.txt
new file mode 100644
index 000000000..275149d7c
--- /dev/null
+++ b/contributors/Czino.txt
@@ -0,0 +1,3 @@
+I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of January 25, 2022.
+
+Signed: Czino
From 1f8f40011a83b337011b88c44b1b7735a22cae7b Mon Sep 17 00:00:00 2001
From: Czino
Date: Sat, 29 Jul 2023 09:57:40 +0200
Subject: [PATCH 065/105] Update date
---
contributors/Czino.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/contributors/Czino.txt b/contributors/Czino.txt
index 275149d7c..08affb095 100644
--- a/contributors/Czino.txt
+++ b/contributors/Czino.txt
@@ -1,3 +1,3 @@
-I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of January 25, 2022.
+I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of July 29, 2023.
Signed: Czino
From 562a5f68788dbf5c1de2dab3fe556f501bac7b1c Mon Sep 17 00:00:00 2001
From: Rishabh
Date: Sat, 29 Jul 2023 15:54:34 +0530
Subject: [PATCH 066/105] rishkwal contributor license agreement added
---
contributors/rishkwal.txt | 3 +++
1 file changed, 3 insertions(+)
create mode 100644 contributors/rishkwal.txt
diff --git a/contributors/rishkwal.txt b/contributors/rishkwal.txt
new file mode 100644
index 000000000..9a50bda6b
--- /dev/null
+++ b/contributors/rishkwal.txt
@@ -0,0 +1,3 @@
+I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of July 29, 2023.
+
+Signed: rishkwal
From 2670589293a12027d9ca5594da1525c4c4ddf735 Mon Sep 17 00:00:00 2001
From: fiatjaf_
Date: Sat, 29 Jul 2023 09:27:12 -0300
Subject: [PATCH 067/105] Create fiatjaf.txt
---
contributors/fiatjaf.txt | 5 +++++
1 file changed, 5 insertions(+)
create mode 100644 contributors/fiatjaf.txt
diff --git a/contributors/fiatjaf.txt b/contributors/fiatjaf.txt
new file mode 100644
index 000000000..cdd716d3c
--- /dev/null
+++ b/contributors/fiatjaf.txt
@@ -0,0 +1,5 @@
+I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of January 25, 2022.
+I also regret having ever contributed to this repository since they keep asking me to sign this legalese timewaste things.
+And finally I don't care about licenses and won't sue anyone over intellectual property, which is a fake statist construct invented by evil lobby lawyers.
+
+Signed: fiatjaf
From 945a8ce92e7bc99d3ae3b1dcde746cf1799536c1 Mon Sep 17 00:00:00 2001
From: Mononaut
Date: Sun, 30 Jul 2023 18:56:57 +0900
Subject: [PATCH 068/105] Use log10 scale for projected block fee graph
---
.../fee-distribution-graph.component.ts | 12 +++++++++---
1 file changed, 9 insertions(+), 3 deletions(-)
diff --git a/frontend/src/app/components/fee-distribution-graph/fee-distribution-graph.component.ts b/frontend/src/app/components/fee-distribution-graph/fee-distribution-graph.component.ts
index f275588a1..212510e71 100644
--- a/frontend/src/app/components/fee-distribution-graph/fee-distribution-graph.component.ts
+++ b/frontend/src/app/components/fee-distribution-graph/fee-distribution-graph.component.ts
@@ -74,14 +74,14 @@ export class FeeDistributionGraphComponent implements OnInit, OnChanges, OnDestr
this.labelInterval = this.numSamples / this.numLabels;
while (nextSample <= maxBlockVSize) {
if (txIndex >= txs.length) {
- samples.push([(1 - (sampleIndex / this.numSamples)) * 100, 0]);
+ samples.push([(1 - (sampleIndex / this.numSamples)) * 100, 0.000001]);
nextSample += sampleInterval;
sampleIndex++;
continue;
}
while (txs[txIndex] && nextSample < cumVSize + txs[txIndex].vsize) {
- samples.push([(1 - (sampleIndex / this.numSamples)) * 100, txs[txIndex].rate]);
+ samples.push([(1 - (sampleIndex / this.numSamples)) * 100, txs[txIndex].rate || 0.000001]);
nextSample += sampleInterval;
sampleIndex++;
}
@@ -118,7 +118,9 @@ export class FeeDistributionGraphComponent implements OnInit, OnChanges, OnDestr
},
},
yAxis: {
- type: 'value',
+ type: 'log',
+ min: 1,
+ max: this.data.reduce((min, val) => Math.max(min, val[1]), 1),
// name: 'Effective Fee Rate s/vb',
// nameLocation: 'middle',
splitLine: {
@@ -129,12 +131,16 @@ export class FeeDistributionGraphComponent implements OnInit, OnChanges, OnDestr
}
},
axisLabel: {
+ show: true,
formatter: (value: number): string => {
const unitValue = this.weightMode ? value / 4 : value;
const selectedPowerOfTen = selectPowerOfTen(unitValue);
const newVal = Math.round(unitValue / selectedPowerOfTen.divider);
return `${newVal}${selectedPowerOfTen.unit}`;
},
+ },
+ axisTick: {
+ show: true,
}
},
series: [{
From c88b7ddc7732201b17b9e54c50b1df32f147644e Mon Sep 17 00:00:00 2001
From: wiz
Date: Mon, 31 Jul 2023 11:12:54 +0900
Subject: [PATCH 069/105] ops: Use TK7 for unfurler
---
production/unfurler-config.mainnet.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/production/unfurler-config.mainnet.json b/production/unfurler-config.mainnet.json
index 77df23704..fd389476b 100644
--- a/production/unfurler-config.mainnet.json
+++ b/production/unfurler-config.mainnet.json
@@ -1,6 +1,6 @@
{
"SERVER": {
- "HOST": "https://mempool.fra.mempool.space",
+ "HOST": "https://mempool.tk7.mempool.space",
"HTTP_PORT": 8001
},
"MEMPOOL": {
From 16401044f6e1da00c932575876f4a78ce8aed624 Mon Sep 17 00:00:00 2001
From: wiz
Date: Mon, 31 Jul 2023 11:13:56 +0900
Subject: [PATCH 070/105] Remove text from mempool.space preview image
---
.../src/resources/mempool-space-preview.png | Bin 315182 -> 295855 bytes
frontend/src/resources/previews/dashboard.png | Bin 742892 -> 295855 bytes
2 files changed, 0 insertions(+), 0 deletions(-)
diff --git a/frontend/src/resources/mempool-space-preview.png b/frontend/src/resources/mempool-space-preview.png
index bfc59f60017e436a49a081e0a3e022e5300b15a9..60c8bcc6ac4a1ef484e3df91b4fada5e068426c4 100644
GIT binary patch
literal 295855
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 | | |