Merge pull request #5156 from mempool/simon/default-frontend-network-setting
Root frontend network setting
This commit is contained in:
		
						commit
						8d66374374
					
				| @ -16,6 +16,7 @@ fi | ||||
| 
 | ||||
| # Runtime overrides - read env vars defined in docker compose | ||||
| 
 | ||||
| __MAINNET_ENABLED__=${MAINNET_ENABLED:=true} | ||||
| __TESTNET_ENABLED__=${TESTNET_ENABLED:=false} | ||||
| __SIGNET_ENABLED__=${SIGNET_ENABLED:=false} | ||||
| __LIQUID_ENABLED__=${LIQUID_ENABLED:=false} | ||||
| @ -28,6 +29,7 @@ __NGINX_PORT__=${NGINX_PORT:=8999} | ||||
| __BLOCK_WEIGHT_UNITS__=${BLOCK_WEIGHT_UNITS:=4000000} | ||||
| __MEMPOOL_BLOCKS_AMOUNT__=${MEMPOOL_BLOCKS_AMOUNT:=8} | ||||
| __BASE_MODULE__=${BASE_MODULE:=mempool} | ||||
| __ROOT_NETWORK__=${ROOT_NETWORK:=} | ||||
| __MEMPOOL_WEBSITE_URL__=${MEMPOOL_WEBSITE_URL:=https://mempool.space} | ||||
| __LIQUID_WEBSITE_URL__=${LIQUID_WEBSITE_URL:=https://liquid.network} | ||||
| __MINING_DASHBOARD__=${MINING_DASHBOARD:=true} | ||||
| @ -42,6 +44,7 @@ __HISTORICAL_PRICE__=${HISTORICAL_PRICE:=true} | ||||
| __ADDITIONAL_CURRENCIES__=${ADDITIONAL_CURRENCIES:=false} | ||||
| 
 | ||||
| # Export as environment variables to be used by envsubst | ||||
| export __MAINNET_ENABLED__ | ||||
| export __TESTNET_ENABLED__ | ||||
| export __SIGNET_ENABLED__ | ||||
| export __LIQUID_ENABLED__ | ||||
| @ -54,6 +57,7 @@ export __NGINX_PORT__ | ||||
| export __BLOCK_WEIGHT_UNITS__ | ||||
| export __MEMPOOL_BLOCKS_AMOUNT__ | ||||
| export __BASE_MODULE__ | ||||
| export __ROOT_NETWORK__ | ||||
| export __MEMPOOL_WEBSITE_URL__ | ||||
| export __LIQUID_WEBSITE_URL__ | ||||
| export __MINING_DASHBOARD__ | ||||
|  | ||||
| @ -4,6 +4,7 @@ | ||||
|   "SIGNET_ENABLED": false, | ||||
|   "LIQUID_ENABLED": false, | ||||
|   "LIQUID_TESTNET_ENABLED": false, | ||||
|   "MAINNET_ENABLED": true, | ||||
|   "ITEMS_PER_PAGE": 10, | ||||
|   "KEEP_BLOCKS_AMOUNT": 8, | ||||
|   "NGINX_PROTOCOL": "http", | ||||
| @ -12,6 +13,7 @@ | ||||
|   "BLOCK_WEIGHT_UNITS": 4000000, | ||||
|   "MEMPOOL_BLOCKS_AMOUNT": 8, | ||||
|   "BASE_MODULE": "mempool", | ||||
|   "ROOT_NETWORK": "", | ||||
|   "MEMPOOL_WEBSITE_URL": "https://mempool.space", | ||||
|   "LIQUID_WEBSITE_URL": "https://liquid.network", | ||||
|   "MINING_DASHBOARD": true, | ||||
|  | ||||
| @ -62,12 +62,12 @@ | ||||
|     } | ||||
|   </a> | ||||
| 
 | ||||
|   <div (window:resize)="onResize()" ngbDropdown class="dropdown-container" *ngIf="env.TESTNET_ENABLED || env.TESTNET4_ENABLED || env.SIGNET_ENABLED || env.LIQUID_ENABLED || env.LIQUID_TESTNET_ENABLED"> | ||||
|   <div (window:resize)="onResize()" ngbDropdown class="dropdown-container" *ngIf="isDropdownVisible"> | ||||
|     <button ngbDropdownToggle type="button" class="btn btn-secondary dropdown-toggle-split d-flex justify-content-center align-items-center" aria-haspopup="true"> | ||||
|       <app-svg-images class="d-flex justify-content-center align-items-center current-network-svg" [name]="network.val === '' ? 'bitcoin' : network.val" width="20" height="20" viewBox="0 0 65 65"></app-svg-images> | ||||
|     </button> | ||||
|     <div ngbDropdownMenu [ngClass]="{'dropdown-menu-right' : isMobile}"> | ||||
|       <a ngbDropdownItem class="mainnet" [routerLink]="networkPaths['mainnet'] || '/'"><app-svg-images name="bitcoin" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Mainnet</a> | ||||
|       <a ngbDropdownItem *ngIf="env.MAINNET_ENABLED" class="mainnet" [routerLink]="networkPaths['mainnet'] || '/'"><app-svg-images name="bitcoin" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Mainnet</a> | ||||
|       <a ngbDropdownItem *ngIf="env.SIGNET_ENABLED" class="signet" [class.active]="network.val === 'signet'" [routerLink]="networkPaths['signet'] || '/signet'"><app-svg-images name="signet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Signet</a> | ||||
|       <a ngbDropdownItem *ngIf="env.TESTNET_ENABLED" class="testnet" [class.active]="network.val === 'testnet'" [routerLink]="networkPaths['testnet'] || '/testnet'"><app-svg-images name="testnet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Testnet3</a> | ||||
|       <a ngbDropdownItem *ngIf="env.TESTNET4_ENABLED" class="testnet4" [class.active]="network.val === 'testnet4'" [routerLink]="networkPaths['testnet4'] || '/testnet4'"><app-svg-images name="testnet4" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Testnet4 <span class="badge badge-pill badge-warning beta-network" i18n="beta">beta</span></a> | ||||
|  | ||||
| @ -31,6 +31,7 @@ export class MasterPageComponent implements OnInit, OnDestroy { | ||||
|   user: any = undefined; | ||||
|   servicesEnabled = false; | ||||
|   menuOpen = false; | ||||
|   isDropdownVisible: boolean; | ||||
|    | ||||
|   enterpriseInfo: any; | ||||
|   enterpriseInfo$: Subscription; | ||||
| @ -74,19 +75,27 @@ export class MasterPageComponent implements OnInit, OnDestroy { | ||||
| 
 | ||||
|     const isServicesPage = this.router.url.includes('/services/'); | ||||
|     this.menuOpen = isServicesPage && !this.isSmallScreen(); | ||||
|     this.setDropdownVisibility(); | ||||
|   } | ||||
| 
 | ||||
|   ngOnDestroy() { | ||||
|     if (this.enterpriseInfo$) { | ||||
|       this.enterpriseInfo$.unsubscribe(); | ||||
|     } | ||||
|   setDropdownVisibility(): void { | ||||
|     const networks = [ | ||||
|       this.env.TESTNET_ENABLED, | ||||
|       this.env.TESTNET4_ENABLED, | ||||
|       this.env.SIGNET_ENABLED, | ||||
|       this.env.LIQUID_ENABLED, | ||||
|       this.env.LIQUID_TESTNET_ENABLED, | ||||
|       this.env.MAINNET_ENABLED, | ||||
|     ]; | ||||
|     const enabledNetworksCount = networks.filter((networkEnabled) => networkEnabled).length; | ||||
|     this.isDropdownVisible = enabledNetworksCount > 1; | ||||
|   } | ||||
| 
 | ||||
|   collapse(): void { | ||||
|     this.navCollapsed = !this.navCollapsed; | ||||
|   } | ||||
| 
 | ||||
|   isSmallScreen() { | ||||
|   isSmallScreen(): boolean { | ||||
|     return window.innerWidth <= 767.98; | ||||
|   } | ||||
| 
 | ||||
| @ -117,4 +126,11 @@ export class MasterPageComponent implements OnInit, OnDestroy { | ||||
|   menuToggled(isOpen: boolean): void { | ||||
|     this.menuOpen = isOpen; | ||||
|   } | ||||
| 
 | ||||
|   ngOnDestroy(): void { | ||||
|     if (this.enterpriseInfo$) { | ||||
|       this.enterpriseInfo$.unsubscribe(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
| } | ||||
|  | ||||
| @ -23,7 +23,7 @@ export class LightningApiService { | ||||
|     } | ||||
|     this.apiBasePath = ''; // assume mainnet by default
 | ||||
|     this.stateService.networkChanged$.subscribe((network) => { | ||||
|       this.apiBasePath = network ? '/' + network : ''; | ||||
|       this.apiBasePath = network && network !== this.stateService.env.ROOT_NETWORK ? '/' + network : ''; | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|  | ||||
| @ -29,7 +29,7 @@ export class ApiService { | ||||
|     } | ||||
|     this.apiBasePath = ''; // assume mainnet by default
 | ||||
|     this.stateService.networkChanged$.subscribe((network) => { | ||||
|       this.apiBasePath = network ? '/' + network : ''; | ||||
|       this.apiBasePath = network && network !== this.stateService.env.ROOT_NETWORK ? '/' + network : ''; | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|  | ||||
| @ -25,7 +25,7 @@ export class ElectrsApiService { | ||||
|     } | ||||
|     this.apiBasePath = ''; // assume mainnet by default
 | ||||
|     this.stateService.networkChanged$.subscribe((network) => { | ||||
|       this.apiBasePath = network ? '/' + network : ''; | ||||
|       this.apiBasePath = network && network !== this.stateService.env.ROOT_NETWORK ? '/' + network : ''; | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|  | ||||
| @ -1,32 +1,31 @@ | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { Router, ActivatedRoute, NavigationEnd, ActivatedRouteSnapshot } from '@angular/router'; | ||||
| import { Router, NavigationEnd, ActivatedRouteSnapshot } from '@angular/router'; | ||||
| import { BehaviorSubject } from 'rxjs'; | ||||
| import { filter, map } from 'rxjs/operators'; | ||||
| import { StateService } from './state.service'; | ||||
| 
 | ||||
| const networkModules = { | ||||
|   bitcoin: { | ||||
|     subnets: [ | ||||
|       { name: 'mainnet', path: '' }, | ||||
|       { name: 'testnet', path: '/testnet' }, | ||||
|       { name: 'testnet4', path: '/testnet4' }, | ||||
|       { name: 'signet', path: '/signet' }, | ||||
|     ], | ||||
|   }, | ||||
|   liquid: { | ||||
|     subnets: [ | ||||
|       { name: 'liquid', path: '' }, | ||||
|       { name: 'liquidtestnet', path: '/testnet' }, | ||||
|     ], | ||||
|   } | ||||
| }; | ||||
| const networks = Object.keys(networkModules); | ||||
| 
 | ||||
| @Injectable({ | ||||
|   providedIn: 'root' | ||||
| }) | ||||
| export class NavigationService { | ||||
|   subnetPaths = new BehaviorSubject<Record<string,string>>({}); | ||||
|   networkModules = { | ||||
|     bitcoin: { | ||||
|       subnets: [ | ||||
|         { name: 'mainnet', path: '' }, | ||||
|         { name: 'testnet', path: this.stateService.env.ROOT_NETWORK === 'testnet' ? '/' : '/testnet' }, | ||||
|         { name: 'testnet4', path: this.stateService.env.ROOT_NETWORK === 'testnet4' ? '/' : '/testnet4' }, | ||||
|         { name: 'signet', path: this.stateService.env.ROOT_NETWORK === 'signet' ? '/' : '/signet' }, | ||||
|       ], | ||||
|     }, | ||||
|     liquid: { | ||||
|       subnets: [ | ||||
|         { name: 'liquid', path: '' }, | ||||
|         { name: 'liquidtestnet', path: '/testnet' }, | ||||
|       ], | ||||
|     } | ||||
|   }; | ||||
|   networks = Object.keys(this.networkModules); | ||||
| 
 | ||||
|   constructor( | ||||
|     private stateService: StateService, | ||||
| @ -46,11 +45,11 @@ export class NavigationService { | ||||
|     const networkPaths = {}; | ||||
|     let route = root; | ||||
|     // traverse the router state tree until all network paths are set, or we reach the end of the tree
 | ||||
|     while (!networks.reduce((acc, network) => acc && !!networkPaths[network], true) && route) { | ||||
|     while (!this.networks.reduce((acc, network) => acc && !!networkPaths[network], true) && route) { | ||||
|       // 'networkSpecific' paths may correspond to valid routes on other networks, but aren't directly compatible
 | ||||
|       // (e.g. we shouldn't link a mainnet transaction page to the same txid on testnet or liquid)
 | ||||
|       if (route.data?.networkSpecific) { | ||||
|         networks.forEach(network => { | ||||
|         this.networks.forEach(network => { | ||||
|           if (networkPaths[network] == null) { | ||||
|             networkPaths[network] = path; | ||||
|           } | ||||
| @ -59,7 +58,7 @@ export class NavigationService { | ||||
|       // null or empty networks list is shorthand for "compatible with every network"
 | ||||
|       if (route.data?.networks?.length) { | ||||
|         // if the list is non-empty, only those networks are compatible
 | ||||
|         networks.forEach(network => { | ||||
|         this.networks.forEach(network => { | ||||
|           if (!route.data.networks.includes(network)) { | ||||
|             if (networkPaths[network] == null) { | ||||
|               networkPaths[network] = path; | ||||
| @ -76,7 +75,7 @@ export class NavigationService { | ||||
|     } | ||||
| 
 | ||||
|     const subnetPaths = {}; | ||||
|     Object.entries(networkModules).forEach(([key, network]) => { | ||||
|     Object.entries(this.networkModules).forEach(([key, network]) => { | ||||
|       network.subnets.forEach(subnet => { | ||||
|         subnetPaths[subnet.name] = subnet.path + (networkPaths[key] != null ? networkPaths[key] : path); | ||||
|       }); | ||||
|  | ||||
| @ -43,6 +43,7 @@ export interface Customization { | ||||
| } | ||||
| 
 | ||||
| export interface Env { | ||||
|   MAINNET_ENABLED: boolean; | ||||
|   TESTNET_ENABLED: boolean; | ||||
|   TESTNET4_ENABLED: boolean; | ||||
|   SIGNET_ENABLED: boolean; | ||||
| @ -52,6 +53,7 @@ export interface Env { | ||||
|   KEEP_BLOCKS_AMOUNT: number; | ||||
|   OFFICIAL_MEMPOOL_SPACE: boolean; | ||||
|   BASE_MODULE: string; | ||||
|   ROOT_NETWORK: string; | ||||
|   NGINX_PROTOCOL?: string; | ||||
|   NGINX_HOSTNAME?: string; | ||||
|   NGINX_PORT?: string; | ||||
| @ -77,12 +79,14 @@ export interface Env { | ||||
| } | ||||
| 
 | ||||
| const defaultEnv: Env = { | ||||
|   'MAINNET_ENABLED': true, | ||||
|   'TESTNET_ENABLED': false, | ||||
|   'TESTNET4_ENABLED': false, | ||||
|   'SIGNET_ENABLED': false, | ||||
|   'LIQUID_ENABLED': false, | ||||
|   'LIQUID_TESTNET_ENABLED': false, | ||||
|   'BASE_MODULE': 'mempool', | ||||
|   'ROOT_NETWORK': '', | ||||
|   'ITEMS_PER_PAGE': 10, | ||||
|   'KEEP_BLOCKS_AMOUNT': 8, | ||||
|   'OFFICIAL_MEMPOOL_SPACE': false, | ||||
| @ -325,7 +329,12 @@ export class StateService { | ||||
|     // (?:preview\/)?                               optional "preview" prefix (non-capturing)
 | ||||
|     // (testnet|signet)/                            network string (captured as networkMatches[1])
 | ||||
|     // ($|\/)                                       network string must end or end with a slash
 | ||||
|     const networkMatches = url.match(/^\/(?:[a-z]{2}(?:-[A-Z]{2})?\/)?(?:preview\/)?(testnet4?|signet)($|\/)/); | ||||
|     let networkMatches: object = url.match(/^\/(?:[a-z]{2}(?:-[A-Z]{2})?\/)?(?:preview\/)?(testnet4?|signet)($|\/)/); | ||||
| 
 | ||||
|     if (!networkMatches && this.env.ROOT_NETWORK) { | ||||
|       networkMatches = { 1: this.env.ROOT_NETWORK }; | ||||
|     } | ||||
| 
 | ||||
|     switch (networkMatches && networkMatches[1]) { | ||||
|       case 'signet': | ||||
|         if (this.network !== 'signet') { | ||||
|  | ||||
| @ -55,7 +55,7 @@ export class WebsocketService { | ||||
|         .pipe(take(1)) | ||||
|         .subscribe((response) => this.handleResponse(response)); | ||||
|     } else { | ||||
|       this.network = this.stateService.network; | ||||
|       this.network = this.stateService.network === this.stateService.env.ROOT_NETWORK ? '' : this.stateService.network; | ||||
|       this.websocketSubject = webSocket<WebsocketResponse>(this.webSocketUrl.replace('{network}', this.network ? '/' + this.network : '')); | ||||
| 
 | ||||
|       const { response: theInitData } = this.transferState.get<any>(initData, null) || {}; | ||||
| @ -75,7 +75,7 @@ export class WebsocketService { | ||||
|         if (network === this.network) { | ||||
|           return; | ||||
|         } | ||||
|         this.network = network; | ||||
|         this.network = network === this.stateService.env.ROOT_NETWORK ? '' : network; | ||||
|         clearTimeout(this.onlineCheckTimeout); | ||||
|         clearTimeout(this.onlineCheckTimeoutTwo); | ||||
| 
 | ||||
|  | ||||
| @ -12,7 +12,9 @@ export class RelativeUrlPipe implements PipeTransform { | ||||
| 
 | ||||
|   transform(value: string, swapNetwork?: string): string { | ||||
|     let network = swapNetwork || this.stateService.network; | ||||
|     if (network === 'mainnet') network = ''; | ||||
|     if (network === 'mainnet' || network === this.stateService.env.ROOT_NETWORK) {  | ||||
|       network = ''; | ||||
|     } | ||||
|     if (this.stateService.env.BASE_MODULE === 'liquid' && network === 'liquidtestnet') { | ||||
|       network = 'testnet'; | ||||
|     } else if (this.stateService.env.BASE_MODULE !== 'mempool') { | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user