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 'v0_p2wsh': flags |= TransactionFlags.p2wsh; break; | ||||||
|         case 'v1_p2tr': { |         case 'v1_p2tr': { | ||||||
|           flags |= TransactionFlags.p2tr; |           flags |= TransactionFlags.p2tr; | ||||||
|           if (vin.witness.length > 2) { |           // in taproot, if the last witness item begins with 0x50, it's an annex
 | ||||||
|             const asm = vin.inner_witnessscript_asm || transactionUtils.convertScriptSigAsm(vin.witness[vin.witness.length - 2]); |           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')) { |             if (asm?.includes('OP_0 OP_IF')) { | ||||||
|               flags |= TransactionFlags.inscription; |               flags |= TransactionFlags.inscription; | ||||||
|             } |             } | ||||||
|  | |||||||
							
								
								
									
										14
									
								
								frontend/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										14
									
								
								frontend/package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -58,7 +58,7 @@ | |||||||
|       "optionalDependencies": { |       "optionalDependencies": { | ||||||
|         "@cypress/schematic": "^2.5.0", |         "@cypress/schematic": "^2.5.0", | ||||||
|         "@types/cypress": "^1.1.3", |         "@types/cypress": "^1.1.3", | ||||||
|         "cypress": "^13.6.0", |         "cypress": "^13.6.2", | ||||||
|         "cypress-fail-on-console-error": "~5.1.0", |         "cypress-fail-on-console-error": "~5.1.0", | ||||||
|         "cypress-wait-until": "^2.0.1", |         "cypress-wait-until": "^2.0.1", | ||||||
|         "mock-socket": "~9.3.1", |         "mock-socket": "~9.3.1", | ||||||
| @ -7083,9 +7083,9 @@ | |||||||
|       "peer": true |       "peer": true | ||||||
|     }, |     }, | ||||||
|     "node_modules/cypress": { |     "node_modules/cypress": { | ||||||
|       "version": "13.6.0", |       "version": "13.6.2", | ||||||
|       "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.6.0.tgz", |       "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.6.2.tgz", | ||||||
|       "integrity": "sha512-quIsnFmtj4dBUEJYU4OH0H12bABJpSujvWexC24Ju1gTlKMJbeT6tTO0vh7WNfiBPPjoIXLN+OUqVtiKFs6SGw==", |       "integrity": "sha512-TW3bGdPU4BrfvMQYv1z3oMqj71YI4AlgJgnrycicmPZAXtvywVFZW9DAToshO65D97rCWfG/kqMFsYB6Kp91gQ==", | ||||||
|       "hasInstallScript": true, |       "hasInstallScript": true, | ||||||
|       "optional": true, |       "optional": true, | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
| @ -22272,9 +22272,9 @@ | |||||||
|       "peer": true |       "peer": true | ||||||
|     }, |     }, | ||||||
|     "cypress": { |     "cypress": { | ||||||
|       "version": "13.6.0", |       "version": "13.6.2", | ||||||
|       "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.6.0.tgz", |       "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.6.2.tgz", | ||||||
|       "integrity": "sha512-quIsnFmtj4dBUEJYU4OH0H12bABJpSujvWexC24Ju1gTlKMJbeT6tTO0vh7WNfiBPPjoIXLN+OUqVtiKFs6SGw==", |       "integrity": "sha512-TW3bGdPU4BrfvMQYv1z3oMqj71YI4AlgJgnrycicmPZAXtvywVFZW9DAToshO65D97rCWfG/kqMFsYB6Kp91gQ==", | ||||||
|       "optional": true, |       "optional": true, | ||||||
|       "requires": { |       "requires": { | ||||||
|         "@cypress/request": "^3.0.0", |         "@cypress/request": "^3.0.0", | ||||||
|  | |||||||
| @ -110,7 +110,7 @@ | |||||||
|   "optionalDependencies": { |   "optionalDependencies": { | ||||||
|     "@cypress/schematic": "^2.5.0", |     "@cypress/schematic": "^2.5.0", | ||||||
|     "@types/cypress": "^1.1.3", |     "@types/cypress": "^1.1.3", | ||||||
|     "cypress": "^13.6.0", |     "cypress": "^13.6.2", | ||||||
|     "cypress-fail-on-console-error": "~5.1.0", |     "cypress-fail-on-console-error": "~5.1.0", | ||||||
|     "cypress-wait-until": "^2.0.1", |     "cypress-wait-until": "^2.0.1", | ||||||
|     "mock-socket": "~9.3.1", |     "mock-socket": "~9.3.1", | ||||||
|  | |||||||
| @ -47,7 +47,7 @@ | |||||||
|               <tr> |               <tr> | ||||||
|                 <td i18n="block.timestamp">Timestamp</td> |                 <td i18n="block.timestamp">Timestamp</td> | ||||||
|                 <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> |                 </td> | ||||||
|               </tr> |               </tr> | ||||||
|               <tr> |               <tr> | ||||||
| @ -233,7 +233,9 @@ | |||||||
|           <ng-container *ngIf="!isMobile || mode !== 'actual'; else emptyBlockInfo"></ng-container> |           <ng-container *ngIf="!isMobile || mode !== 'actual'; else emptyBlockInfo"></ng-container> | ||||||
|         </div> |         </div> | ||||||
|         <ng-container *ngIf="network !== 'liquid'"> |         <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> |         </ng-container> | ||||||
|       </div> |       </div> | ||||||
|       <div class="col-sm" *ngIf="!isMobile"> |       <div class="col-sm" *ngIf="!isMobile"> | ||||||
| @ -245,7 +247,9 @@ | |||||||
|           <ng-container *ngTemplateOutlet="emptyBlockInfo"></ng-container> |           <ng-container *ngTemplateOutlet="emptyBlockInfo"></ng-container> | ||||||
|         </div> |         </div> | ||||||
|         <ng-container *ngIf="network !== 'liquid'"> |         <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> |         </ng-container> | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
| @ -452,5 +456,24 @@ | |||||||
|   </table> |   </table> | ||||||
| </ng-template> | </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> | ||||||
| <br> | <br> | ||||||
|  | |||||||
| @ -46,7 +46,7 @@ | |||||||
|             </div> |             </div> | ||||||
|           </td> |           </td> | ||||||
|           <td class="timestamp" *ngIf="!widget" [ngClass]="{'widget': widget, 'legacy': !isMempoolModule}"> |           <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> | ||||||
|           <td *ngIf="auditAvailable" class="health text-right" [ngClass]="{'widget': widget, 'legacy': !isMempoolModule}"> |           <td *ngIf="auditAvailable" class="health text-right" [ngClass]="{'widget': widget, 'legacy': !isMempoolModule}"> | ||||||
|             <a |             <a | ||||||
|  | |||||||
| @ -224,7 +224,7 @@ | |||||||
|             <a [routerLink]="['/block' | relativeUrl, block.id]">{{ block.height }}</a> |             <a [routerLink]="['/block' | relativeUrl, block.id]">{{ block.height }}</a> | ||||||
|           </td> |           </td> | ||||||
|           <td class="timestamp"> |           <td class="timestamp"> | ||||||
|             ‎{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }} |             ‎{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm:ss' }} | ||||||
|           </td> |           </td> | ||||||
|           <td class="mined"> |           <td class="mined"> | ||||||
|             <app-time kind="since" [time]="block.timestamp" [fastRender]="true"></app-time> |             <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 { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; | ||||||
| import { EventType, NavigationStart, Router } from '@angular/router'; | import { EventType, NavigationStart, Router } from '@angular/router'; | ||||||
| import { AssetsService } from '../../services/assets.service'; | 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 { Observable, of, Subject, zip, BehaviorSubject, combineLatest } from 'rxjs'; | ||||||
| import { debounceTime, distinctUntilChanged, switchMap, catchError, map, startWith,  tap } from 'rxjs/operators'; | import { debounceTime, distinctUntilChanged, switchMap, catchError, map, startWith,  tap } from 'rxjs/operators'; | ||||||
| import { ElectrsApiService } from '../../services/electrs-api.service'; | import { ElectrsApiService } from '../../services/electrs-api.service'; | ||||||
| import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; | import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; | ||||||
| import { ApiService } from '../../services/api.service'; | import { ApiService } from '../../services/api.service'; | ||||||
| import { SearchResultsComponent } from './search-results/search-results.component'; | import { SearchResultsComponent } from './search-results/search-results.component'; | ||||||
|  | import { Network, findOtherNetworks, getRegex, getTargetUrl, needBaseModuleChange } from '../../shared/regex.utils'; | ||||||
| 
 | 
 | ||||||
| @Component({ | @Component({ | ||||||
|   selector: 'app-search-form', |   selector: 'app-search-form', | ||||||
| @ -18,7 +19,7 @@ import { SearchResultsComponent } from './search-results/search-results.componen | |||||||
| }) | }) | ||||||
| export class SearchFormComponent implements OnInit { | export class SearchFormComponent implements OnInit { | ||||||
|   @Input() hamburgerOpen = false; |   @Input() hamburgerOpen = false; | ||||||
|    |   env: Env; | ||||||
|   network = ''; |   network = ''; | ||||||
|   assets: object = {}; |   assets: object = {}; | ||||||
|   isSearching = false; |   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})$/; |   regexAddress = getRegex('address', 'mainnet'); // Default to mainnet
 | ||||||
|   regexBlockhash = /^[0]{8}[a-fA-F0-9]{56}$/; |   regexBlockhash = getRegex('blockhash', 'mainnet'); | ||||||
|   regexTransaction = /^([a-fA-F0-9]{64})(:\d+)?$/; |   regexTransaction = getRegex('transaction'); | ||||||
|   regexBlockheight = /^[0-9]{1,9}$/; |   regexBlockheight = getRegex('blockheight'); | ||||||
|   regexDate = /^(?:\d{4}[-/]\d{1,2}[-/]\d{1,2}(?: \d{1,2}:\d{2})?)$/; |   regexDate = getRegex('date'); | ||||||
|   regexUnixTimestamp = /^\d{10}$/; |   regexUnixTimestamp = getRegex('timestamp'); | ||||||
|  | 
 | ||||||
|   focus$ = new Subject<string>(); |   focus$ = new Subject<string>(); | ||||||
|   click$ = new Subject<string>(); |   click$ = new Subject<string>(); | ||||||
| 
 | 
 | ||||||
| @ -66,8 +68,14 @@ export class SearchFormComponent implements OnInit { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   ngOnInit(): void { |   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
 |     this.router.events.subscribe((e: NavigationStart) => { // Reset search focus when changing page
 | ||||||
|       if (this.searchInput && e.type === EventType.NavigationStart) { |       if (this.searchInput && e.type === EventType.NavigationStart) { | ||||||
|         this.searchInput.nativeElement.blur(); |         this.searchInput.nativeElement.blur(); | ||||||
| @ -96,9 +104,6 @@ export class SearchFormComponent implements OnInit { | |||||||
|     const searchText$ = this.searchForm.get('searchText').valueChanges |     const searchText$ = this.searchForm.get('searchText').valueChanges | ||||||
|     .pipe( |     .pipe( | ||||||
|       map((text) => { |       map((text) => { | ||||||
|         if (this.network === 'bisq' && text.match(/^(b)[^c]/i)) { |  | ||||||
|           return text.substr(1); |  | ||||||
|         } |  | ||||||
|         return text.trim(); |         return text.trim(); | ||||||
|       }), |       }), | ||||||
|       tap((text) => { |       tap((text) => { | ||||||
| @ -132,9 +137,6 @@ export class SearchFormComponent implements OnInit { | |||||||
|         ); |         ); | ||||||
|       }), |       }), | ||||||
|       map((result: any[]) => { |       map((result: any[]) => { | ||||||
|         if (this.network === 'bisq') { |  | ||||||
|           result[0] = result[0].map((address: string) => 'B' + address); |  | ||||||
|         } |  | ||||||
|         return result; |         return result; | ||||||
|       }), |       }), | ||||||
|       tap(() => { |       tap(() => { | ||||||
| @ -164,6 +166,7 @@ export class SearchFormComponent implements OnInit { | |||||||
|               blockHeight: false, |               blockHeight: false, | ||||||
|               txId: false, |               txId: false, | ||||||
|               address: false, |               address: false, | ||||||
|  |               otherNetworks: [], | ||||||
|               addresses: [], |               addresses: [], | ||||||
|               nodes: [], |               nodes: [], | ||||||
|               channels: [], |               channels: [], | ||||||
| @ -174,15 +177,21 @@ export class SearchFormComponent implements OnInit { | |||||||
|           const addressPrefixSearchResults = result[0]; |           const addressPrefixSearchResults = result[0]; | ||||||
|           const lightningResults = result[1]; |           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 matchesBlockHeight = this.regexBlockheight.test(searchText) && parseInt(searchText) <= this.stateService.latestBlockHeight; | ||||||
|           const matchesDateTime = this.regexDate.test(searchText) && new Date(searchText).toString() !== 'Invalid Date'; |           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); |           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 matchesTxId = this.regexTransaction.test(searchText) && !this.regexBlockhash.test(searchText); | ||||||
|           const matchesBlockHash = 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
 | ||||||
|             searchText = 'B' + searchText; |           if (!matchesAddress && this.network === 'bisq' && getRegex('address', 'mainnet').test(searchText)) { | ||||||
|  |               searchText = 'B' + searchText; | ||||||
|  |               matchesAddress = !matchesTxId && this.regexAddress.test(searchText); | ||||||
|           } |           } | ||||||
| 
 | 
 | ||||||
|           if (matchesDateTime && searchText.indexOf('/') !== -1) { |           if (matchesDateTime && searchText.indexOf('/') !== -1) { | ||||||
| @ -198,7 +207,8 @@ export class SearchFormComponent implements OnInit { | |||||||
|             txId: matchesTxId, |             txId: matchesTxId, | ||||||
|             blockHash: matchesBlockHash, |             blockHash: matchesBlockHash, | ||||||
|             address: matchesAddress, |             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, |             nodes: lightningResults.nodes, | ||||||
|             channels: lightningResults.channels, |             channels: lightningResults.channels, | ||||||
|           }; |           }; | ||||||
| @ -223,6 +233,15 @@ export class SearchFormComponent implements OnInit { | |||||||
|       this.navigate('/lightning/node/', result.public_key); |       this.navigate('/lightning/node/', result.public_key); | ||||||
|     } else if (result.short_id) { |     } else if (result.short_id) { | ||||||
|       this.navigate('/lightning/channel/', result.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(); |     const searchText = result || this.searchForm.value.searchText.trim(); | ||||||
|     if (searchText) { |     if (searchText) { | ||||||
|       this.isSearching = true; |       this.isSearching = true; | ||||||
|  | 
 | ||||||
|       if (!this.regexTransaction.test(searchText) && this.regexAddress.test(searchText)) { |       if (!this.regexTransaction.test(searchText) && this.regexAddress.test(searchText)) { | ||||||
|         this.navigate('/address/', searchText); |         this.navigate('/address/', searchText); | ||||||
|       } else if (this.regexBlockhash.test(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)) { |       } else if (this.regexDate.test(searchText) || this.regexUnixTimestamp.test(searchText)) { | ||||||
|         let timestamp: number; |         let timestamp: number; | ||||||
|         this.regexDate.test(searchText) ? timestamp = Math.floor(new Date(searchText).getTime() / 1000) : timestamp = searchText; |         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( |         this.apiService.getBlockDataFromTimestamp$(timestamp).subscribe( | ||||||
|           (data) => { this.navigate('/block/', data.hash); }, |           (data) => { this.navigate('/block/', data.hash); }, | ||||||
|           (error) => { console.log(error); this.isSearching = false; } |           (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); |   navigate(url: string, searchText: string, extras?: any, swapNetwork?: string) { | ||||||
|     this.searchTriggered.emit(); |     if (needBaseModuleChange(this.env.BASE_MODULE as 'liquid' | 'bisq' | 'mempool', swapNetwork as Network)) { | ||||||
|     this.searchForm.setValue({ |       window.location.href = getTargetUrl(swapNetwork as Network, searchText, this.env); | ||||||
|       searchText: '', |     } else { | ||||||
|     }); |       this.router.navigate([this.relativeUrlPipe.transform(url, swapNetwork), searchText], extras); | ||||||
|     this.isSearching = false; |       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"> |   <ng-template [ngIf]="results.blockHeight"> | ||||||
|     <div class="card-title" i18n="search.bitcoin-block-height">Bitcoin Block Height</div> |     <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"> |     <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> |       <ng-container *ngTemplateOutlet="goTo; context: { $implicit: results.searchText | shortenString : 13 }"></ng-container> | ||||||
|     </button> |     </button> | ||||||
|   </ng-template> |   </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> |     <div class="card-title" i18n="search.bitcoin-addresses">Bitcoin Addresses</div> | ||||||
|     <ng-template ngFor [ngForOf]="results.addresses" let-address let-i="index"> |     <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> |         <ngb-highlight [result]="address | shortenString : isMobile ? 25 : 36" [term]="results.searchText"></ngb-highlight> | ||||||
|       </button> |       </button> | ||||||
|     </ng-template> |     </ng-template> | ||||||
| @ -46,7 +54,7 @@ | |||||||
|   <ng-template [ngIf]="results.nodes.length"> |   <ng-template [ngIf]="results.nodes.length"> | ||||||
|     <div class="card-title" i18n="search.lightning-nodes">Lightning Nodes</div> |     <div class="card-title" i18n="search.lightning-nodes">Lightning Nodes</div> | ||||||
|     <ng-template ngFor [ngForOf]="results.nodes" let-node let-i="index"> |     <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> |         <ngb-highlight [result]="node.alias" [term]="results.searchText"></ngb-highlight>  <span class="symbol">{{ node.public_key | shortenString : 10 }}</span> | ||||||
|       </button> |       </button> | ||||||
|     </ng-template> |     </ng-template> | ||||||
| @ -54,7 +62,7 @@ | |||||||
|   <ng-template [ngIf]="results.channels.length"> |   <ng-template [ngIf]="results.channels.length"> | ||||||
|     <div class="card-title" i18n="search.lightning-channels">Lightning Channels</div> |     <div class="card-title" i18n="search.lightning-channels">Lightning Channels</div> | ||||||
|     <ng-template ngFor [ngForOf]="results.channels" let-channel let-i="index"> |     <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> |         <ngb-highlight [result]="channel.short_id" [term]="results.searchText"></ngb-highlight>  <span class="symbol">{{ channel.id }}</span> | ||||||
|       </button> |       </button> | ||||||
|     </ng-template> |     </ng-template> | ||||||
|  | |||||||
| @ -7,6 +7,10 @@ | |||||||
|   margin-left: 10px; |   margin-left: 10px; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .danger { | ||||||
|  |   color: #dc3545; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .dropdown-menu { | .dropdown-menu { | ||||||
|   position: absolute; |   position: absolute; | ||||||
|   top: 42px; |   top: 42px; | ||||||
|  | |||||||
| @ -22,7 +22,7 @@ export class SearchResultsComponent implements OnChanges { | |||||||
|   ngOnChanges() { |   ngOnChanges() { | ||||||
|     this.activeIdx = 0; |     this.activeIdx = 0; | ||||||
|     if (this.results) { |     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; |         break; | ||||||
|       case 'Enter': |       case 'Enter': | ||||||
|         event.preventDefault(); |         event.preventDefault(); | ||||||
|  |         if (this.resultsFlattened[this.activeIdx]?.isNetworkAvailable === false) { | ||||||
|  |           return; | ||||||
|  |         } | ||||||
|         if (this.resultsFlattened[this.activeIdx]) { |         if (this.resultsFlattened[this.activeIdx]) { | ||||||
|           this.selectedResult.emit(this.resultsFlattened[this.activeIdx]); |           this.selectedResult.emit(this.resultsFlattened[this.activeIdx]); | ||||||
|         } else { |         } else { | ||||||
|  | |||||||
| @ -87,8 +87,8 @@ | |||||||
|               <th class="table-cell-new-fee" i18n="dashboard.new-transaction-fee">New fee</th> |               <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> |               <th class="table-cell-badges" i18n="transaction.status|Transaction Status">Status</th> | ||||||
|             </thead> |             </thead> | ||||||
|             <tbody> |             <tbody *ngIf="replacements$ | async as replacements; else replacementsSkeleton"> | ||||||
|               <tr *ngFor="let replacement of replacements$ | async;"> |               <tr *ngFor="let replacement of replacements"> | ||||||
|                 <td class="table-cell-txid"> |                 <td class="table-cell-txid"> | ||||||
|                   <a [routerLink]="['/tx' | relativeUrl, replacement.txid]"> |                   <a [routerLink]="['/tx' | relativeUrl, replacement.txid]"> | ||||||
|                     <app-truncate [text]="replacement.txid" [lastChars]="5"></app-truncate> |                     <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-fiat" *ngIf="(network$ | async) === ''">{{ currency }}</th> | ||||||
|               <th class="table-cell-fees" i18n="transaction.fee|Transaction fee">Fee</th> |               <th class="table-cell-fees" i18n="transaction.fee|Transaction fee">Fee</th> | ||||||
|             </thead> |             </thead> | ||||||
|             <tbody> |             <tbody *ngIf="transactions$ | async as transactions else recentTransactionsSkeleton"> | ||||||
|               <tr *ngFor="let transaction of transactions$ | async; let i = index;"> |               <tr *ngFor="let transaction of transactions; let i = index;"> | ||||||
|                 <td class="table-cell-txid"> |                 <td class="table-cell-txid"> | ||||||
|                   <a [routerLink]="['/tx' | relativeUrl, transaction.txid]"> |                   <a [routerLink]="['/tx' | relativeUrl, transaction.txid]"> | ||||||
|                     <app-truncate [text]="transaction.txid" [lastChars]="5"></app-truncate> |                     <app-truncate [text]="transaction.txid" [lastChars]="5"></app-truncate> | ||||||
| @ -199,6 +199,28 @@ | |||||||
|   </table> |   </table> | ||||||
| </ng-template> | </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> | <ng-template #loadingTransactions> | ||||||
|   <div class="skeleton-loader skeleton-loader-transactions"></div> |   <div class="skeleton-loader skeleton-loader-transactions"></div> | ||||||
| </ng-template> | </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="officialMempoolSpace" viewBox="0 0 500 126"></app-svg-images> | ||||||
|           <app-svg-images *ngIf="!officialMempoolSpace" name="mempoolSpace" viewBox="0 0 500 126"></app-svg-images> |           <app-svg-images *ngIf="!officialMempoolSpace" name="mempoolSpace" viewBox="0 0 500 126"></app-svg-images> | ||||||
|         </div> |         </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-container i18n="@@7deec1c1520f06170e1f8e8ddfbe4532312f638f">Explore the full Bitcoin ecosystem</ng-container> | ||||||
|           <ng-template [ngIf]="locale.substr(0, 2) === 'en'"> ™</ng-template> |           <ng-template [ngIf]="locale.substr(0, 2) === 'en'"> ™</ng-template> | ||||||
|         </p> |         </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"> |           <div class="selector"> | ||||||
|             <app-language-selector></app-language-selector> |             <app-language-selector></app-language-selector> | ||||||
|           </div> |           </div> | ||||||
| @ -26,11 +26,11 @@ | |||||||
|             <span *ngIf="!loggedIn" i18n="shared.sign-in">Sign In</span> |             <span *ngIf="!loggedIn" i18n="shared.sign-in">Sign In</span> | ||||||
|           </a> |           </a> | ||||||
|         </div> |         </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.my-account">My Account</span> | ||||||
|           <span *ngIf="!loggedIn" i18n="shared.sign-in">Sign In</span> |           <span *ngIf="!loggedIn" i18n="shared.sign-in">Sign In</span> | ||||||
|         </a> |         </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-container i18n="@@7deec1c1520f06170e1f8e8ddfbe4532312f638f">Explore the full Bitcoin ecosystem</ng-container> | ||||||
|           <ng-template [ngIf]="locale.substr(0, 2) === 'en'"> ™</ng-template> |           <ng-template [ngIf]="locale.substr(0, 2) === 'en'"> ™</ng-template> | ||||||
|         </p> |         </p> | ||||||
|  | |||||||
| @ -132,10 +132,36 @@ footer .row.version p a { | |||||||
| footer .sponsor { | footer .sponsor { | ||||||
|   height: 31px; |   height: 31px; | ||||||
|   align-items: center; |   align-items: center; | ||||||
|  |   margin-right: 5px; | ||||||
|   margin-left: 5px; |   margin-left: 5px; | ||||||
|   max-width: 160px; |   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) { | @media (max-width: 1200px) { | ||||||
| 
 | 
 | ||||||
|   .main-logo { |   .main-logo { | ||||||
| @ -195,10 +221,6 @@ footer .sponsor { | |||||||
|     float: none; |     float: none; | ||||||
|     margin-top: 15px; |     margin-top: 15px; | ||||||
|   } |   } | ||||||
| 
 |  | ||||||
|   footer .selector:not(:last-child) { |  | ||||||
|     margin-right: 10px; |  | ||||||
|   } |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @media (max-width: 1147px) { | @media (max-width: 1147px) { | ||||||
|  | |||||||
| @ -10,8 +10,9 @@ export class RelativeUrlPipe implements PipeTransform { | |||||||
|     private stateService: StateService, |     private stateService: StateService, | ||||||
|   ) { } |   ) { } | ||||||
| 
 | 
 | ||||||
|   transform(value: string): string { |   transform(value: string, swapNetwork?: string): string { | ||||||
|     let network = this.stateService.network; |     let network = swapNetwork || this.stateService.network; | ||||||
|  |     if (network === 'mainnet') network = ''; | ||||||
|     if (this.stateService.env.BASE_MODULE === 'liquid' && network === 'liquidtestnet') { |     if (this.stateService.env.BASE_MODULE === 'liquid' && network === 'liquidtestnet') { | ||||||
|       network = 'testnet'; |       network = 'testnet'; | ||||||
|     } else if (this.stateService.env.BASE_MODULE !== 'mempool') { |     } 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", |     "NETWORK": "mainnet", | ||||||
|     "BACKEND": "esplora", |     "BACKEND": "esplora", | ||||||
|     "HTTP_PORT": 8999, |     "HTTP_PORT": 8999, | ||||||
|  |     "CACHE_ENABLED": false, | ||||||
|     "MINED_BLOCKS_CACHE": 144, |     "MINED_BLOCKS_CACHE": 144, | ||||||
|     "SPAWN_CLUSTER_PROCS": 0, |     "SPAWN_CLUSTER_PROCS": 0, | ||||||
|     "API_URL_PREFIX": "/api/v1/", |     "API_URL_PREFIX": "/api/v1/", | ||||||
|  | |||||||
| @ -19,6 +19,8 @@ client_header_timeout 10s; | |||||||
| keepalive_timeout 69s; | keepalive_timeout 69s; | ||||||
| # maximum time between packets nginx is allowed to pause when sending the client data | # maximum time between packets nginx is allowed to pause when sending the client data | ||||||
| send_timeout 69s; | 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 | # number of requests per connection, does not affect SPDY | ||||||
| keepalive_requests 1337; | keepalive_requests 1337; | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user