Merge pull request #4543 from natsee/cross-network-address-search
Search bar feature: network based regex and address matches
This commit is contained in:
@@ -2,13 +2,14 @@ import { Component, OnInit, ChangeDetectionStrategy, EventEmitter, Output, ViewC
|
||||
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
|
||||
import { EventType, NavigationStart, Router } from '@angular/router';
|
||||
import { AssetsService } from '../../services/assets.service';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { Env, StateService } from '../../services/state.service';
|
||||
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';
|
||||
import { SearchResultsComponent } from './search-results/search-results.component';
|
||||
import { Network, findOtherNetworks, getRegex, getTargetUrl, needBaseModuleChange } from '../../shared/regex.utils';
|
||||
|
||||
@Component({
|
||||
selector: 'app-search-form',
|
||||
@@ -18,7 +19,7 @@ import { SearchResultsComponent } from './search-results/search-results.componen
|
||||
})
|
||||
export class SearchFormComponent implements OnInit {
|
||||
@Input() hamburgerOpen = false;
|
||||
|
||||
env: Env;
|
||||
network = '';
|
||||
assets: object = {};
|
||||
isSearching = false;
|
||||
@@ -36,12 +37,13 @@ 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}|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}$/;
|
||||
regexDate = /^(?:\d{4}[-/]\d{1,2}[-/]\d{1,2}(?: \d{1,2}:\d{2})?)$/;
|
||||
regexUnixTimestamp = /^\d{10}$/;
|
||||
regexAddress = getRegex('address', 'mainnet'); // Default to mainnet
|
||||
regexBlockhash = getRegex('blockhash', 'mainnet');
|
||||
regexTransaction = getRegex('transaction');
|
||||
regexBlockheight = getRegex('blockheight');
|
||||
regexDate = getRegex('date');
|
||||
regexUnixTimestamp = getRegex('timestamp');
|
||||
|
||||
focus$ = new Subject<string>();
|
||||
click$ = new Subject<string>();
|
||||
|
||||
@@ -66,8 +68,14 @@ export class SearchFormComponent implements OnInit {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.stateService.networkChanged$.subscribe((network) => this.network = network);
|
||||
|
||||
this.env = this.stateService.env;
|
||||
this.stateService.networkChanged$.subscribe((network) => {
|
||||
this.network = network;
|
||||
// TODO: Eventually change network type here from string to enum of consts
|
||||
this.regexAddress = getRegex('address', network as any || 'mainnet');
|
||||
this.regexBlockhash = getRegex('blockhash', network as any || 'mainnet');
|
||||
});
|
||||
|
||||
this.router.events.subscribe((e: NavigationStart) => { // Reset search focus when changing page
|
||||
if (this.searchInput && e.type === EventType.NavigationStart) {
|
||||
this.searchInput.nativeElement.blur();
|
||||
@@ -96,9 +104,6 @@ export class SearchFormComponent implements OnInit {
|
||||
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();
|
||||
}),
|
||||
tap((text) => {
|
||||
@@ -132,9 +137,6 @@ export class SearchFormComponent implements OnInit {
|
||||
);
|
||||
}),
|
||||
map((result: any[]) => {
|
||||
if (this.network === 'bisq') {
|
||||
result[0] = result[0].map((address: string) => 'B' + address);
|
||||
}
|
||||
return result;
|
||||
}),
|
||||
tap(() => {
|
||||
@@ -164,6 +166,7 @@ export class SearchFormComponent implements OnInit {
|
||||
blockHeight: false,
|
||||
txId: false,
|
||||
address: false,
|
||||
otherNetworks: [],
|
||||
addresses: [],
|
||||
nodes: [],
|
||||
channels: [],
|
||||
@@ -174,15 +177,21 @@ export class SearchFormComponent implements OnInit {
|
||||
const addressPrefixSearchResults = result[0];
|
||||
const lightningResults = result[1];
|
||||
|
||||
// Do not show date and timestamp results for liquid and bisq
|
||||
const isNetworkBitcoin = this.network === '' || this.network === 'testnet' || this.network === 'signet';
|
||||
|
||||
const matchesBlockHeight = this.regexBlockheight.test(searchText) && parseInt(searchText) <= this.stateService.latestBlockHeight;
|
||||
const matchesDateTime = this.regexDate.test(searchText) && new Date(searchText).toString() !== 'Invalid Date';
|
||||
const matchesUnixTimestamp = this.regexUnixTimestamp.test(searchText);
|
||||
const matchesDateTime = this.regexDate.test(searchText) && new Date(searchText).toString() !== 'Invalid Date' && new Date(searchText).getTime() <= Date.now() && isNetworkBitcoin;
|
||||
const matchesUnixTimestamp = this.regexUnixTimestamp.test(searchText) && parseInt(searchText) <= Math.floor(Date.now() / 1000) && isNetworkBitcoin;
|
||||
const matchesTxId = this.regexTransaction.test(searchText) && !this.regexBlockhash.test(searchText);
|
||||
const matchesBlockHash = this.regexBlockhash.test(searchText);
|
||||
const matchesAddress = !matchesTxId && this.regexAddress.test(searchText);
|
||||
let matchesAddress = !matchesTxId && this.regexAddress.test(searchText);
|
||||
const otherNetworks = findOtherNetworks(searchText, this.network as any || 'mainnet', this.env);
|
||||
|
||||
if (matchesAddress && this.network === 'bisq') {
|
||||
searchText = 'B' + searchText;
|
||||
// Add B prefix to addresses in Bisq network
|
||||
if (!matchesAddress && this.network === 'bisq' && getRegex('address', 'mainnet').test(searchText)) {
|
||||
searchText = 'B' + searchText;
|
||||
matchesAddress = !matchesTxId && this.regexAddress.test(searchText);
|
||||
}
|
||||
|
||||
if (matchesDateTime && searchText.indexOf('/') !== -1) {
|
||||
@@ -198,7 +207,8 @@ export class SearchFormComponent implements OnInit {
|
||||
txId: matchesTxId,
|
||||
blockHash: matchesBlockHash,
|
||||
address: matchesAddress,
|
||||
addresses: addressPrefixSearchResults,
|
||||
addresses: matchesAddress && addressPrefixSearchResults.length === 1 && searchText === addressPrefixSearchResults[0] ? [] : addressPrefixSearchResults, // If there is only one address and it matches the search text, don't show it in the dropdown
|
||||
otherNetworks: otherNetworks,
|
||||
nodes: lightningResults.nodes,
|
||||
channels: lightningResults.channels,
|
||||
};
|
||||
@@ -223,6 +233,15 @@ export class SearchFormComponent implements OnInit {
|
||||
this.navigate('/lightning/node/', result.public_key);
|
||||
} else if (result.short_id) {
|
||||
this.navigate('/lightning/channel/', result.id);
|
||||
} else if (result.network) {
|
||||
if (result.isNetworkAvailable) {
|
||||
this.navigate('/address/', result.address, undefined, result.network);
|
||||
} else {
|
||||
this.searchForm.setValue({
|
||||
searchText: '',
|
||||
});
|
||||
this.isSearching = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,6 +249,7 @@ export class SearchFormComponent implements OnInit {
|
||||
const searchText = result || this.searchForm.value.searchText.trim();
|
||||
if (searchText) {
|
||||
this.isSearching = true;
|
||||
|
||||
if (!this.regexTransaction.test(searchText) && this.regexAddress.test(searchText)) {
|
||||
this.navigate('/address/', searchText);
|
||||
} else if (this.regexBlockhash.test(searchText)) {
|
||||
@@ -258,6 +278,11 @@ export class SearchFormComponent implements OnInit {
|
||||
} else if (this.regexDate.test(searchText) || this.regexUnixTimestamp.test(searchText)) {
|
||||
let timestamp: number;
|
||||
this.regexDate.test(searchText) ? timestamp = Math.floor(new Date(searchText).getTime() / 1000) : timestamp = searchText;
|
||||
// Check if timestamp is too far in the future or before the genesis block
|
||||
if (timestamp > Math.floor(Date.now() / 1000)) {
|
||||
this.isSearching = false;
|
||||
return;
|
||||
}
|
||||
this.apiService.getBlockDataFromTimestamp$(timestamp).subscribe(
|
||||
(data) => { this.navigate('/block/', data.hash); },
|
||||
(error) => { console.log(error); this.isSearching = false; }
|
||||
@@ -269,12 +294,17 @@ export class SearchFormComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
navigate(url: string, searchText: string, extras?: any): void {
|
||||
this.router.navigate([this.relativeUrlPipe.transform(url), searchText], extras);
|
||||
this.searchTriggered.emit();
|
||||
this.searchForm.setValue({
|
||||
searchText: '',
|
||||
});
|
||||
this.isSearching = false;
|
||||
|
||||
navigate(url: string, searchText: string, extras?: any, swapNetwork?: string) {
|
||||
if (needBaseModuleChange(this.env.BASE_MODULE as 'liquid' | 'bisq' | 'mempool', swapNetwork as Network)) {
|
||||
window.location.href = getTargetUrl(swapNetwork as Network, searchText, this.env);
|
||||
} else {
|
||||
this.router.navigate([this.relativeUrlPipe.transform(url, swapNetwork), searchText], extras);
|
||||
this.searchTriggered.emit();
|
||||
this.searchForm.setValue({
|
||||
searchText: '',
|
||||
});
|
||||
this.isSearching = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="dropdown-menu show" *ngIf="results" [hidden]="!results.hashQuickMatch && !results.addresses.length && !results.nodes.length && !results.channels.length">
|
||||
<div class="dropdown-menu show" *ngIf="results" [hidden]="!results.hashQuickMatch && !results.otherNetworks.length && !results.addresses.length && !results.nodes.length && !results.channels.length">
|
||||
<ng-template [ngIf]="results.blockHeight">
|
||||
<div class="card-title" i18n="search.bitcoin-block-height">Bitcoin Block Height</div>
|
||||
<button (click)="clickItem(0)" [class.active]="0 === activeIdx" type="button" role="option" class="dropdown-item">
|
||||
@@ -35,10 +35,18 @@
|
||||
<ng-container *ngTemplateOutlet="goTo; context: { $implicit: results.searchText | shortenString : 13 }"></ng-container>
|
||||
</button>
|
||||
</ng-template>
|
||||
<ng-template [ngIf]="results.addresses.length && !(results.address && results.addresses.length === 1 && results.searchText === results.addresses[0])">
|
||||
<ng-template [ngIf]="results.otherNetworks.length">
|
||||
<div class="card-title danger" i18n="search.other-networks">Other Network Address</div>
|
||||
<ng-template ngFor [ngForOf]="results.otherNetworks" let-otherNetwork let-i="index">
|
||||
<button (click)="clickItem(results.hashQuickMatch + i)" [class.active]="(results.hashQuickMatch + i) === activeIdx" [class.inactive]="!otherNetwork.isNetworkAvailable" type="button" role="option" class="dropdown-item">
|
||||
<ng-container *ngTemplateOutlet="goTo; context: { $implicit: otherNetwork.address| shortenString : isMobile ? 20 : 25 }"></ng-container> <b>({{ otherNetwork.network.charAt(0).toUpperCase() + otherNetwork.network.slice(1) }})</b>
|
||||
</button>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
<ng-template [ngIf]="results.addresses.length">
|
||||
<div class="card-title" i18n="search.bitcoin-addresses">Bitcoin Addresses</div>
|
||||
<ng-template ngFor [ngForOf]="results.addresses" let-address let-i="index">
|
||||
<button (click)="clickItem(results.hashQuickMatch + i)" [class.active]="(results.hashQuickMatch + i) === activeIdx" type="button" role="option" class="dropdown-item">
|
||||
<button (click)="clickItem(results.hashQuickMatch + results.otherNetworks.length + i)" [class.active]="(results.hashQuickMatch + results.otherNetworks.length + i) === activeIdx" type="button" role="option" class="dropdown-item">
|
||||
<ngb-highlight [result]="address | shortenString : isMobile ? 25 : 36" [term]="results.searchText"></ngb-highlight>
|
||||
</button>
|
||||
</ng-template>
|
||||
@@ -46,7 +54,7 @@
|
||||
<ng-template [ngIf]="results.nodes.length">
|
||||
<div class="card-title" i18n="search.lightning-nodes">Lightning Nodes</div>
|
||||
<ng-template ngFor [ngForOf]="results.nodes" let-node let-i="index">
|
||||
<button (click)="clickItem(results.hashQuickMatch + results.addresses.length + i)" [class.inactive]="node.status === 0" [class.active]="results.hashQuickMatch + results.addresses.length + i === activeIdx" [routerLink]="['/lightning/node' | relativeUrl, node.public_key]" type="button" role="option" class="dropdown-item">
|
||||
<button (click)="clickItem(results.hashQuickMatch + results.otherNetworks.length + results.addresses.length + i)" [class.inactive]="node.status === 0" [class.active]="results.hashQuickMatch + results.otherNetworks.length + results.addresses.length + i === activeIdx" [routerLink]="['/lightning/node' | relativeUrl, node.public_key]" type="button" role="option" class="dropdown-item">
|
||||
<ngb-highlight [result]="node.alias" [term]="results.searchText"></ngb-highlight> <span class="symbol">{{ node.public_key | shortenString : 10 }}</span>
|
||||
</button>
|
||||
</ng-template>
|
||||
@@ -54,7 +62,7 @@
|
||||
<ng-template [ngIf]="results.channels.length">
|
||||
<div class="card-title" i18n="search.lightning-channels">Lightning Channels</div>
|
||||
<ng-template ngFor [ngForOf]="results.channels" let-channel let-i="index">
|
||||
<button (click)="clickItem(results.hashQuickMatch + results.addresses.length + results.nodes.length + i)" [class.inactive]="channel.status === 2" [class.active]="results.hashQuickMatch + results.addresses.length + results.nodes.length + i === activeIdx" type="button" role="option" class="dropdown-item">
|
||||
<button (click)="clickItem(results.hashQuickMatch + results.otherNetworks.length + results.addresses.length + results.nodes.length + i)" [class.inactive]="channel.status === 2" [class.active]="results.hashQuickMatch + results.otherNetworks.length + results.addresses.length + results.nodes.length + i === activeIdx" type="button" role="option" class="dropdown-item">
|
||||
<ngb-highlight [result]="channel.short_id" [term]="results.searchText"></ngb-highlight> <span class="symbol">{{ channel.id }}</span>
|
||||
</button>
|
||||
</ng-template>
|
||||
|
||||
@@ -7,6 +7,10 @@
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.danger {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
position: absolute;
|
||||
top: 42px;
|
||||
|
||||
@@ -22,7 +22,7 @@ export class SearchResultsComponent implements OnChanges {
|
||||
ngOnChanges() {
|
||||
this.activeIdx = 0;
|
||||
if (this.results) {
|
||||
this.resultsFlattened = [...(this.results.hashQuickMatch ? [this.results.searchText] : []), ...this.results.addresses, ...this.results.nodes, ...this.results.channels];
|
||||
this.resultsFlattened = [...(this.results.hashQuickMatch ? [this.results.searchText] : []), ...this.results.otherNetworks, ...this.results.addresses, ...this.results.nodes, ...this.results.channels];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,9 @@ export class SearchResultsComponent implements OnChanges {
|
||||
break;
|
||||
case 'Enter':
|
||||
event.preventDefault();
|
||||
if (this.resultsFlattened[this.activeIdx]?.isNetworkAvailable === false) {
|
||||
return;
|
||||
}
|
||||
if (this.resultsFlattened[this.activeIdx]) {
|
||||
this.selectedResult.emit(this.resultsFlattened[this.activeIdx]);
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user