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 446d8be5a..2a9983a24 100644 --- a/frontend/src/app/components/search-form/search-form.component.ts +++ b/frontend/src/app/components/search-form/search-form.component.ts @@ -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(); click$ = new Subject(); @@ -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; + } } } 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 6c096fc4e..5d64b7495 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,4 +1,4 @@ -