From 4154d3081ddc40bfc972bb183f7913008688d0ea Mon Sep 17 00:00:00 2001 From: softsimon Date: Thu, 29 Sep 2022 13:45:03 +0400 Subject: [PATCH 01/17] Handle network url ending matching better --- frontend/src/app/services/state.service.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/services/state.service.ts b/frontend/src/app/services/state.service.ts index b0e018941..7b513ee54 100644 --- a/frontend/src/app/services/state.service.ts +++ b/frontend/src/app/services/state.service.ts @@ -158,7 +158,8 @@ export class StateService { // (?:[a-z]{2}(?:-[A-Z]{2})?\/)? optional locale prefix (non-capturing) // (?:preview\/)? optional "preview" prefix (non-capturing) // (bisq|testnet|liquidtestnet|liquid|signet)/ network string (captured as networkMatches[1]) - const networkMatches = url.match(/^\/(?:[a-z]{2}(?:-[A-Z]{2})?\/)?(?:preview\/)?(bisq|testnet|liquidtestnet|liquid|signet)/); + // ($|\/) network string must end or end with a slash + const networkMatches = url.match(/^\/(?:[a-z]{2}(?:-[A-Z]{2})?\/)?(?:preview\/)?(bisq|testnet|liquidtestnet|liquid|signet)($|\/)/); switch (networkMatches && networkMatches[1]) { case 'liquid': if (this.network !== 'liquid') { From 0a4c1c24afa049f52edc2412a88fb627604b0cdd Mon Sep 17 00:00:00 2001 From: nymkappa Date: Fri, 30 Sep 2022 19:10:11 +0200 Subject: [PATCH 02/17] Fixes #2592 --- backend/src/api/lightning/clightning/clightning-convert.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/src/api/lightning/clightning/clightning-convert.ts b/backend/src/api/lightning/clightning/clightning-convert.ts index 121fb20ea..9b3c62f04 100644 --- a/backend/src/api/lightning/clightning/clightning-convert.ts +++ b/backend/src/api/lightning/clightning/clightning-convert.ts @@ -70,6 +70,8 @@ export async function convertAndmergeBidirectionalChannels(clChannels: any[]): P logger.info(`Building partial channels from clightning output. Channels processed: ${channelProcessed + 1} of ${keys.length}`); loggerTimer = new Date().getTime() / 1000; } + + channelProcessed++; } return consolidatedChannelList; From 39718147103b5fde0f46875fb77d5d34404e60a1 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Tue, 11 Oct 2022 17:01:23 +0000 Subject: [PATCH 03/17] Save flow diagram preference to localStorage --- .../transaction/transaction.component.html | 6 ++-- .../transaction/transaction.component.ts | 30 +++++++++++++++---- frontend/src/app/services/state.service.ts | 11 +++++++ frontend/src/app/services/storage.service.ts | 8 +++++ 4 files changed, 46 insertions(+), 9 deletions(-) diff --git a/frontend/src/app/components/transaction/transaction.component.html b/frontend/src/app/components/transaction/transaction.component.html index 360d3e34f..86930bcc7 100644 --- a/frontend/src/app/components/transaction/transaction.component.html +++ b/frontend/src/app/components/transaction/transaction.component.html @@ -190,7 +190,7 @@
- +

Flow

@@ -238,7 +238,7 @@
- +
@@ -329,7 +329,7 @@
- +

Flow

diff --git a/frontend/src/app/components/transaction/transaction.component.ts b/frontend/src/app/components/transaction/transaction.component.ts index c64c112b1..2be549569 100644 --- a/frontend/src/app/components/transaction/transaction.component.ts +++ b/frontend/src/app/components/transaction/transaction.component.ts @@ -49,12 +49,15 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { liquidUnblinding = new LiquidUnblinding(); inputIndex: number; outputIndex: number; - showFlow: boolean = true; graphExpanded: boolean = false; graphWidth: number = 1000; graphHeight: number = 360; inOutLimit: number = 150; maxInOut: number = 0; + flowPrefSubscription: Subscription; + hideFlow: boolean = this.stateService.hideFlow.value; + overrideFlowPreference: boolean = null; + flowEnabled: boolean; tooltipPosition: { x: number, y: number }; @@ -78,6 +81,12 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { (network) => (this.network = network) ); + this.setFlowEnabled(); + this.flowPrefSubscription = this.stateService.hideFlow.subscribe((hide) => { + this.hideFlow = !!hide; + this.setFlowEnabled(); + }); + this.timeAvg$ = timer(0, 1000) .pipe( switchMap(() => this.stateService.difficultyAdjustment$), @@ -245,11 +254,14 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { this.queryParamsSubscription = this.route.queryParams.subscribe((params) => { if (params.showFlow === 'false') { - this.showFlow = false; + this.overrideFlowPreference = false; + } else if (params.showFlow === 'true') { + this.overrideFlowPreference = true; } else { - this.showFlow = true; - this.setGraphSize(); + this.overrideFlowPreference = null; } + this.setFlowEnabled(); + this.setGraphSize(); }); } @@ -325,15 +337,20 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { } toggleGraph() { - this.showFlow = !this.showFlow; + const showFlow = !this.flowEnabled; + this.stateService.hideFlow.next(!showFlow); this.router.navigate([], { relativeTo: this.route, - queryParams: { showFlow: this.showFlow }, + queryParams: { showFlow: showFlow }, queryParamsHandling: 'merge', fragment: 'flow' }); } + setFlowEnabled() { + this.flowEnabled = (this.overrideFlowPreference != null ? this.overrideFlowPreference : !this.hideFlow); + } + expandGraph() { this.graphExpanded = true; } @@ -365,6 +382,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { this.txReplacedSubscription.unsubscribe(); this.blocksSubscription.unsubscribe(); this.queryParamsSubscription.unsubscribe(); + this.flowPrefSubscription.unsubscribe(); this.leaveTransaction(); } } diff --git a/frontend/src/app/services/state.service.ts b/frontend/src/app/services/state.service.ts index 9f7cc58a3..d7feb38bb 100644 --- a/frontend/src/app/services/state.service.ts +++ b/frontend/src/app/services/state.service.ts @@ -110,6 +110,7 @@ export class StateService { blockScrolling$: Subject = new Subject(); timeLtr: BehaviorSubject; + hideFlow: BehaviorSubject; constructor( @Inject(PLATFORM_ID) private platformId: any, @@ -159,6 +160,16 @@ export class StateService { this.timeLtr.subscribe((ltr) => { this.storageService.setValue('time-preference-ltr', ltr ? 'true' : 'false'); }); + + const savedFlowPreference = this.storageService.getValue('flow-preference'); + this.hideFlow = new BehaviorSubject(savedFlowPreference === 'hide'); + this.hideFlow.subscribe((hide) => { + if (hide) { + this.storageService.setValue('flow-preference', hide ? 'hide' : 'show'); + } else { + this.storageService.removeItem('flow-preference'); + } + }); } setNetworkBasedonUrl(url: string) { diff --git a/frontend/src/app/services/storage.service.ts b/frontend/src/app/services/storage.service.ts index f3ea694b2..73a013146 100644 --- a/frontend/src/app/services/storage.service.ts +++ b/frontend/src/app/services/storage.service.ts @@ -46,4 +46,12 @@ export class StorageService { console.log(e); } } + + removeItem(key: string): void { + try { + localStorage.removeItem(key); + } catch (e) { + console.log(e); + } + } } From 5cdb0c5ce97cd74f111872523a38c0628fa83c9f Mon Sep 17 00:00:00 2001 From: Mononaut Date: Tue, 11 Oct 2022 23:06:46 +0000 Subject: [PATCH 04/17] Fix tx preview flow diagram highlight bug --- .../components/tx-bowtie-graph/tx-bowtie-graph.component.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/components/tx-bowtie-graph/tx-bowtie-graph.component.html b/frontend/src/app/components/tx-bowtie-graph/tx-bowtie-graph.component.html index ced3b5f57..a85f62c65 100644 --- a/frontend/src/app/components/tx-bowtie-graph/tx-bowtie-graph.component.html +++ b/frontend/src/app/components/tx-bowtie-graph/tx-bowtie-graph.component.html @@ -68,7 +68,7 @@ Date: Wed, 12 Oct 2022 22:13:29 +0000 Subject: [PATCH 05/17] Maintain page when switching networks --- frontend/src/app/app-routing.module.ts | 46 ++++++++-- frontend/src/app/bisq/bisq.routing.module.ts | 7 ++ .../bisq-master-page.component.html | 12 +-- .../bisq-master-page.component.ts | 7 ++ .../liquid-master-page.component.html | 12 +-- .../liquid-master-page.component.ts | 7 ++ .../master-page/master-page.component.html | 12 +-- .../master-page/master-page.component.ts | 7 ++ frontend/src/app/docs/docs.routing.module.ts | 1 + .../src/app/graphs/graphs.routing.module.ts | 20 +++++ .../app/lightning/lightning.routing.module.ts | 2 + .../src/app/services/navigation.service.ts | 90 +++++++++++++++++++ 12 files changed, 199 insertions(+), 24 deletions(-) create mode 100644 frontend/src/app/services/navigation.service.ts diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index 260efcafe..69c78fc83 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -74,12 +74,14 @@ let routes: Routes = [ children: [], component: AddressComponent, data: { - ogImage: true + ogImage: true, + networkSpecific: true, } }, { path: 'tx', component: StartComponent, + data: { networkSpecific: true }, children: [ { path: ':id', @@ -90,6 +92,7 @@ let routes: Routes = [ { path: 'block', component: StartComponent, + data: { networkSpecific: true }, children: [ { path: ':id', @@ -102,6 +105,7 @@ let routes: Routes = [ }, { path: 'block-audit', + data: { networkSpecific: true }, children: [ { path: ':id', @@ -121,12 +125,13 @@ let routes: Routes = [ { path: 'lightning', loadChildren: () => import('./lightning/lightning.module').then(m => m.LightningModule), - data: { preload: browserWindowEnv && browserWindowEnv.LIGHTNING === true }, + data: { preload: browserWindowEnv && browserWindowEnv.LIGHTNING === true, networks: ['bitcoin'] }, }, ], }, { path: 'status', + data: { networks: ['bitcoin', 'liquid'] }, component: StatusViewComponent }, { @@ -185,11 +190,13 @@ let routes: Routes = [ children: [], component: AddressComponent, data: { - ogImage: true + ogImage: true, + networkSpecific: true, } }, { path: 'tx', + data: { networkSpecific: true }, component: StartComponent, children: [ { @@ -200,6 +207,7 @@ let routes: Routes = [ }, { path: 'block', + data: { networkSpecific: true }, component: StartComponent, children: [ { @@ -213,6 +221,7 @@ let routes: Routes = [ }, { path: 'block-audit', + data: { networkSpecific: true }, children: [ { path: ':id', @@ -230,12 +239,14 @@ let routes: Routes = [ }, { path: 'lightning', + data: { networks: ['bitcoin'] }, loadChildren: () => import('./lightning/lightning.module').then(m => m.LightningModule) }, ], }, { path: 'status', + data: { networks: ['bitcoin', 'liquid'] }, component: StatusViewComponent }, { @@ -291,11 +302,13 @@ let routes: Routes = [ children: [], component: AddressComponent, data: { - ogImage: true + ogImage: true, + networkSpecific: true, } }, { path: 'tx', + data: { networkSpecific: true }, component: StartComponent, children: [ { @@ -306,6 +319,7 @@ let routes: Routes = [ }, { path: 'block', + data: { networkSpecific: true }, component: StartComponent, children: [ { @@ -319,6 +333,7 @@ let routes: Routes = [ }, { path: 'block-audit', + data: { networkSpecific: true }, children: [ { path: ':id', @@ -336,6 +351,7 @@ let routes: Routes = [ }, { path: 'lightning', + data: { networks: ['bitcoin'] }, loadChildren: () => import('./lightning/lightning.module').then(m => m.LightningModule) }, ], @@ -359,6 +375,7 @@ let routes: Routes = [ }, { path: 'status', + data: { networks: ['bitcoin', 'liquid'] }, component: StatusViewComponent }, { @@ -422,11 +439,13 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') { children: [], component: AddressComponent, data: { - ogImage: true + ogImage: true, + networkSpecific: true, } }, { path: 'tx', + data: { networkSpecific: true }, component: StartComponent, children: [ { @@ -437,6 +456,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') { }, { path: 'block', + data: { networkSpecific: true }, component: StartComponent, children: [ { @@ -450,18 +470,22 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') { }, { path: 'assets', + data: { networks: ['liquid'] }, component: AssetsNavComponent, children: [ { path: 'all', + data: { networks: ['liquid'] }, component: AssetsComponent, }, { path: 'asset/:id', + data: { networkSpecific: true }, component: AssetComponent }, { path: 'group/:id', + data: { networkSpecific: true }, component: AssetGroupComponent }, { @@ -482,6 +506,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') { }, { path: 'status', + data: { networks: ['bitcoin', 'liquid'] }, component: StatusViewComponent }, { @@ -532,11 +557,13 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') { children: [], component: AddressComponent, data: { - ogImage: true + ogImage: true, + networkSpecific: true, } }, { path: 'tx', + data: { networkSpecific: true }, component: StartComponent, children: [ { @@ -547,6 +574,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') { }, { path: 'block', + data: { networkSpecific: true }, component: StartComponent, children: [ { @@ -560,22 +588,27 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') { }, { path: 'assets', + data: { networks: ['liquid'] }, component: AssetsNavComponent, children: [ { path: 'featured', + data: { networkSpecific: true }, component: AssetsFeaturedComponent, }, { path: 'all', + data: { networks: ['liquid'] }, component: AssetsComponent, }, { path: 'asset/:id', + data: { networkSpecific: true }, component: AssetComponent }, { path: 'group/:id', + data: { networkSpecific: true }, component: AssetGroupComponent }, { @@ -609,6 +642,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') { }, { path: 'status', + data: { networks: ['bitcoin', 'liquid']}, component: StatusViewComponent }, { diff --git a/frontend/src/app/bisq/bisq.routing.module.ts b/frontend/src/app/bisq/bisq.routing.module.ts index f7385ae63..11acdca2a 100644 --- a/frontend/src/app/bisq/bisq.routing.module.ts +++ b/frontend/src/app/bisq/bisq.routing.module.ts @@ -20,14 +20,17 @@ const routes: Routes = [ }, { path: 'markets', + data: { networks: ['bisq'] }, component: BisqDashboardComponent, }, { path: 'transactions', + data: { networks: ['bisq'] }, component: BisqTransactionsComponent }, { path: 'market/:pair', + data: { networkSpecific: true }, component: BisqMarketComponent, }, { @@ -36,6 +39,7 @@ const routes: Routes = [ }, { path: 'tx/:id', + data: { networkSpecific: true }, component: BisqTransactionComponent }, { @@ -45,14 +49,17 @@ const routes: Routes = [ }, { path: 'block/:id', + data: { networkSpecific: true }, component: BisqBlockComponent, }, { path: 'address/:id', + data: { networkSpecific: true }, component: BisqAddressComponent, }, { path: 'stats', + data: { networks: ['bisq'] }, component: BisqStatsComponent, }, { diff --git a/frontend/src/app/components/bisq-master-page/bisq-master-page.component.html b/frontend/src/app/components/bisq-master-page/bisq-master-page.component.html index d07f9d60c..2054f1a5d 100644 --- a/frontend/src/app/components/bisq-master-page/bisq-master-page.component.html +++ b/frontend/src/app/components/bisq-master-page/bisq-master-page.component.html @@ -44,13 +44,13 @@ diff --git a/frontend/src/app/components/bisq-master-page/bisq-master-page.component.ts b/frontend/src/app/components/bisq-master-page/bisq-master-page.component.ts index d69b37d3d..941d9e21e 100644 --- a/frontend/src/app/components/bisq-master-page/bisq-master-page.component.ts +++ b/frontend/src/app/components/bisq-master-page/bisq-master-page.component.ts @@ -3,6 +3,7 @@ import { Env, StateService } from '../../services/state.service'; import { Observable } from 'rxjs'; import { LanguageService } from '../../services/language.service'; import { EnterpriseService } from '../../services/enterprise.service'; +import { NavigationService } from '../../services/navigation.service'; @Component({ selector: 'app-bisq-master-page', @@ -15,17 +16,23 @@ export class BisqMasterPageComponent implements OnInit { env: Env; isMobile = window.innerWidth <= 767.98; urlLanguage: string; + networkPaths: { [network: string]: string }; constructor( private stateService: StateService, private languageService: LanguageService, private enterpriseService: EnterpriseService, + private navigationService: NavigationService, ) { } ngOnInit() { this.env = this.stateService.env; this.connectionState$ = this.stateService.connectionState$; this.urlLanguage = this.languageService.getLanguageForUrl(); + this.navigationService.subnetPaths.subscribe((paths) => { + console.log('network paths updated...'); + this.networkPaths = paths; + }); } collapse(): void { diff --git a/frontend/src/app/components/liquid-master-page/liquid-master-page.component.html b/frontend/src/app/components/liquid-master-page/liquid-master-page.component.html index 7f22fd465..17f371202 100644 --- a/frontend/src/app/components/liquid-master-page/liquid-master-page.component.html +++ b/frontend/src/app/components/liquid-master-page/liquid-master-page.component.html @@ -49,13 +49,13 @@ diff --git a/frontend/src/app/components/liquid-master-page/liquid-master-page.component.ts b/frontend/src/app/components/liquid-master-page/liquid-master-page.component.ts index d78cd457e..c57673529 100644 --- a/frontend/src/app/components/liquid-master-page/liquid-master-page.component.ts +++ b/frontend/src/app/components/liquid-master-page/liquid-master-page.component.ts @@ -3,6 +3,7 @@ import { Env, StateService } from '../../services/state.service'; import { merge, Observable, of} from 'rxjs'; import { LanguageService } from '../../services/language.service'; import { EnterpriseService } from '../../services/enterprise.service'; +import { NavigationService } from '../../services/navigation.service'; @Component({ selector: 'app-liquid-master-page', @@ -17,11 +18,13 @@ export class LiquidMasterPageComponent implements OnInit { officialMempoolSpace = this.stateService.env.OFFICIAL_MEMPOOL_SPACE; network$: Observable; urlLanguage: string; + networkPaths: { [network: string]: string }; constructor( private stateService: StateService, private languageService: LanguageService, private enterpriseService: EnterpriseService, + private navigationService: NavigationService, ) { } ngOnInit() { @@ -29,6 +32,10 @@ export class LiquidMasterPageComponent implements OnInit { this.connectionState$ = this.stateService.connectionState$; this.network$ = merge(of(''), this.stateService.networkChanged$); this.urlLanguage = this.languageService.getLanguageForUrl(); + this.navigationService.subnetPaths.subscribe((paths) => { + console.log('network paths updated...'); + this.networkPaths = paths; + }); } collapse(): void { diff --git a/frontend/src/app/components/master-page/master-page.component.html b/frontend/src/app/components/master-page/master-page.component.html index 1c28d5dce..5c365f0f9 100644 --- a/frontend/src/app/components/master-page/master-page.component.html +++ b/frontend/src/app/components/master-page/master-page.component.html @@ -22,13 +22,13 @@ diff --git a/frontend/src/app/components/master-page/master-page.component.ts b/frontend/src/app/components/master-page/master-page.component.ts index 994f0412f..8f7b4fecc 100644 --- a/frontend/src/app/components/master-page/master-page.component.ts +++ b/frontend/src/app/components/master-page/master-page.component.ts @@ -3,6 +3,7 @@ import { Env, StateService } from '../../services/state.service'; import { Observable, merge, of } from 'rxjs'; import { LanguageService } from '../../services/language.service'; import { EnterpriseService } from '../../services/enterprise.service'; +import { NavigationService } from '../../services/navigation.service'; @Component({ selector: 'app-master-page', @@ -18,11 +19,13 @@ export class MasterPageComponent implements OnInit { officialMempoolSpace = this.stateService.env.OFFICIAL_MEMPOOL_SPACE; urlLanguage: string; subdomain = ''; + networkPaths: { [network: string]: string }; constructor( public stateService: StateService, private languageService: LanguageService, private enterpriseService: EnterpriseService, + private navigationService: NavigationService, ) { } ngOnInit() { @@ -31,6 +34,10 @@ export class MasterPageComponent implements OnInit { this.network$ = merge(of(''), this.stateService.networkChanged$); this.urlLanguage = this.languageService.getLanguageForUrl(); this.subdomain = this.enterpriseService.getSubdomain(); + this.navigationService.subnetPaths.subscribe((paths) => { + console.log('network paths updated...'); + this.networkPaths = paths; + }); } collapse(): void { diff --git a/frontend/src/app/docs/docs.routing.module.ts b/frontend/src/app/docs/docs.routing.module.ts index 52eb54f2e..0f01016de 100644 --- a/frontend/src/app/docs/docs.routing.module.ts +++ b/frontend/src/app/docs/docs.routing.module.ts @@ -39,6 +39,7 @@ if (browserWindowEnv.BASE_MODULE && (browserWindowEnv.BASE_MODULE === 'bisq' || }, { path: 'faq', + data: { networks: ['bitcoin'] }, component: DocsComponent }, { diff --git a/frontend/src/app/graphs/graphs.routing.module.ts b/frontend/src/app/graphs/graphs.routing.module.ts index 57ef6cef7..bf0e0a0a7 100644 --- a/frontend/src/app/graphs/graphs.routing.module.ts +++ b/frontend/src/app/graphs/graphs.routing.module.ts @@ -37,10 +37,12 @@ const routes: Routes = [ children: [ { path: 'mining/pool/:slug', + data: { networks: ['bitcoin'] }, component: PoolComponent, }, { path: 'mining', + data: { networks: ['bitcoin'] }, component: StartComponent, children: [ { @@ -51,6 +53,7 @@ const routes: Routes = [ }, { path: 'mempool-block/:id', + data: { networks: ['bitcoin', 'liquid'] }, component: StartComponent, children: [ { @@ -61,62 +64,77 @@ const routes: Routes = [ }, { path: 'graphs', + data: { networks: ['bitcoin', 'liquid'] }, component: GraphsComponent, children: [ { path: 'mempool', + data: { networks: ['bitcoin', 'liquid'] }, component: StatisticsComponent, }, { path: 'mining/hashrate-difficulty', + data: { networks: ['bitcoin'] }, component: HashrateChartComponent, }, { path: 'mining/pools-dominance', + data: { networks: ['bitcoin'] }, component: HashrateChartPoolsComponent, }, { path: 'mining/pools', + data: { networks: ['bitcoin'] }, component: PoolRankingComponent, }, { path: 'mining/block-fees', + data: { networks: ['bitcoin'] }, component: BlockFeesGraphComponent, }, { path: 'mining/block-rewards', + data: { networks: ['bitcoin'] }, component: BlockRewardsGraphComponent, }, { path: 'mining/block-fee-rates', + data: { networks: ['bitcoin'] }, component: BlockFeeRatesGraphComponent, }, { path: 'mining/block-sizes-weights', + data: { networks: ['bitcoin'] }, component: BlockSizesWeightsGraphComponent, }, { path: 'lightning/nodes-networks', + data: { networks: ['bitcoin'] }, component: NodesNetworksChartComponent, }, { path: 'lightning/capacity', + data: { networks: ['bitcoin'] }, component: LightningStatisticsChartComponent, }, { path: 'lightning/nodes-per-isp', + data: { networks: ['bitcoin'] }, component: NodesPerISPChartComponent, }, { path: 'lightning/nodes-per-country', + data: { networks: ['bitcoin'] }, component: NodesPerCountryChartComponent, }, { path: 'lightning/nodes-map', + data: { networks: ['bitcoin'] }, component: NodesMap, }, { path: 'lightning/nodes-channels-map', + data: { networks: ['bitcoin'] }, component: NodesChannelsMap, }, { @@ -125,6 +143,7 @@ const routes: Routes = [ }, { path: 'mining/block-prediction', + data: { networks: ['bitcoin'] }, component: BlockPredictionGraphComponent, }, ] @@ -141,6 +160,7 @@ const routes: Routes = [ }, { path: 'tv', + data: { networks: ['bitcoin', 'liquid'] }, component: TelevisionComponent }, ]; diff --git a/frontend/src/app/lightning/lightning.routing.module.ts b/frontend/src/app/lightning/lightning.routing.module.ts index e2121f8e8..79c3bc297 100644 --- a/frontend/src/app/lightning/lightning.routing.module.ts +++ b/frontend/src/app/lightning/lightning.routing.module.ts @@ -21,10 +21,12 @@ const routes: Routes = [ }, { path: 'node/:public_key', + data: { networkSpecific: true }, component: NodeComponent, }, { path: 'channel/:short_id', + data: { networkSpecific: true }, component: ChannelComponent, }, { diff --git a/frontend/src/app/services/navigation.service.ts b/frontend/src/app/services/navigation.service.ts new file mode 100644 index 000000000..661a8c38f --- /dev/null +++ b/frontend/src/app/services/navigation.service.ts @@ -0,0 +1,90 @@ +import { Injectable } from '@angular/core'; +import { Router, ActivatedRoute, NavigationEnd, ActivatedRouteSnapshot } from '@angular/router'; +import { BehaviorSubject } from 'rxjs'; +import { filter, map } from 'rxjs/operators'; +import { StateService } from './state.service'; + +const networkModules = { + bitcoin: { + subnets: [ + { name: 'mainnet', path: '' }, + { name: 'testnet', path: '/testnet' }, + { name: 'signet', path: '/signet' }, + ], + }, + liquid: { + subnets: [ + { name: 'liquid', path: '' }, + { name: 'liquidtestnet', path: '/testnet' }, + ], + }, + bisq: { + subnets: [ + { name: 'bisq', path: '' }, + ], + }, +}; +const networks = Object.keys(networkModules); + +@Injectable({ + providedIn: 'root' +}) +export class NavigationService { + subnetPaths = new BehaviorSubject>({}); + + constructor( + private stateService: StateService, + private router: Router, + ) { + this.router.events.pipe( + filter(event => event instanceof NavigationEnd), + map(() => this.router.routerState.snapshot.root), + ).subscribe((state) => { + this.updateSubnetPaths(state); + }); + } + + // For each network (bitcoin/liquid/bisq), find and save the longest url path compatible with the current route + updateSubnetPaths(root: ActivatedRouteSnapshot): void { + let path = ''; + const networkPaths = {}; + let route = root; + // traverse the router state tree until all network paths are set, or we reach the end of the tree + while (!networks.reduce((acc, network) => acc && !!networkPaths[network], true) && route) { + // 'networkSpecific' paths may correspond to valid routes on other networks, but aren't directly compatible + // (e.g. we shouldn't link a mainnet transaction page to the same txid on testnet or liquid) + if (route.data?.networkSpecific) { + networks.forEach(network => { + if (networkPaths[network] == null) { + networkPaths[network] = path; + } + }); + } + // null or empty networks list is shorthand for "compatible with every network" + if (route.data?.networks?.length) { + // if the list is non-empty, only those networks are compatible + networks.forEach(network => { + if (!route.data.networks.includes(network)) { + if (networkPaths[network] == null) { + networkPaths[network] = path; + } + } + }); + } + if (route.url?.length) { + path = [path, ...route.url.map(segment => segment.path).filter(path => { + return path.length && !['testnet', 'signet'].includes(path); + })].join('/'); + } + route = route.firstChild; + } + + const subnetPaths = {}; + Object.entries(networkModules).forEach(([key, network]) => { + network.subnets.forEach(subnet => { + subnetPaths[subnet.name] = subnet.path + (networkPaths[key] != null ? networkPaths[key] : path); + }); + }); + this.subnetPaths.next(subnetPaths); + } +} From f4df51dd21558049f8de8c6f65ec147bd5676af2 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sat, 15 Oct 2022 00:45:48 +0000 Subject: [PATCH 06/17] API method for node fee histogram data --- backend/src/api/explorer/nodes.api.ts | 50 +++++++++++++++++++ backend/src/api/explorer/nodes.routes.ts | 17 +++++++ .../app/lightning/lightning-api.service.ts | 4 ++ 3 files changed, 71 insertions(+) diff --git a/backend/src/api/explorer/nodes.api.ts b/backend/src/api/explorer/nodes.api.ts index cbd70a34f..d8dceab19 100644 --- a/backend/src/api/explorer/nodes.api.ts +++ b/backend/src/api/explorer/nodes.api.ts @@ -129,6 +129,56 @@ class NodesApi { } } + public async $getFeeHistogram(node_public_key: string): Promise { + try { + const inQuery = ` + SELECT CASE WHEN fee_rate <= 10.0 THEN CEIL(fee_rate) + WHEN (fee_rate > 10.0 and fee_rate <= 100.0) THEN CEIL(fee_rate / 10.0) * 10.0 + WHEN (fee_rate > 100.0 and fee_rate <= 1000.0) THEN CEIL(fee_rate / 100.0) * 100.0 + WHEN fee_rate > 1000.0 THEN CEIL(fee_rate / 1000.0) * 1000.0 + END as bucket, + count(short_id) as count, + sum(capacity) as capacity + FROM ( + SELECT CASE WHEN node1_public_key = ? THEN node2_fee_rate WHEN node2_public_key = ? THEN node1_fee_rate END as fee_rate, + short_id as short_id, + capacity as capacity + FROM channels + WHERE status = 1 AND (channels.node1_public_key = ? OR channels.node2_public_key = ?) + ) as fee_rate_table + GROUP BY bucket; + `; + const [inRows]: any[] = await DB.query(inQuery, [node_public_key, node_public_key, node_public_key, node_public_key]); + + const outQuery = ` + SELECT CASE WHEN fee_rate <= 10.0 THEN CEIL(fee_rate) + WHEN (fee_rate > 10.0 and fee_rate <= 100.0) THEN CEIL(fee_rate / 10.0) * 10.0 + WHEN (fee_rate > 100.0 and fee_rate <= 1000.0) THEN CEIL(fee_rate / 100.0) * 100.0 + WHEN fee_rate > 1000.0 THEN CEIL(fee_rate / 1000.0) * 1000.0 + END as bucket, + count(short_id) as count, + sum(capacity) as capacity + FROM ( + SELECT CASE WHEN node1_public_key = ? THEN node1_fee_rate WHEN node2_public_key = ? THEN node2_fee_rate END as fee_rate, + short_id as short_id, + capacity as capacity + FROM channels + WHERE status = 1 AND (channels.node1_public_key = ? OR channels.node2_public_key = ?) + ) as fee_rate_table + GROUP BY bucket; + `; + const [outRows]: any[] = await DB.query(outQuery, [node_public_key, node_public_key, node_public_key, node_public_key]); + + return { + incoming: inRows.length > 0 ? inRows : [], + outgoing: outRows.length > 0 ? outRows : [], + }; + } catch (e) { + logger.err(`Cannot get node fee distribution for ${node_public_key}. Reason: ${(e instanceof Error ? e.message : e)}`); + throw e; + } + } + public async $getAllNodes(): Promise { try { const query = `SELECT * FROM nodes`; diff --git a/backend/src/api/explorer/nodes.routes.ts b/backend/src/api/explorer/nodes.routes.ts index 589e337cf..c19bde236 100644 --- a/backend/src/api/explorer/nodes.routes.ts +++ b/backend/src/api/explorer/nodes.routes.ts @@ -20,6 +20,7 @@ class NodesRoutes { .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/rankings/connectivity', this.$getTopNodesByChannels) .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/rankings/age', this.$getOldestNodes) .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/:public_key/statistics', this.$getHistoricalNodeStats) + .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/:public_key/fees/histogram', this.$getFeeHistogram) .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/:public_key', this.$getNode) .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/group/:name', this.$getNodeGroup) ; @@ -95,6 +96,22 @@ class NodesRoutes { } } + private async $getFeeHistogram(req: Request, res: Response) { + try { + const node = await nodesApi.$getFeeHistogram(req.params.public_key); + if (!node) { + res.status(404).send('Node not found'); + return; + } + res.header('Pragma', 'public'); + res.header('Cache-control', 'public'); + res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); + res.json(node); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } + private async $getNodesRanking(req: Request, res: Response): Promise { try { const topCapacityNodes = await nodesApi.$getTopCapacityNodes(false); diff --git a/frontend/src/app/lightning/lightning-api.service.ts b/frontend/src/app/lightning/lightning-api.service.ts index 7a38538ff..6ea550591 100644 --- a/frontend/src/app/lightning/lightning-api.service.ts +++ b/frontend/src/app/lightning/lightning-api.service.ts @@ -53,6 +53,10 @@ export class LightningApiService { return this.httpClient.get(this.apiBasePath + '/api/v1/lightning/nodes/' + publicKey + '/statistics'); } + getNodeFeeHistogram$(publicKey: string): Observable { + return this.httpClient.get(this.apiBasePath + '/api/v1/lightning/nodes/' + publicKey + '/fees/histogram'); + } + getNodesRanking$(): Observable { return this.httpClient.get(this.apiBasePath + '/api/v1/lightning/nodes/rankings'); } From 893aa03622227e83d4d73584830c0a7dbf57048c Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sat, 15 Oct 2022 01:19:45 +0000 Subject: [PATCH 07/17] Add fee histogram chart to the node page --- .../src/app/lightning/lightning.module.ts | 3 + .../node-fee-chart.component.html | 7 + .../node-fee-chart.component.scss | 5 + .../node-fee-chart.component.ts | 265 ++++++++++++++++++ .../app/lightning/node/node.component.html | 2 + 5 files changed, 282 insertions(+) create mode 100644 frontend/src/app/lightning/node-fee-chart/node-fee-chart.component.html create mode 100644 frontend/src/app/lightning/node-fee-chart/node-fee-chart.component.scss create mode 100644 frontend/src/app/lightning/node-fee-chart/node-fee-chart.component.ts diff --git a/frontend/src/app/lightning/lightning.module.ts b/frontend/src/app/lightning/lightning.module.ts index 3c06fb023..fa2f1a1ec 100644 --- a/frontend/src/app/lightning/lightning.module.ts +++ b/frontend/src/app/lightning/lightning.module.ts @@ -15,6 +15,7 @@ import { ChannelBoxComponent } from './channel/channel-box/channel-box.component import { ClosingTypeComponent } from './channel/closing-type/closing-type.component'; import { LightningStatisticsChartComponent } from './statistics-chart/lightning-statistics-chart.component'; import { NodeStatisticsChartComponent } from './node-statistics-chart/node-statistics-chart.component'; +import { NodeFeeChartComponent } from './node-fee-chart/node-fee-chart.component'; import { GraphsModule } from '../graphs/graphs.module'; import { NodesNetworksChartComponent } from './nodes-networks-chart/nodes-networks-chart.component'; import { ChannelsStatisticsComponent } from './channels-statistics/channels-statistics.component'; @@ -38,6 +39,7 @@ import { GroupComponent } from './group/group.component'; NodesListComponent, NodeStatisticsComponent, NodeStatisticsChartComponent, + NodeFeeChartComponent, NodeComponent, ChannelsListComponent, ChannelComponent, @@ -73,6 +75,7 @@ import { GroupComponent } from './group/group.component'; NodesListComponent, NodeStatisticsComponent, NodeStatisticsChartComponent, + NodeFeeChartComponent, NodeComponent, ChannelsListComponent, ChannelComponent, diff --git a/frontend/src/app/lightning/node-fee-chart/node-fee-chart.component.html b/frontend/src/app/lightning/node-fee-chart/node-fee-chart.component.html new file mode 100644 index 000000000..c8f674f11 --- /dev/null +++ b/frontend/src/app/lightning/node-fee-chart/node-fee-chart.component.html @@ -0,0 +1,7 @@ +
+

Fee distribution

+
+
+
d +
+
diff --git a/frontend/src/app/lightning/node-fee-chart/node-fee-chart.component.scss b/frontend/src/app/lightning/node-fee-chart/node-fee-chart.component.scss new file mode 100644 index 000000000..d738daa81 --- /dev/null +++ b/frontend/src/app/lightning/node-fee-chart/node-fee-chart.component.scss @@ -0,0 +1,5 @@ +.full-container { + margin-top: 25px; + margin-bottom: 25px; + min-height: 100%; +} diff --git a/frontend/src/app/lightning/node-fee-chart/node-fee-chart.component.ts b/frontend/src/app/lightning/node-fee-chart/node-fee-chart.component.ts new file mode 100644 index 000000000..f0370d3e1 --- /dev/null +++ b/frontend/src/app/lightning/node-fee-chart/node-fee-chart.component.ts @@ -0,0 +1,265 @@ +import { Component, Inject, Input, LOCALE_ID, OnInit, HostBinding } from '@angular/core'; +import { EChartsOption } from 'echarts'; +import { switchMap } from 'rxjs/operators'; +import { download } from '../../shared/graphs.utils'; +import { LightningApiService } from '../lightning-api.service'; +import { ActivatedRoute, ParamMap } from '@angular/router'; +import { AmountShortenerPipe } from '../../shared/pipes/amount-shortener.pipe'; + +@Component({ + selector: 'app-node-fee-chart', + templateUrl: './node-fee-chart.component.html', + styleUrls: ['./node-fee-chart.component.scss'], + styles: [` + .loadingGraphs { + position: absolute; + top: 50%; + left: calc(50% - 15px); + z-index: 100; + } + `], +}) +export class NodeFeeChartComponent implements OnInit { + chartOptions: EChartsOption = {}; + chartInitOptions = { + renderer: 'svg', + }; + + @HostBinding('attr.dir') dir = 'ltr'; + + isLoading = true; + chartInstance: any = undefined; + + constructor( + @Inject(LOCALE_ID) public locale: string, + private lightningApiService: LightningApiService, + private activatedRoute: ActivatedRoute, + private amountShortenerPipe: AmountShortenerPipe, + ) { + } + + ngOnInit(): void { + + this.activatedRoute.paramMap + .pipe( + switchMap((params: ParamMap) => { + this.isLoading = true; + return this.lightningApiService.getNodeFeeHistogram$(params.get('public_key')); + }), + ).subscribe((data) => { + if (data && data.incoming && data.outgoing) { + const outgoingHistogram = this.bucketsToHistogram(data.outgoing); + const incomingHistogram = this.bucketsToHistogram(data.incoming); + this.prepareChartOptions(outgoingHistogram, incomingHistogram); + } + this.isLoading = false; + }); + } + + bucketsToHistogram(buckets): { label: string, count: number, capacity: number}[] { + const histogram = []; + let increment = 1; + let lower = -increment; + let upper = 0; + + let nullBucket; + if (buckets.length && buckets[0] && buckets[0].bucket == null) { + nullBucket = buckets.shift(); + } + + while (upper <= 5000) { + let bucket; + if (buckets.length && buckets[0] && upper >= Number(buckets[0].bucket)) { + bucket = buckets.shift(); + } + histogram.push({ + label: upper === 0 ? '0 ppm' : `${lower} - ${upper} ppm`, + count: Number(bucket?.count || 0) + (upper === 0 ? Number(nullBucket?.count || 0) : 0), + capacity: Number(bucket?.capacity || 0) + (upper === 0 ? Number(nullBucket?.capacity || 0) : 0), + }); + + if (upper >= increment * 10) { + increment *= 10; + lower = increment; + upper = increment + increment; + } else { + lower += increment; + upper += increment; + } + } + const rest = buckets.reduce((acc, bucket) => { + acc.count += Number(bucket.count); + acc.capacity += Number(bucket.capacity); + return acc; + }, { count: 0, capacity: 0 }); + histogram.push({ + label: `5000+ ppm`, + count: rest.count, + capacity: rest.capacity, + }); + return histogram; + } + + prepareChartOptions(outgoingData, incomingData): void { + let title: object; + if (outgoingData.length === 0) { + title = { + textStyle: { + color: 'grey', + fontSize: 15 + }, + text: $localize`No data to display yet. Try again later.`, + left: 'center', + top: 'center' + }; + } + + this.chartOptions = { + title: outgoingData.length === 0 ? title : undefined, + animation: false, + grid: { + top: 30, + bottom: 20, + right: 20, + left: 65, + }, + tooltip: { + show: !this.isMobile(), + trigger: 'axis', + axisPointer: { + type: 'line' + }, + backgroundColor: 'rgba(17, 19, 31, 1)', + borderRadius: 4, + shadowColor: 'rgba(0, 0, 0, 0.5)', + textStyle: { + color: '#b1b1b1', + align: 'left', + }, + borderColor: '#000', + formatter: (ticks): string => { + return ` + ${ticks[0].data.label}
+
+ ${ticks[0].marker} Outgoing
+ Capacity: ${this.amountShortenerPipe.transform(ticks[0].data.capacity, 2, undefined, true)} sats
+ Channels: ${ticks[0].data.count}
+
+ ${ticks[1].marker} Incoming
+ Capacity: ${this.amountShortenerPipe.transform(ticks[1].data.capacity, 2, undefined, true)} sats
+ Channels: ${ticks[1].data.count}
+ `; + } + }, + xAxis: outgoingData.length === 0 ? undefined : { + type: 'category', + axisLine: { onZero: true }, + axisLabel: { + align: 'center', + fontSize: 11, + lineHeight: 12, + hideOverlap: true, + padding: [0, 5], + }, + data: outgoingData.map(bucket => bucket.label) + }, + legend: outgoingData.length === 0 ? undefined : { + padding: 10, + data: [ + { + name: 'Outgoing Fees', + inactiveColor: 'rgb(110, 112, 121)', + textStyle: { + color: 'white', + }, + icon: 'roundRect', + }, + { + name: 'Incoming Fees', + inactiveColor: 'rgb(110, 112, 121)', + textStyle: { + color: 'white', + }, + icon: 'roundRect', + }, + ], + }, + yAxis: outgoingData.length === 0 ? undefined : [ + { + type: 'value', + axisLabel: { + color: 'rgb(110, 112, 121)', + formatter: (val) => { + return `${this.amountShortenerPipe.transform(Math.abs(val), 2, undefined, true)} sats`; + } + }, + splitLine: { + lineStyle: { + type: 'dotted', + color: '#ffffff66', + opacity: 0.25, + } + }, + }, + ], + series: outgoingData.length === 0 ? undefined : [ + { + zlevel: 0, + name: 'Outgoing Fees', + data: outgoingData.map(bucket => ({ + value: bucket.capacity, + label: bucket.label, + capacity: bucket.capacity, + count: bucket.count, + })), + type: 'bar', + barWidth: '90%', + barMaxWidth: 50, + stack: 'fees', + }, + { + zlevel: 0, + name: 'Incoming Fees', + data: incomingData.map(bucket => ({ + value: -bucket.capacity, + label: bucket.label, + capacity: bucket.capacity, + count: bucket.count, + })), + type: 'bar', + barWidth: '90%', + barMaxWidth: 50, + stack: 'fees', + }, + ], + }; + } + + onChartInit(ec) { + if (this.chartInstance !== undefined) { + return; + } + + this.chartInstance = ec; + } + + isMobile() { + return (window.innerWidth <= 767.98); + } + + onSaveChart() { + // @ts-ignore + const prevBottom = this.chartOptions.grid.bottom; + // @ts-ignore + this.chartOptions.grid.bottom = 40; + this.chartOptions.backgroundColor = '#11131f'; + this.chartInstance.setOption(this.chartOptions); + download(this.chartInstance.getDataURL({ + pixelRatio: 2, + }), `node-fee-chart.svg`); + // @ts-ignore + this.chartOptions.grid.bottom = prevBottom; + this.chartOptions.backgroundColor = 'none'; + this.chartInstance.setOption(this.chartOptions); + } +} diff --git a/frontend/src/app/lightning/node/node.component.html b/frontend/src/app/lightning/node/node.component.html index c6e3e794c..7d506e6b0 100644 --- a/frontend/src/app/lightning/node/node.component.html +++ b/frontend/src/app/lightning/node/node.component.html @@ -140,6 +140,8 @@ + +

Open channels From 3e66e4d6dbf57acab80b628bb57d8b1cff8eb315 Mon Sep 17 00:00:00 2001 From: softsimon Date: Fri, 14 Oct 2022 05:09:25 +0400 Subject: [PATCH 08/17] Handle instant block, txid and address search fixes #2619 --- .../search-form/search-form.component.ts | 149 +++++++++++------- .../search-results.component.html | 28 +++- .../search-results.component.ts | 2 +- 3 files changed, 119 insertions(+), 60 deletions(-) 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 3cff7c188..dc9ea8940 100644 --- a/frontend/src/app/components/search-form/search-form.component.ts +++ b/frontend/src/app/components/search-form/search-form.component.ts @@ -3,8 +3,8 @@ import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import { AssetsService } from '../../services/assets.service'; import { StateService } from '../../services/state.service'; -import { Observable, of, Subject, zip, BehaviorSubject } from 'rxjs'; -import { debounceTime, distinctUntilChanged, switchMap, catchError, map } from 'rxjs/operators'; +import { Observable, of, Subject, zip, BehaviorSubject, combineLatest } from 'rxjs'; +import { debounceTime, distinctUntilChanged, switchMap, catchError, map, startWith, tap } from 'rxjs/operators'; import { ElectrsApiService } from '../../services/electrs-api.service'; import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; import { ApiService } from '../../services/api.service'; @@ -33,7 +33,7 @@ export class SearchFormComponent implements OnInit { @Output() searchTriggered = new EventEmitter(); @ViewChild('searchResults') searchResults: SearchResultsComponent; - @HostListener('keydown', ['$event']) keydown($event) { + @HostListener('keydown', ['$event']) keydown($event): void { this.handleKeyDown($event); } @@ -47,7 +47,7 @@ export class SearchFormComponent implements OnInit { private relativeUrlPipe: RelativeUrlPipe, ) { } - ngOnInit() { + ngOnInit(): void { this.stateService.networkChanged$.subscribe((network) => this.network = network); this.searchForm = this.formBuilder.group({ @@ -61,70 +61,111 @@ export class SearchFormComponent implements OnInit { }); } - this.typeAhead$ = this.searchForm.get('searchText').valueChanges - .pipe( - map((text) => { - if (this.network === 'bisq' && text.match(/^(b)[^c]/i)) { - return text.substr(1); - } - return text.trim(); - }), - debounceTime(200), - distinctUntilChanged(), - switchMap((text) => { - if (!text.length) { - return of([ - '', - [], - { - nodes: [], - channels: [], - } - ]); - } - this.isTypeaheading$.next(true); - if (!this.stateService.env.LIGHTNING) { - return zip( - of(text), - this.electrsApiService.getAddressesByPrefix$(text).pipe(catchError(() => of([]))), - [{ nodes: [], channels: [] }], - of(this.regexBlockheight.test(text)), - ); - } + const searchText$ = this.searchForm.get('searchText').valueChanges + .pipe( + map((text) => { + if (this.network === 'bisq' && text.match(/^(b)[^c]/i)) { + return text.substr(1); + } + return text.trim(); + }), + distinctUntilChanged(), + ); + + const searchResults$ = searchText$.pipe( + debounceTime(200), + switchMap((text) => { + if (!text.length) { + return of([ + [], + { nodes: [], channels: [] } + ]); + } + this.isTypeaheading$.next(true); + if (!this.stateService.env.LIGHTNING) { return zip( - of(text), this.electrsApiService.getAddressesByPrefix$(text).pipe(catchError(() => of([]))), - this.apiService.lightningSearch$(text).pipe(catchError(() => of({ + [{ nodes: [], channels: [] }], + ); + } + return zip( + this.electrsApiService.getAddressesByPrefix$(text).pipe(catchError(() => of([]))), + this.apiService.lightningSearch$(text).pipe(catchError(() => of({ + nodes: [], + channels: [], + }))), + ); + }), + tap((result: any[]) => { + this.isTypeaheading$.next(false); + }) + ); + + this.typeAhead$ = combineLatest( + [ + searchText$, + searchResults$.pipe( + startWith([ + [], + { + nodes: [], + channels: [], + } + ])) + ] + ).pipe( + map((latestData) => { + const searchText = latestData[0]; + if (!searchText.length) { + return { + searchText: '', + hashQuickMatch: false, + blockHeight: false, + txId: false, + address: false, + addresses: [], nodes: [], channels: [], - }))), - ); - }), - map((result: any[]) => { - this.isTypeaheading$.next(false); - if (this.network === 'bisq') { - return result[0].map((address: string) => 'B' + address); + }; } + + const result = latestData[1]; + const addressPrefixSearchResults = result[0]; + const lightningResults = result[1]; + + if (this.network === 'bisq') { + return searchText.map((address: string) => 'B' + address); + } + + const matchesBlockHeight = this.regexBlockheight.test(searchText); + const matchesTxId = this.regexTransaction.test(searchText) && !this.regexBlockhash.test(searchText); + const matchesBlockHash = this.regexBlockhash.test(searchText); + const matchesAddress = this.regexAddress.test(searchText); + return { - searchText: result[0], - blockHeight: this.regexBlockheight.test(result[0]) ? [parseInt(result[0], 10)] : [], - addresses: result[1], - nodes: result[2].nodes, - channels: result[2].channels, - totalResults: result[1].length + result[2].nodes.length + result[2].channels.length, + searchText: searchText, + hashQuickMatch: +(matchesBlockHeight || matchesBlockHash || matchesTxId || matchesAddress), + blockHeight: matchesBlockHeight, + txId: matchesTxId, + blockHash: matchesBlockHash, + address: matchesAddress, + addresses: addressPrefixSearchResults, + nodes: lightningResults.nodes, + channels: lightningResults.channels, }; }) ); } - handleKeyDown($event) { + + handleKeyDown($event): void { this.searchResults.handleKeyDown($event); } - itemSelected() { + itemSelected(): void { setTimeout(() => this.search()); } - selectedResult(result: any) { + selectedResult(result: any): void { if (typeof result === 'string') { this.search(result); } else if (typeof result === 'number') { @@ -136,7 +177,7 @@ export class SearchFormComponent implements OnInit { } } - search(result?: string) { + search(result?: string): void { const searchText = result || this.searchForm.value.searchText.trim(); if (searchText) { this.isSearching = true; @@ -170,7 +211,7 @@ export class SearchFormComponent implements OnInit { } } - navigate(url: string, searchText: string, extras?: any) { + navigate(url: string, searchText: string, extras?: any): void { this.router.navigate([this.relativeUrlPipe.transform(url), searchText], extras); this.searchTriggered.emit(); this.searchForm.setValue({ diff --git a/frontend/src/app/components/search-form/search-results/search-results.component.html b/frontend/src/app/components/search-form/search-results/search-results.component.html index 9ed829aff..4e8b9bb7c 100644 --- a/frontend/src/app/components/search-form/search-results/search-results.component.html +++ b/frontend/src/app/components/search-form/search-results/search-results.component.html @@ -1,14 +1,32 @@ -