mirror of
https://github.com/mempool/mempool.git
synced 2025-02-20 13:34:40 +01:00
Merge branch 'master' into lightning-dashboard-ownership
This commit is contained in:
commit
8cb49ba8e3
18 changed files with 676 additions and 74 deletions
|
@ -263,8 +263,13 @@ export class Common {
|
|||
case 'v0_p2wsh': flags |= TransactionFlags.p2wsh; break;
|
||||
case 'v1_p2tr': {
|
||||
flags |= TransactionFlags.p2tr;
|
||||
if (vin.witness.length > 2) {
|
||||
const asm = vin.inner_witnessscript_asm || transactionUtils.convertScriptSigAsm(vin.witness[vin.witness.length - 2]);
|
||||
// in taproot, if the last witness item begins with 0x50, it's an annex
|
||||
const hasAnnex = vin.witness?.[vin.witness.length - 1].startsWith('50');
|
||||
// script spends have more than one witness item, not counting the annex (if present)
|
||||
if (vin.witness.length > (hasAnnex ? 2 : 1)) {
|
||||
// the script itself is the second-to-last witness item, not counting the annex
|
||||
const asm = vin.inner_witnessscript_asm || transactionUtils.convertScriptSigAsm(vin.witness[vin.witness.length - (hasAnnex ? 3 : 2)]);
|
||||
// inscriptions smuggle data within an 'OP_0 OP_IF ... OP_ENDIF' envelope
|
||||
if (asm?.includes('OP_0 OP_IF')) {
|
||||
flags |= TransactionFlags.inscription;
|
||||
}
|
||||
|
|
14
frontend/package-lock.json
generated
14
frontend/package-lock.json
generated
|
@ -58,7 +58,7 @@
|
|||
"optionalDependencies": {
|
||||
"@cypress/schematic": "^2.5.0",
|
||||
"@types/cypress": "^1.1.3",
|
||||
"cypress": "^13.6.0",
|
||||
"cypress": "^13.6.2",
|
||||
"cypress-fail-on-console-error": "~5.1.0",
|
||||
"cypress-wait-until": "^2.0.1",
|
||||
"mock-socket": "~9.3.1",
|
||||
|
@ -7083,9 +7083,9 @@
|
|||
"peer": true
|
||||
},
|
||||
"node_modules/cypress": {
|
||||
"version": "13.6.0",
|
||||
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.6.0.tgz",
|
||||
"integrity": "sha512-quIsnFmtj4dBUEJYU4OH0H12bABJpSujvWexC24Ju1gTlKMJbeT6tTO0vh7WNfiBPPjoIXLN+OUqVtiKFs6SGw==",
|
||||
"version": "13.6.2",
|
||||
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.6.2.tgz",
|
||||
"integrity": "sha512-TW3bGdPU4BrfvMQYv1z3oMqj71YI4AlgJgnrycicmPZAXtvywVFZW9DAToshO65D97rCWfG/kqMFsYB6Kp91gQ==",
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
|
@ -22272,9 +22272,9 @@
|
|||
"peer": true
|
||||
},
|
||||
"cypress": {
|
||||
"version": "13.6.0",
|
||||
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.6.0.tgz",
|
||||
"integrity": "sha512-quIsnFmtj4dBUEJYU4OH0H12bABJpSujvWexC24Ju1gTlKMJbeT6tTO0vh7WNfiBPPjoIXLN+OUqVtiKFs6SGw==",
|
||||
"version": "13.6.2",
|
||||
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.6.2.tgz",
|
||||
"integrity": "sha512-TW3bGdPU4BrfvMQYv1z3oMqj71YI4AlgJgnrycicmPZAXtvywVFZW9DAToshO65D97rCWfG/kqMFsYB6Kp91gQ==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"@cypress/request": "^3.0.0",
|
||||
|
|
|
@ -110,7 +110,7 @@
|
|||
"optionalDependencies": {
|
||||
"@cypress/schematic": "^2.5.0",
|
||||
"@types/cypress": "^1.1.3",
|
||||
"cypress": "^13.6.0",
|
||||
"cypress": "^13.6.2",
|
||||
"cypress-fail-on-console-error": "~5.1.0",
|
||||
"cypress-wait-until": "^2.0.1",
|
||||
"mock-socket": "~9.3.1",
|
||||
|
|
|
@ -47,7 +47,7 @@
|
|||
<tr>
|
||||
<td i18n="block.timestamp">Timestamp</td>
|
||||
<td>
|
||||
<app-timestamp [unixTime]="block.timestamp" [precision]="1" minUnit="minute"></app-timestamp>
|
||||
<app-timestamp [customFormat]="'yyyy-MM-dd HH:mm:ss'" [unixTime]="block.timestamp" [precision]="1" minUnit="minute"></app-timestamp>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
@ -233,7 +233,9 @@
|
|||
<ng-container *ngIf="!isMobile || mode !== 'actual'; else emptyBlockInfo"></ng-container>
|
||||
</div>
|
||||
<ng-container *ngIf="network !== 'liquid'">
|
||||
<ng-container *ngTemplateOutlet="isMobile && mode === 'actual' ? actualDetails : expectedDetails"></ng-container>
|
||||
<ng-template [ngIf]="!isLoadingOverview" [ngIfElse]="loadingDetailsSkeletons">
|
||||
<ng-container *ngTemplateOutlet="isMobile && mode === 'actual' ? actualDetails : expectedDetails"></ng-container>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="col-sm" *ngIf="!isMobile">
|
||||
|
@ -245,7 +247,9 @@
|
|||
<ng-container *ngTemplateOutlet="emptyBlockInfo"></ng-container>
|
||||
</div>
|
||||
<ng-container *ngIf="network !== 'liquid'">
|
||||
<ng-container *ngTemplateOutlet="actualDetails"></ng-container>
|
||||
<ng-template [ngIf]="!isLoadingOverview" [ngIfElse]="loadingDetailsSkeletons">
|
||||
<ng-container *ngTemplateOutlet="actualDetails"></ng-container>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -452,5 +456,24 @@
|
|||
</table>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #loadingDetailsSkeletons>
|
||||
<table class="table table-borderless table-striped audit-details-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="w-50" i18n="block.total-fees|Total fees in a block">Total fees</td>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="block.weight">Weight</td>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="mempool-block.transactions">Transactions</td>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</ng-template>
|
||||
|
||||
<br>
|
||||
<br>
|
||||
|
|
|
@ -46,7 +46,7 @@
|
|||
</div>
|
||||
</td>
|
||||
<td class="timestamp" *ngIf="!widget" [ngClass]="{'widget': widget, 'legacy': !isMempoolModule}">
|
||||
‎{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}
|
||||
‎{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm:ss' }}
|
||||
</td>
|
||||
<td *ngIf="auditAvailable" class="health text-right" [ngClass]="{'widget': widget, 'legacy': !isMempoolModule}">
|
||||
<a
|
||||
|
|
|
@ -224,7 +224,7 @@
|
|||
<a [routerLink]="['/block' | relativeUrl, block.id]">{{ block.height }}</a>
|
||||
</td>
|
||||
<td class="timestamp">
|
||||
‎{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}
|
||||
‎{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm:ss' }}
|
||||
</td>
|
||||
<td class="mined">
|
||||
<app-time kind="since" [time]="block.timestamp" [fastRender]="true"></app-time>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -87,8 +87,8 @@
|
|||
<th class="table-cell-new-fee" i18n="dashboard.new-transaction-fee">New fee</th>
|
||||
<th class="table-cell-badges" i18n="transaction.status|Transaction Status">Status</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let replacement of replacements$ | async;">
|
||||
<tbody *ngIf="replacements$ | async as replacements; else replacementsSkeleton">
|
||||
<tr *ngFor="let replacement of replacements">
|
||||
<td class="table-cell-txid">
|
||||
<a [routerLink]="['/tx' | relativeUrl, replacement.txid]">
|
||||
<app-truncate [text]="replacement.txid" [lastChars]="5"></app-truncate>
|
||||
|
@ -158,8 +158,8 @@
|
|||
<th class="table-cell-fiat" *ngIf="(network$ | async) === ''">{{ currency }}</th>
|
||||
<th class="table-cell-fees" i18n="transaction.fee|Transaction fee">Fee</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let transaction of transactions$ | async; let i = index;">
|
||||
<tbody *ngIf="transactions$ | async as transactions else recentTransactionsSkeleton">
|
||||
<tr *ngFor="let transaction of transactions; let i = index;">
|
||||
<td class="table-cell-txid">
|
||||
<a [routerLink]="['/tx' | relativeUrl, transaction.txid]">
|
||||
<app-truncate [text]="transaction.txid" [lastChars]="5"></app-truncate>
|
||||
|
@ -199,6 +199,28 @@
|
|||
</table>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #replacementsSkeleton>
|
||||
<tbody>
|
||||
<tr *ngFor="let i of [1,2,3,4,5,6]">
|
||||
<td class="table-cell-txid"><div class="skeleton-loader skeleton-loader-transactions"></div></td>
|
||||
<td class="table-cell-old-fee"><div class="skeleton-loader skeleton-loader-transactions"></div></td>
|
||||
<td class="table-cell-new-fee"><div class="skeleton-loader skeleton-loader-transactions"></div></td>
|
||||
<td class="table-cell-badges"><div class="skeleton-loader skeleton-loader-transactions"></div></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #recentTransactionsSkeleton>
|
||||
<tbody>
|
||||
<tr *ngFor="let i of [1,2,3,4,5,6]">
|
||||
<td class="table-cell-txid"><div class="skeleton-loader skeleton-loader-transactions"></div> </td>
|
||||
<td class="table-cell-satoshis"><div class="skeleton-loader skeleton-loader-transactions"></div></td>
|
||||
<td class="table-cell-fiat" *ngIf="(network$ | async) === ''"><div class="skeleton-loader skeleton-loader-transactions"></div></td>
|
||||
<td class="table-cell-fees"><div class="skeleton-loader skeleton-loader-transactions"></div></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #loadingTransactions>
|
||||
<div class="skeleton-loader skeleton-loader-transactions"></div>
|
||||
</ng-template>
|
||||
|
|
|
@ -7,11 +7,11 @@
|
|||
<app-svg-images *ngIf="officialMempoolSpace" name="officialMempoolSpace" viewBox="0 0 500 126"></app-svg-images>
|
||||
<app-svg-images *ngIf="!officialMempoolSpace" name="mempoolSpace" viewBox="0 0 500 126"></app-svg-images>
|
||||
</div>
|
||||
<p class="d-block d-sm-none">
|
||||
<p class="explore-tagline-mobile">
|
||||
<ng-container i18n="@@7deec1c1520f06170e1f8e8ddfbe4532312f638f">Explore the full Bitcoin ecosystem</ng-container>
|
||||
<ng-template [ngIf]="locale.substr(0, 2) === 'en'"> ™</ng-template>
|
||||
</p>
|
||||
<div class="site-options float-right d-flex justify-content-center align-items-center" [class]="{'services': isServicesPage}">
|
||||
<div class="site-options language-selector d-flex justify-content-center align-items-center" [class]="{'services': isServicesPage}">
|
||||
<div class="selector">
|
||||
<app-language-selector></app-language-selector>
|
||||
</div>
|
||||
|
@ -26,11 +26,11 @@
|
|||
<span *ngIf="!loggedIn" i18n="shared.sign-in">Sign In</span>
|
||||
</a>
|
||||
</div>
|
||||
<a *ngIf="servicesEnabled" class="btn btn-purple sponsor d-flex d-sm-none justify-content-center ml-auto mr-auto mt-3 mb-2" [routerLink]="['/login' | relativeUrl]">
|
||||
<a *ngIf="servicesEnabled" class="btn btn-purple sponsor d-flex d-sm-none justify-content-center ml-auto mr-auto mt-0 mb-2" [routerLink]="['/login' | relativeUrl]">
|
||||
<span *ngIf="loggedIn" i18n="shared.my-account">My Account</span>
|
||||
<span *ngIf="!loggedIn" i18n="shared.sign-in">Sign In</span>
|
||||
</a>
|
||||
<p class="d-none d-sm-block">
|
||||
<p class="explore-tagline-desktop">
|
||||
<ng-container i18n="@@7deec1c1520f06170e1f8e8ddfbe4532312f638f">Explore the full Bitcoin ecosystem</ng-container>
|
||||
<ng-template [ngIf]="locale.substr(0, 2) === 'en'"> ™</ng-template>
|
||||
</p>
|
||||
|
|
|
@ -132,10 +132,36 @@ footer .row.version p a {
|
|||
footer .sponsor {
|
||||
height: 31px;
|
||||
align-items: center;
|
||||
margin-right: 5px;
|
||||
margin-left: 5px;
|
||||
max-width: 160px;
|
||||
}
|
||||
|
||||
.explore-tagline-desktop {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.explore-tagline-mobile {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media (min-width: 901px) {
|
||||
:host-context(.ltr-layout) .language-selector {
|
||||
float: right !important;
|
||||
}
|
||||
:host-context(.rtl-layout) .language-selector {
|
||||
float: left !important;
|
||||
}
|
||||
|
||||
.explore-tagline-desktop {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.explore-tagline-mobile {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
|
||||
.main-logo {
|
||||
|
@ -195,10 +221,6 @@ footer .sponsor {
|
|||
float: none;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
footer .selector:not(:last-child) {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1147px) {
|
||||
|
|
|
@ -10,8 +10,9 @@ export class RelativeUrlPipe implements PipeTransform {
|
|||
private stateService: StateService,
|
||||
) { }
|
||||
|
||||
transform(value: string): string {
|
||||
let network = this.stateService.network;
|
||||
transform(value: string, swapNetwork?: string): string {
|
||||
let network = swapNetwork || this.stateService.network;
|
||||
if (network === 'mainnet') network = '';
|
||||
if (this.stateService.env.BASE_MODULE === 'liquid' && network === 'liquidtestnet') {
|
||||
network = 'testnet';
|
||||
} else if (this.stateService.env.BASE_MODULE !== 'mempool') {
|
||||
|
|
343
frontend/src/app/shared/regex.utils.ts
Normal file
343
frontend/src/app/shared/regex.utils.ts
Normal file
|
@ -0,0 +1,343 @@
|
|||
import { Env } from '../services/state.service';
|
||||
|
||||
// all base58 characters
|
||||
const BASE58_CHARS = `[a-km-zA-HJ-NP-Z1-9]`;
|
||||
|
||||
// all bech32 characters (after the separator)
|
||||
const BECH32_CHARS_LW = `[ac-hj-np-z02-9]`;
|
||||
const BECH32_CHARS_UP = `[AC-HJ-NP-Z02-9]`;
|
||||
|
||||
// Hex characters
|
||||
const HEX_CHARS = `[a-fA-F0-9]`;
|
||||
|
||||
// A regex to say "A single 0 OR any number with no leading zeroes"
|
||||
// Capped at 9 digits so as to not be confused with lightning channel IDs (which are around 17 digits)
|
||||
// (?: // Start a non-capturing group
|
||||
// 0 // A single 0
|
||||
// | // OR
|
||||
// [1-9][0-9]{0,8} // Any succession of numbers up to 9 digits starting with 1-9
|
||||
// ) // End the non-capturing group.
|
||||
const ZERO_INDEX_NUMBER_CHARS = `(?:0|[1-9][0-9]{0,8})`;
|
||||
|
||||
// Simple digits only regex
|
||||
const NUMBER_CHARS = `[0-9]`;
|
||||
|
||||
// Formatting of the address regex is for readability,
|
||||
// We should ignore formatting it with automated formatting tools like prettier.
|
||||
//
|
||||
// prettier-ignore
|
||||
const ADDRESS_CHARS: {
|
||||
[k in Network]: {
|
||||
base58: string;
|
||||
bech32: string;
|
||||
};
|
||||
} = {
|
||||
mainnet: {
|
||||
base58: `[13]` // Starts with a single 1 or 3
|
||||
+ BASE58_CHARS
|
||||
+ `{26,33}`, // Repeat the previous char 26-33 times.
|
||||
// Version byte 0x00 (P2PKH) can be as short as 27 characters, up to 34 length
|
||||
// P2SH must be 34 length
|
||||
bech32: `(?:`
|
||||
+ `bc1` // Starts with bc1
|
||||
+ BECH32_CHARS_LW
|
||||
+ `{20,100}` // As per bech32, 6 char checksum is minimum
|
||||
+ `|`
|
||||
+ `BC1` // All upper case version
|
||||
+ BECH32_CHARS_UP
|
||||
+ `{20,100}`
|
||||
+ `)`,
|
||||
},
|
||||
testnet: {
|
||||
base58: `[mn2]` // Starts with a single m, n, or 2 (P2PKH is m or n, 2 is P2SH)
|
||||
+ BASE58_CHARS
|
||||
+ `{33,34}`, // m|n is 34 length, 2 is 35 length (We match the first letter separately)
|
||||
bech32: `(?:`
|
||||
+ `tb1` // Starts with tb1
|
||||
+ BECH32_CHARS_LW
|
||||
+ `{20,100}` // As per bech32, 6 char checksum is minimum
|
||||
+ `|`
|
||||
+ `TB1` // All upper case version
|
||||
+ BECH32_CHARS_UP
|
||||
+ `{20,100}`
|
||||
+ `)`,
|
||||
},
|
||||
signet: {
|
||||
base58: `[mn2]`
|
||||
+ BASE58_CHARS
|
||||
+ `{33,34}`,
|
||||
bech32: `(?:`
|
||||
+ `tb1` // Starts with tb1
|
||||
+ BECH32_CHARS_LW
|
||||
+ `{20,100}`
|
||||
+ `|`
|
||||
+ `TB1` // All upper case version
|
||||
+ BECH32_CHARS_UP
|
||||
+ `{20,100}`
|
||||
+ `)`,
|
||||
},
|
||||
liquid: {
|
||||
base58: `[GHPQ]` // G|H is P2PKH, P|Q is P2SH
|
||||
+ BASE58_CHARS
|
||||
+ `{33}`, // All min-max lengths are 34
|
||||
bech32: `(?:`
|
||||
+ `(?:` // bech32 liquid starts with ex1 or lq1
|
||||
+ `ex1`
|
||||
+ `|`
|
||||
+ `lq1`
|
||||
+ `)`
|
||||
+ BECH32_CHARS_LW // blech32 and bech32 are the same alphabet and protocol, different checksums.
|
||||
+ `{20,100}`
|
||||
+ `|`
|
||||
+ `(?:` // Same as above but all upper case
|
||||
+ `EX1`
|
||||
+ `|`
|
||||
+ `LQ1`
|
||||
+ `)`
|
||||
+ BECH32_CHARS_UP
|
||||
+ `{20,100}`
|
||||
+ `)`,
|
||||
},
|
||||
liquidtestnet: {
|
||||
base58: `[89]` // ???(TODO: find version) is P2PKH, 8|9 is P2SH
|
||||
+ BASE58_CHARS
|
||||
+ `{33}`, // P2PKH is ???(TODO: find size), P2SH is 34
|
||||
bech32: `(?:`
|
||||
+ `(?:` // bech32 liquid testnet starts with tex or tlq
|
||||
+ `tex1` // TODO: Why does mempool use this and not ert|el like in the elements source?
|
||||
+ `|`
|
||||
+ `tlq1` // TODO: does this exist?
|
||||
+ `)`
|
||||
+ BECH32_CHARS_LW // blech32 and bech32 are the same alphabet and protocol, different checksums.
|
||||
+ `{20,100}`
|
||||
+ `|`
|
||||
+ `(?:` // Same as above but all upper case
|
||||
+ `TEX1`
|
||||
+ `|`
|
||||
+ `TLQ1`
|
||||
+ `)`
|
||||
+ BECH32_CHARS_UP
|
||||
+ `{20,100}`
|
||||
+ `)`,
|
||||
},
|
||||
bisq: {
|
||||
base58: `(?:[bB][13]` // b or B at the start, followed by a single 1 or 3
|
||||
+ BASE58_CHARS
|
||||
+ `{26,33})`,
|
||||
bech32: `(?:`
|
||||
+ `[bB]bc1` // b or B at the start, followed by bc1
|
||||
+ BECH32_CHARS_LW
|
||||
+ `{20,100}`
|
||||
+ `|`
|
||||
+ `[bB]BC1` // b or B at the start, followed by BC1
|
||||
+ BECH32_CHARS_UP
|
||||
+ `{20,100}`
|
||||
+ `)`,
|
||||
},
|
||||
}
|
||||
type RegexTypeNoAddrNoBlockHash = | `transaction` | `blockheight` | `date` | `timestamp`;
|
||||
export type RegexType = `address` | `blockhash` | RegexTypeNoAddrNoBlockHash;
|
||||
|
||||
export const NETWORKS = [`testnet`, `signet`, `liquid`, `liquidtestnet`, `bisq`, `mainnet`] as const;
|
||||
export type Network = typeof NETWORKS[number]; // Turn const array into union type
|
||||
|
||||
export const ADDRESS_REGEXES: [RegExp, Network][] = NETWORKS
|
||||
.map(network => [getRegex('address', network), network])
|
||||
|
||||
export function findOtherNetworks(address: string, skipNetwork: Network, env: Env): { network: Network, address: string, isNetworkAvailable: boolean }[] {
|
||||
return ADDRESS_REGEXES
|
||||
.filter(([regex, network]) => network !== skipNetwork && regex.test(address))
|
||||
.map(([, network]) => ({ network, address, isNetworkAvailable: isNetworkAvailable(network, env) }));
|
||||
}
|
||||
|
||||
function isNetworkAvailable(network: Network, env: Env): boolean {
|
||||
switch (network) {
|
||||
case 'testnet':
|
||||
return env.TESTNET_ENABLED === true;
|
||||
case 'signet':
|
||||
return env.SIGNET_ENABLED === true;
|
||||
case 'liquid':
|
||||
return env.LIQUID_ENABLED === true;
|
||||
case 'liquidtestnet':
|
||||
return env.LIQUID_TESTNET_ENABLED === true;
|
||||
case 'bisq':
|
||||
return env.BISQ_ENABLED === true;
|
||||
case 'mainnet':
|
||||
return true; // There is no "MAINNET_ENABLED" flag
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function needBaseModuleChange(fromBaseModule: 'mempool' | 'liquid' | 'bisq', toNetwork: Network): boolean {
|
||||
if (!toNetwork) return false; // No target network means no change needed
|
||||
if (fromBaseModule === 'mempool') {
|
||||
return toNetwork !== 'mainnet' && toNetwork !== 'testnet' && toNetwork !== 'signet';
|
||||
}
|
||||
if (fromBaseModule === 'liquid') {
|
||||
return toNetwork !== 'liquid' && toNetwork !== 'liquidtestnet';
|
||||
}
|
||||
if (fromBaseModule === 'bisq') {
|
||||
return toNetwork !== 'bisq';
|
||||
}
|
||||
}
|
||||
|
||||
export function getTargetUrl(toNetwork: Network, address: string, env: Env): string {
|
||||
let targetUrl = '';
|
||||
if (toNetwork === 'liquid' || toNetwork === 'liquidtestnet') {
|
||||
targetUrl = env.LIQUID_WEBSITE_URL;
|
||||
targetUrl += (toNetwork === 'liquidtestnet' ? '/testnet' : '');
|
||||
targetUrl += '/address/';
|
||||
targetUrl += address;
|
||||
}
|
||||
if (toNetwork === 'bisq') {
|
||||
targetUrl = env.BISQ_WEBSITE_URL;
|
||||
targetUrl += '/address/';
|
||||
targetUrl += address;
|
||||
}
|
||||
if (toNetwork === 'mainnet' || toNetwork === 'testnet' || toNetwork === 'signet') {
|
||||
targetUrl = env.MEMPOOL_WEBSITE_URL;
|
||||
targetUrl += (toNetwork === 'mainnet' ? '' : `/${toNetwork}`);
|
||||
targetUrl += '/address/';
|
||||
targetUrl += address;
|
||||
}
|
||||
return targetUrl;
|
||||
}
|
||||
|
||||
export function getRegex(type: RegexTypeNoAddrNoBlockHash): RegExp;
|
||||
export function getRegex(type: 'address', network: Network): RegExp;
|
||||
export function getRegex(type: 'blockhash', network: Network): RegExp;
|
||||
export function getRegex(type: RegexType, network?: Network): RegExp {
|
||||
let regex = `^`; // ^ = Start of string
|
||||
switch (type) {
|
||||
// Match a block height number
|
||||
// [Testing Order]: any order is fine
|
||||
case `blockheight`:
|
||||
regex += ZERO_INDEX_NUMBER_CHARS; // block height is a 0 indexed number
|
||||
break;
|
||||
// Match a 32 byte block hash in hex.
|
||||
// [Testing Order]: Must always be tested before `transaction`
|
||||
case `blockhash`:
|
||||
if (!network) {
|
||||
throw new Error(`Must pass network when type is blockhash`);
|
||||
}
|
||||
let leadingZeroes: number;
|
||||
switch (network) {
|
||||
case `mainnet`:
|
||||
leadingZeroes = 8; // Assumes at least 32 bits of difficulty
|
||||
break;
|
||||
case `testnet`:
|
||||
leadingZeroes = 8; // Assumes at least 32 bits of difficulty
|
||||
break;
|
||||
case `signet`:
|
||||
leadingZeroes = 5;
|
||||
break;
|
||||
case `liquid`:
|
||||
leadingZeroes = 8; // We are not interested in Liquid block hashes
|
||||
break;
|
||||
case `liquidtestnet`:
|
||||
leadingZeroes = 8; // We are not interested in Liquid block hashes
|
||||
break;
|
||||
case `bisq`:
|
||||
leadingZeroes = 8; // Assumes at least 32 bits of difficulty
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Invalid Network ${network} (Unreachable error in TypeScript)`);
|
||||
}
|
||||
regex += `0{${leadingZeroes}}`;
|
||||
regex += `${HEX_CHARS}{${64 - leadingZeroes}}`; // Exactly 64 hex letters/numbers
|
||||
break;
|
||||
// Match a 32 byte tx hash in hex. Contains optional output index specifier.
|
||||
// [Testing Order]: Must always be tested after `blockhash`
|
||||
case `transaction`:
|
||||
regex += `${HEX_CHARS}{64}`; // Exactly 64 hex letters/numbers
|
||||
regex += `(?:`; // Start a non-capturing group
|
||||
regex += `:`; // 1 instances of the symbol ":"
|
||||
regex += ZERO_INDEX_NUMBER_CHARS; // A zero indexed number
|
||||
regex += `)?`; // End the non-capturing group. This group appears 0 or 1 times
|
||||
break;
|
||||
// Match any one of the many address types
|
||||
// [Testing Order]: While possible that a bech32 address happens to be 64 hex
|
||||
// characters in the future (current lengths are not 64), it is highly unlikely
|
||||
// Order therefore, does not matter.
|
||||
case `address`:
|
||||
if (!network) {
|
||||
throw new Error(`Must pass network when type is address`);
|
||||
}
|
||||
regex += `(?:`; // Start a non-capturing group (each network has multiple options)
|
||||
switch (network) {
|
||||
case `mainnet`:
|
||||
regex += ADDRESS_CHARS.mainnet.base58;
|
||||
regex += `|`; // OR
|
||||
regex += ADDRESS_CHARS.mainnet.bech32;
|
||||
regex += `|`; // OR
|
||||
regex += `04${HEX_CHARS}{128}`; // Uncompressed pubkey
|
||||
regex += `|`; // OR
|
||||
regex += `(?:02|03)${HEX_CHARS}{64}`; // Compressed pubkey
|
||||
break;
|
||||
case `testnet`:
|
||||
regex += ADDRESS_CHARS.testnet.base58;
|
||||
regex += `|`; // OR
|
||||
regex += ADDRESS_CHARS.testnet.bech32;
|
||||
regex += `|`; // OR
|
||||
regex += `04${HEX_CHARS}{128}`; // Uncompressed pubkey
|
||||
regex += `|`; // OR
|
||||
regex += `(?:02|03)${HEX_CHARS}{64}`; // Compressed pubkey
|
||||
break;
|
||||
case `signet`:
|
||||
regex += ADDRESS_CHARS.signet.base58;
|
||||
regex += `|`; // OR
|
||||
regex += ADDRESS_CHARS.signet.bech32;
|
||||
regex += `|`; // OR
|
||||
regex += `04${HEX_CHARS}{128}`; // Uncompressed pubkey
|
||||
regex += `|`; // OR
|
||||
regex += `(?:02|03)${HEX_CHARS}{64}`; // Compressed pubkey
|
||||
break;
|
||||
case `liquid`:
|
||||
regex += ADDRESS_CHARS.liquid.base58;
|
||||
regex += `|`; // OR
|
||||
regex += ADDRESS_CHARS.liquid.bech32;
|
||||
break;
|
||||
case `liquidtestnet`:
|
||||
regex += ADDRESS_CHARS.liquidtestnet.base58;
|
||||
regex += `|`; // OR
|
||||
regex += ADDRESS_CHARS.liquidtestnet.bech32;
|
||||
break;
|
||||
case `bisq`:
|
||||
regex += ADDRESS_CHARS.bisq.base58;
|
||||
regex += `|`; // OR
|
||||
regex += ADDRESS_CHARS.bisq.bech32;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Invalid Network ${network} (Unreachable error in TypeScript)`);
|
||||
}
|
||||
regex += `)`; // End the non-capturing group
|
||||
break;
|
||||
// Match a date in the format YYYY-MM-DD (optional: HH:MM)
|
||||
// [Testing Order]: any order is fine
|
||||
case `date`:
|
||||
regex += `(?:`; // Start a non-capturing group
|
||||
regex += `${NUMBER_CHARS}{4}`; // Exactly 4 digits
|
||||
regex += `[-/]`; // 1 instance of the symbol "-" or "/"
|
||||
regex += `${NUMBER_CHARS}{1,2}`; // Exactly 4 digits
|
||||
regex += `[-/]`; // 1 instance of the symbol "-" or "/"
|
||||
regex += `${NUMBER_CHARS}{1,2}`; // Exactly 4 digits
|
||||
regex += `(?:`; // Start a non-capturing group
|
||||
regex += ` `; // 1 instance of the symbol " "
|
||||
regex += `${NUMBER_CHARS}{1,2}`; // Exactly 4 digits
|
||||
regex += `:`; // 1 instance of the symbol ":"
|
||||
regex += `${NUMBER_CHARS}{1,2}`; // Exactly 4 digits
|
||||
regex += `)?`; // End the non-capturing group. This group appears 0 or 1 times
|
||||
regex += `)`; // End the non-capturing group
|
||||
break;
|
||||
// Match a unix timestamp
|
||||
// [Testing Order]: any order is fine
|
||||
case `timestamp`:
|
||||
regex += `${NUMBER_CHARS}{10}`; // Exactly 10 digits
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Invalid RegexType ${type} (Unreachable error in TypeScript)`);
|
||||
}
|
||||
regex += `$`; // $ = End of string
|
||||
return new RegExp(regex);
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -3,6 +3,7 @@
|
|||
"NETWORK": "mainnet",
|
||||
"BACKEND": "esplora",
|
||||
"HTTP_PORT": 8999,
|
||||
"CACHE_ENABLED": false,
|
||||
"MINED_BLOCKS_CACHE": 144,
|
||||
"SPAWN_CLUSTER_PROCS": 0,
|
||||
"API_URL_PREFIX": "/api/v1/",
|
||||
|
|
|
@ -19,6 +19,8 @@ client_header_timeout 10s;
|
|||
keepalive_timeout 69s;
|
||||
# maximum time between packets nginx is allowed to pause when sending the client data
|
||||
send_timeout 69s;
|
||||
# maximum time to wait for response from upstream backends
|
||||
proxy_read_timeout 120s;
|
||||
|
||||
# number of requests per connection, does not affect SPDY
|
||||
keepalive_requests 1337;
|
||||
|
|
Loading…
Add table
Reference in a new issue