Merge branch 'master' into lightning-dashboard-ownership
This commit is contained in:
		
						commit
						8cb49ba8e3
					
				| @ -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-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-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,7 +68,13 @@ 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) { | ||||
| @ -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') { | ||||
|           // 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,8 +294,12 @@ export class SearchFormComponent implements OnInit { | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   navigate(url: string, searchText: string, extras?: any): void { | ||||
|     this.router.navigate([this.relativeUrlPipe.transform(url), searchText], extras); | ||||
| 
 | ||||
|   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: '', | ||||
| @ -278,3 +307,4 @@ export class SearchFormComponent implements OnInit { | ||||
|       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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user