Merge pull request #187 from mempool/simon/angular-universal

Angular Universal Milestone 1
This commit is contained in:
wiz 2020-12-24 04:13:17 +09:00 committed by GitHub
commit 8f3db3690c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 35693 additions and 31483 deletions

2
frontend/.gitignore vendored
View File

@ -4,6 +4,8 @@
/dist /dist
/tmp /tmp
/out-tsc /out-tsc
server.run.js
# Only exists if Bazel was run # Only exists if Bazel was run
/bazel-out /bazel-out

View File

@ -101,7 +101,7 @@
"build": { "build": {
"builder": "@angular-devkit/build-angular:browser", "builder": "@angular-devkit/build-angular:browser",
"options": { "options": {
"outputPath": "dist/mempool", "outputPath": "dist/mempool/browser",
"index": "src/index.html", "index": "src/index.html",
"main": "src/main.ts", "main": "src/main.ts",
"polyfills": "src/polyfills.ts", "polyfills": "src/polyfills.ts",
@ -113,7 +113,8 @@
"src/robots.txt" "src/robots.txt"
], ],
"styles": [ "styles": [
"src/styles.scss" "src/styles.scss",
"node_modules/@fortawesome/fontawesome-svg-core/styles.css"
], ],
"scripts": [ "scripts": [
"generated-config.js" "generated-config.js"
@ -189,7 +190,8 @@
"tsConfig": [ "tsConfig": [
"tsconfig.app.json", "tsconfig.app.json",
"tsconfig.spec.json", "tsconfig.spec.json",
"e2e/tsconfig.json" "e2e/tsconfig.json",
"tsconfig.server.json"
], ],
"exclude": [ "exclude": [
"**/node_modules/**" "**/node_modules/**"
@ -207,6 +209,54 @@
"devServerTarget": "mempool:serve:production" "devServerTarget": "mempool:serve:production"
} }
} }
},
"server": {
"builder": "@angular-devkit/build-angular:server",
"options": {
"outputPath": "dist/mempool/server",
"main": "server.ts",
"tsConfig": "tsconfig.server.json"
},
"configurations": {
"production": {
"outputHashing": "media",
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"sourceMap": false,
"localize": true,
"optimization": true
}
}
},
"serve-ssr": {
"builder": "@nguniversal/builders:ssr-dev-server",
"options": {
"browserTarget": "mempool:build",
"serverTarget": "mempool:server"
},
"configurations": {
"production": {
"browserTarget": "mempool:build:production",
"serverTarget": "mempool:server:production"
}
}
},
"prerender": {
"builder": "@nguniversal/builders:prerender",
"options": {
"browserTarget": "mempool:build:production",
"serverTarget": "mempool:server:production",
"routes": [
"/"
]
},
"configurations": {
"production": {}
}
} }
} }
}}, }},

View File

@ -25,7 +25,7 @@ for (setting in configContent) {
const code = `(function (window) { const code = `(function (window) {
window.__env = window.__env || {};${settings.reduce((str, obj) => `${str} window.__env = window.__env || {};${settings.reduce((str, obj) => `${str}
window.__env.${obj.key} = ${ typeof obj.value === 'string' ? `'${obj.value}'` : obj.value };`, '')} window.__env.${obj.key} = ${ typeof obj.value === 'string' ? `'${obj.value}'` : obj.value };`, '')}
}(this));`; }(global || this));`;
try { try {
fs.writeFileSync(GENERATED_CONFIG_FILE_NAME, code, 'utf8'); fs.writeFileSync(GENERATED_CONFIG_FILE_NAME, code, 'utf8');

View File

@ -3,7 +3,10 @@
"LIQUID_ENABLED": false, "LIQUID_ENABLED": false,
"BISQ_ENABLED": false, "BISQ_ENABLED": false,
"BISQ_SEPARATE_BACKEND": false, "BISQ_SEPARATE_BACKEND": false,
"ELCTRS_ITEMS_PER_PAGE": 25, "ELECTRS_ITEMS_PER_PAGE": 25,
"KEEP_BLOCKS_AMOUNT": 8, "KEEP_BLOCKS_AMOUNT": 8,
"SPONSORS_ENABLED": false "SPONSORS_ENABLED": false,
} "NGINX_PROTOCOL": "http",
"NGINX_HOSTNAME": "127.0.0.1",
"NGINX_PORT": "80"
}

65974
frontend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -32,7 +32,11 @@
"generate-config": "node generate-config.js", "generate-config": "node generate-config.js",
"test": "ng test", "test": "ng test",
"lint": "ng lint", "lint": "ng lint",
"e2e": "ng e2e" "e2e": "ng e2e",
"dev:ssr": "npm run generate-config && ng run mempool:serve-ssr",
"serve:ssr": "node server.run.js",
"build:ssr": "npm run build && ng run mempool:server:production && ./node_modules/typescript/bin/tsc server.run.ts",
"prerender": "ng run mempool:prerender"
}, },
"dependencies": { "dependencies": {
"@angular/animations": "~10.2.3", "@angular/animations": "~10.2.3",
@ -43,6 +47,7 @@
"@angular/localize": "^10.2.3", "@angular/localize": "^10.2.3",
"@angular/platform-browser": "~10.2.3", "@angular/platform-browser": "~10.2.3",
"@angular/platform-browser-dynamic": "~10.2.3", "@angular/platform-browser-dynamic": "~10.2.3",
"@angular/platform-server": "~10.2.2",
"@angular/router": "~10.2.3", "@angular/router": "~10.2.3",
"@fortawesome/angular-fontawesome": "^0.7.0", "@fortawesome/angular-fontawesome": "^0.7.0",
"@fortawesome/fontawesome-common-types": "^0.2.30", "@fortawesome/fontawesome-common-types": "^0.2.30",
@ -50,13 +55,16 @@
"@fortawesome/free-solid-svg-icons": "^5.14.0", "@fortawesome/free-solid-svg-icons": "^5.14.0",
"@mempool/chartist": "^0.11.4", "@mempool/chartist": "^0.11.4",
"@ng-bootstrap/ng-bootstrap": "^7.0.0", "@ng-bootstrap/ng-bootstrap": "^7.0.0",
"@nguniversal/express-engine": "10.1.0",
"@types/qrcode": "^1.3.4", "@types/qrcode": "^1.3.4",
"bootstrap": "4.5.0", "bootstrap": "4.5.0",
"clipboard": "^2.0.4", "clipboard": "^2.0.4",
"domino": "^2.1.6",
"express": "^4.15.2",
"ngx-bootrap-multiselect": "^2.0.0", "ngx-bootrap-multiselect": "^2.0.0",
"ngx-infinite-scroll": "^9.0.0", "ngx-infinite-scroll": "^9.0.0",
"qrcode": "^1.4.4", "qrcode": "^1.4.4",
"rxjs": "^6.6.0", "rxjs": "^6.6.3",
"tlite": "^0.1.9", "tlite": "^0.1.9",
"tslib": "^2.0.0", "tslib": "^2.0.0",
"zone.js": "~0.10.3" "zone.js": "~0.10.3"
@ -64,12 +72,15 @@
"devDependencies": { "devDependencies": {
"@angular-devkit/build-angular": "^0.1002.0", "@angular-devkit/build-angular": "^0.1002.0",
"@angular/cli": "~10.2.0", "@angular/cli": "~10.2.0",
"@angular/compiler-cli": "~10.2.0", "@angular/compiler-cli": "~10.2.2",
"@angular/language-service": "~10.2.0", "@angular/language-service": "~10.2.2",
"@nguniversal/builders": "^10.1.0",
"@types/express": "^4.17.0",
"@types/jasmine": "~3.3.8", "@types/jasmine": "~3.3.8",
"@types/jasminewd2": "~2.0.3", "@types/jasminewd2": "~2.0.3",
"@types/node": "^12.11.1", "@types/node": "^12.11.1",
"codelyzer": "^6.0.0", "codelyzer": "^6.0.0",
"http-proxy-middleware": "^1.0.5",
"jasmine-core": "~3.5.0", "jasmine-core": "~3.5.0",
"jasmine-spec-reporter": "~5.0.0", "jasmine-spec-reporter": "~5.0.0",
"karma": "~5.0.0", "karma": "~5.0.0",

96
frontend/server.run.ts Normal file
View File

@ -0,0 +1,96 @@
import 'zone.js/dist/zone-node';
import './generated-config';
import * as domino from 'domino';
import * as express from 'express';
import * as fs from 'fs';
import * as path from 'path';
const {readFileSync, existsSync} = require('fs');
const {createProxyMiddleware} = require('http-proxy-middleware');
const template = fs.readFileSync(path.join(process.cwd(), 'dist/mempool/browser/en-US/', 'index.html')).toString();
const win = domino.createWindow(template);
// @ts-ignore
win.__env = global.__env;
// @ts-ignore
win.matchMedia = () => {
return {
matches: true
};
};
// @ts-ignore
win.setTimeout = (fn) => { fn(); };
win.document.body.scrollTo = (() => {});
// @ts-ignore
global['window'] = win;
global['document'] = win.document;
// @ts-ignore
global['history'] = { state: { } };
global['localStorage'] = {
getItem: () => '',
setItem: () => {},
removeItem: () => {},
clear: () => {},
length: 0,
key: () => '',
};
/**
* Return the list of supported and actually active locales
*/
function getActiveLocales() {
const angularConfig = JSON.parse(readFileSync('angular.json', 'utf8'));
const supportedLocales = [
angularConfig.projects.mempool.i18n.sourceLocale,
...Object.keys(angularConfig.projects.mempool.i18n.locales),
];
return supportedLocales.filter(locale => existsSync(`./dist/mempool/server/${locale}`));
}
function app() {
const server = express();
// proxy API to nginx
server.get('/api/**', createProxyMiddleware({
// @ts-ignore
target: win.__env.NGINX_PROTOCOL + '://' + win.__env.NGINX_HOSTNAME + ':' + win.__env.NGINX_PORT,
changeOrigin: true,
}));
// map / and /en to en-US
const defaultLocale = 'en-US';
console.log(`serving default locale: ${defaultLocale}`);
const appServerModule = require(`./dist/mempool/server/${defaultLocale}/main.js`);
server.use('/', appServerModule.app(defaultLocale));
server.use('/en', appServerModule.app(defaultLocale));
// map each locale to its localized main.js
getActiveLocales().forEach(locale => {
console.log('serving locale:', locale);
const appServerModule = require(`./dist/mempool/server/${locale}/main.js`);
// map everything to itself
server.use(`/${locale}`, appServerModule.app(locale));
});
return server;
}
function run() {
const port = process.env.PORT || 4000;
// Start up the Node server
app().listen(port, () => {
console.log(`Node Express server listening on port ${port}`);
});
}
run();

146
frontend/server.ts Normal file
View File

@ -0,0 +1,146 @@
import 'zone.js/dist/zone-node';
import './generated-config';
import { ngExpressEngine } from '@nguniversal/express-engine';
import * as express from 'express';
import * as fs from 'fs';
import * as path from 'path';
import * as domino from 'domino';
import { createProxyMiddleware } from 'http-proxy-middleware';
import { join } from 'path';
import { AppServerModule } from './src/main.server';
import { APP_BASE_HREF } from '@angular/common';
import { existsSync } from 'fs';
const template = fs.readFileSync(path.join(process.cwd(), 'dist/mempool/browser/en-US/', 'index.html')).toString();
const win = domino.createWindow(template);
// @ts-ignore
win.__env = global.__env;
// @ts-ignore
win.matchMedia = () => {
return {
matches: true
};
};
// @ts-ignore
win.setTimeout = (fn) => { fn(); };
win.document.body.scrollTo = (() => {});
// @ts-ignore
global['window'] = win;
global['document'] = win.document;
// @ts-ignore
global['history'] = { state: { } };
global['localStorage'] = {
getItem: () => '',
setItem: () => {},
removeItem: () => {},
clear: () => {},
length: 0,
key: () => '',
};
// The Express app is exported so that it can be used by serverless Functions.
export function app(locale: string): express.Express {
const server = express();
const distFolder = join(process.cwd(), `dist/mempool/browser/${locale}`);
const indexHtml = existsSync(join(distFolder, 'index.original.html')) ? 'index.original.html' : 'index';
// Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine)
server.engine('html', ngExpressEngine({
bootstrap: AppServerModule,
}));
server.set('view engine', 'html');
server.set('views', distFolder);
// only handle URLs that actually exist
//server.get(locale, getLocalizedSSR(indexHtml));
server.get('/', getLocalizedSSR(indexHtml));
server.get('/tx/*', getLocalizedSSR(indexHtml));
server.get('/block/*', getLocalizedSSR(indexHtml));
server.get('/mempool-block/*', getLocalizedSSR(indexHtml));
server.get('/address/*', getLocalizedSSR(indexHtml));
server.get('/blocks', getLocalizedSSR(indexHtml));
server.get('/graphs', getLocalizedSSR(indexHtml));
server.get('/liquid', getLocalizedSSR(indexHtml));
server.get('/liquid/tx/*', getLocalizedSSR(indexHtml));
server.get('/liquid/block/*', getLocalizedSSR(indexHtml));
server.get('/liquid/mempool-block/*', getLocalizedSSR(indexHtml));
server.get('/liquid/address/*', getLocalizedSSR(indexHtml));
server.get('/liquid/asset/*', getLocalizedSSR(indexHtml));
server.get('/liquid/blocks', getLocalizedSSR(indexHtml));
server.get('/liquid/graphs', getLocalizedSSR(indexHtml));
server.get('/liquid/assets', getLocalizedSSR(indexHtml));
server.get('/liquid/api', getLocalizedSSR(indexHtml));
server.get('/liquid/tv', getLocalizedSSR(indexHtml));
server.get('/liquid/status', getLocalizedSSR(indexHtml));
server.get('/liquid/about', getLocalizedSSR(indexHtml));
server.get('/testnet', getLocalizedSSR(indexHtml));
server.get('/testnet/tx/*', getLocalizedSSR(indexHtml));
server.get('/testnet/block/*', getLocalizedSSR(indexHtml));
server.get('/testnet/mempool-block/*', getLocalizedSSR(indexHtml));
server.get('/testnet/address/*', getLocalizedSSR(indexHtml));
server.get('/testnet/blocks', getLocalizedSSR(indexHtml));
server.get('/testnet/graphs', getLocalizedSSR(indexHtml));
server.get('/testnet/api', getLocalizedSSR(indexHtml));
server.get('/testnet/tv', getLocalizedSSR(indexHtml));
server.get('/testnet/status', getLocalizedSSR(indexHtml));
server.get('/testnet/about', getLocalizedSSR(indexHtml));
server.get('/bisq', getLocalizedSSR(indexHtml));
server.get('/bisq/tx/*', getLocalizedSSR(indexHtml));
server.get('/bisq/blocks', getLocalizedSSR(indexHtml));
server.get('/bisq/block/*', getLocalizedSSR(indexHtml));
server.get('/bisq/address/*', getLocalizedSSR(indexHtml));
server.get('/bisq/stats', getLocalizedSSR(indexHtml));
server.get('/bisq/about', getLocalizedSSR(indexHtml));
server.get('/bisq/api', getLocalizedSSR(indexHtml));
server.get('/about', getLocalizedSSR(indexHtml));
server.get('/api', getLocalizedSSR(indexHtml));
server.get('/tv', getLocalizedSSR(indexHtml));
server.get('/status', getLocalizedSSR(indexHtml));
server.get('/terms-of-service', getLocalizedSSR(indexHtml));
// fallback to static file handler so we send HTTP 404 to nginx
server.get('/**', express.static(distFolder, { maxAge: '1y' }));
return server;
}
function getLocalizedSSR(indexHtml) {
return (req, res) => {
res.render(indexHtml, {
req,
providers: [
{ provide: APP_BASE_HREF, useValue: req.baseUrl }
]
});
}
}
// only used for development mode
function run(): void {
const port = process.env.PORT || 4000;
// Start up the Node server
const server = app('en-US');
server.listen(port, () => {
console.log(`Node Express server listening on port ${port}`);
});
}
// Webpack will replace 'require' with '__webpack_require__'
// '__non_webpack_require__' is a proxy to Node 'require'
// The below code is to ensure that the server is run only when not requiring the bundle.
declare const __non_webpack_require__: NodeRequire;
const mainModule = __non_webpack_require__.main;
const moduleFilename = mainModule && mainModule.filename || '';
if (moduleFilename === __filename || moduleFilename.includes('iisnode')) {
run();
}
export * from './src/main.server';

View File

@ -222,7 +222,9 @@ const routes: Routes = [
]; ];
@NgModule({ @NgModule({
imports: [RouterModule.forRoot(routes)], imports: [RouterModule.forRoot(routes, {
initialNavigation: 'enabled'
})],
exports: [RouterModule] exports: [RouterModule]
}) })
export class AppRoutingModule { } export class AppRoutingModule { }

View File

@ -34,31 +34,6 @@ export const mempoolFeeColors = [
export const feeLevels = [1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 30, 40, 50, 60, 70, 80, 90, 100, 125, 150, 175, 200, export const feeLevels = [1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 30, 40, 50, 60, 70, 80, 90, 100, 125, 150, 175, 200,
250, 300, 350, 400, 500, 600, 700, 800, 900, 1000, 1200, 1400, 1600, 1800, 2000]; 250, 300, 350, 400, 500, 600, 700, 800, 900, 1000, 1200, 1400, 1600, 1800, 2000];
interface Env {
TESTNET_ENABLED: boolean;
LIQUID_ENABLED: boolean;
BISQ_ENABLED: boolean;
BISQ_SEPARATE_BACKEND: boolean;
SPONSORS_ENABLED: boolean;
ELCTRS_ITEMS_PER_PAGE: number;
KEEP_BLOCKS_AMOUNT: number;
}
const defaultEnv: Env = {
'TESTNET_ENABLED': false,
'LIQUID_ENABLED': false,
'BISQ_ENABLED': false,
'BISQ_SEPARATE_BACKEND': false,
'SPONSORS_ENABLED': false,
'ELCTRS_ITEMS_PER_PAGE': 25,
'KEEP_BLOCKS_AMOUNT': 8
};
const browserWindow = window || {};
// @ts-ignore
const browserWindowEnv = browserWindow.__env || {};
export const env: Env = Object.assign(defaultEnv, browserWindowEnv);
export interface Language { export interface Language {
code: string; code: string;
name: string; name: string;

View File

@ -1,6 +1,6 @@
import { BrowserModule } from '@angular/platform-browser'; import { BrowserModule, BrowserTransferStateModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { HttpClientModule } from '@angular/common/http'; import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { InfiniteScrollModule } from 'ngx-infinite-scroll'; import { InfiniteScrollModule } from 'ngx-infinite-scroll';
@ -48,6 +48,7 @@ import { faAngleDown, faAngleUp, faBolt, faChartArea, faCogs, faCubes, faDatabas
import { ApiDocsComponent } from './components/api-docs/api-docs.component'; import { ApiDocsComponent } from './components/api-docs/api-docs.component';
import { TermsOfServiceComponent } from './components/terms-of-service/terms-of-service.component'; import { TermsOfServiceComponent } from './components/terms-of-service/terms-of-service.component';
import { StorageService } from './services/storage.service'; import { StorageService } from './services/storage.service';
import { HttpCacheInterceptor } from './services/http-cache.interceptor';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -84,7 +85,8 @@ import { StorageService } from './services/storage.service';
TermsOfServiceComponent, TermsOfServiceComponent,
], ],
imports: [ imports: [
BrowserModule, BrowserModule.withServerTransition({ appId: 'serverApp' }),
BrowserTransferStateModule,
AppRoutingModule, AppRoutingModule,
HttpClientModule, HttpClientModule,
BrowserAnimationsModule, BrowserAnimationsModule,
@ -100,6 +102,7 @@ import { StorageService } from './services/storage.service';
AudioService, AudioService,
SeoService, SeoService,
StorageService, StorageService,
{ provide: HTTP_INTERCEPTORS, useClass: HttpCacheInterceptor, multi: true }
], ],
bootstrap: [AppComponent] bootstrap: [AppComponent]
}) })

View File

@ -0,0 +1,20 @@
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { NgModule } from '@angular/core';
import { ServerModule, ServerTransferStateModule } from '@angular/platform-server';
import { AppModule } from './app.module';
import { AppComponent } from './components/app/app.component';
import { HttpCacheInterceptor } from './services/http-cache.interceptor';
@NgModule({
imports: [
AppModule,
ServerModule,
ServerTransferStateModule,
],
providers: [
{ provide: HTTP_INTERCEPTORS, useClass: HttpCacheInterceptor, multi: true }
],
bootstrap: [AppComponent],
})
export class AppServerModule {}

View File

@ -5,7 +5,6 @@ import { StateService } from 'src/app/services/state.service';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { ApiService } from 'src/app/services/api.service'; import { ApiService } from 'src/app/services/api.service';
import { env } from '../../app.constants';
import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
@ -21,7 +20,7 @@ export class AboutComponent implements OnInit {
donationStatus = 1; donationStatus = 1;
sponsors$: Observable<any>; sponsors$: Observable<any>;
donationObj: any; donationObj: any;
sponsorsEnabled = env.SPONSORS_ENABLED; sponsorsEnabled = this.stateService.env.SPONSORS_ENABLED;
sponsors = null; sponsors = null;
constructor( constructor(

View File

@ -1,3 +1,4 @@
import { Location } from '@angular/common';
import { Component, HostListener, OnInit, Inject, LOCALE_ID, HostBinding } from '@angular/core'; import { Component, HostListener, OnInit, Inject, LOCALE_ID, HostBinding } from '@angular/core';
import { Router, NavigationEnd } from '@angular/router'; import { Router, NavigationEnd } from '@angular/router';
import { WebsocketService } from '../../services/websocket.service'; import { WebsocketService } from '../../services/websocket.service';
@ -15,6 +16,7 @@ export class AppComponent implements OnInit {
public router: Router, public router: Router,
private websocketService: WebsocketService, private websocketService: WebsocketService,
private stateService: StateService, private stateService: StateService,
private location: Location,
@Inject(LOCALE_ID) private locale: string, @Inject(LOCALE_ID) private locale: string,
) { ) {
if (this.locale.startsWith('ar') || this.locale.startsWith('fa')) { if (this.locale.startsWith('ar') || this.locale.startsWith('fa')) {
@ -37,7 +39,7 @@ export class AppComponent implements OnInit {
ngOnInit() { ngOnInit() {
this.router.events.subscribe((val) => { this.router.events.subscribe((val) => {
if (val instanceof NavigationEnd) { if (val instanceof NavigationEnd) {
this.link.setAttribute('href', 'https://mempool.space' + (location.pathname === '/' ? '' : location.pathname)); this.link.setAttribute('href', 'https://mempool.space' + (this.location.path() === '/' ? '' : this.location.path()));
} }
}); });
} }

View File

@ -7,7 +7,6 @@ import { Block, Transaction, Vout } from '../../interfaces/electrs.interface';
import { of, Subscription } from 'rxjs'; import { of, Subscription } from 'rxjs';
import { StateService } from '../../services/state.service'; import { StateService } from '../../services/state.service';
import { SeoService } from 'src/app/services/seo.service'; import { SeoService } from 'src/app/services/seo.service';
import { env } from 'src/app/app.constants';
import { WebsocketService } from 'src/app/services/websocket.service'; import { WebsocketService } from 'src/app/services/websocket.service';
@Component({ @Component({
@ -31,7 +30,7 @@ export class BlockComponent implements OnInit, OnDestroy {
paginationMaxSize: number; paginationMaxSize: number;
coinbaseTx: Transaction; coinbaseTx: Transaction;
page = 1; page = 1;
itemsPerPage = env.ELCTRS_ITEMS_PER_PAGE; itemsPerPage: number;
constructor( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
@ -47,6 +46,7 @@ export class BlockComponent implements OnInit, OnDestroy {
this.websocketService.want(['blocks', 'mempool-blocks']); this.websocketService.want(['blocks', 'mempool-blocks']);
this.paginationMaxSize = window.matchMedia('(max-width: 700px)').matches ? 3 : 5; this.paginationMaxSize = window.matchMedia('(max-width: 700px)').matches ? 3 : 5;
this.network = this.stateService.network; this.network = this.stateService.network;
this.itemsPerPage = this.stateService.env.ELECTRS_ITEMS_PER_PAGE;
this.subscription = this.route.paramMap this.subscription = this.route.paramMap
.pipe( .pipe(

View File

@ -3,7 +3,6 @@ import { Subscription } from 'rxjs';
import { Block } from 'src/app/interfaces/electrs.interface'; import { Block } from 'src/app/interfaces/electrs.interface';
import { StateService } from 'src/app/services/state.service'; import { StateService } from 'src/app/services/state.service';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { env } from 'src/app/app.constants';
@Component({ @Component({
selector: 'app-blockchain-blocks', selector: 'app-blockchain-blocks',
@ -78,7 +77,7 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy {
this.cd.markForCheck(); this.cd.markForCheck();
}, 50); }, 50);
if (this.blocks.length === env.KEEP_BLOCKS_AMOUNT) { if (this.blocks.length === this.stateService.env.KEEP_BLOCKS_AMOUNT) {
this.blocksFilled = true; this.blocksFilled = true;
} }
this.cd.markForCheck(); this.cd.markForCheck();

View File

@ -1,6 +1,5 @@
import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { StateService } from '../../services/state.service'; import { Env, StateService } from '../../services/state.service';
import { env } from 'src/app/app.constants';
import { Observable, merge, of } from 'rxjs'; import { Observable, merge, of } from 'rxjs';
@Component({ @Component({
@ -9,7 +8,7 @@ import { Observable, merge, of } from 'rxjs';
styleUrls: ['./master-page.component.scss'], styleUrls: ['./master-page.component.scss'],
}) })
export class MasterPageComponent implements OnInit { export class MasterPageComponent implements OnInit {
env = env; env: Env;
network$: Observable<string>; network$: Observable<string>;
connectionState$: Observable<number>; connectionState$: Observable<number>;
navCollapsed = false; navCollapsed = false;
@ -20,6 +19,7 @@ export class MasterPageComponent implements OnInit {
) { } ) { }
ngOnInit() { ngOnInit() {
this.env = this.stateService.env;
this.connectionState$ = this.stateService.connectionState$; this.connectionState$ = this.stateService.connectionState$;
this.network$ = merge(of(''), this.stateService.networkChanged$); this.network$ = merge(of(''), this.stateService.networkChanged$);
} }

View File

@ -5,7 +5,6 @@ import { switchMap, map, tap, filter } from 'rxjs/operators';
import { MempoolBlock } from 'src/app/interfaces/websocket.interface'; import { MempoolBlock } from 'src/app/interfaces/websocket.interface';
import { Observable, BehaviorSubject } from 'rxjs'; import { Observable, BehaviorSubject } from 'rxjs';
import { SeoService } from 'src/app/services/seo.service'; import { SeoService } from 'src/app/services/seo.service';
import { env } from 'src/app/app.constants';
import { WebsocketService } from 'src/app/services/websocket.service'; import { WebsocketService } from 'src/app/services/websocket.service';
@Component({ @Component({
@ -70,7 +69,7 @@ export class MempoolBlockComponent implements OnInit, OnDestroy {
const blocksInBlock = Math.ceil(mempoolBlock.blockVSize / 1000000); const blocksInBlock = Math.ceil(mempoolBlock.blockVSize / 1000000);
if (this.mempoolBlockIndex === 0) { if (this.mempoolBlockIndex === 0) {
return $localize`:@@mempool-block.next.block:Next block`; return $localize`:@@mempool-block.next.block:Next block`;
} else if (this.mempoolBlockIndex === env.KEEP_BLOCKS_AMOUNT - 1 && blocksInBlock > 1 ) { } else if (this.mempoolBlockIndex === this.stateService.env.KEEP_BLOCKS_AMOUNT - 1 && blocksInBlock > 1) {
return $localize`:@@mempool-block.stack.of.blocks:Stack of ${blocksInBlock}:INTERPOLATION: mempool blocks`; return $localize`:@@mempool-block.stack.of.blocks:Stack of ${blocksInBlock}:INTERPOLATION: mempool blocks`;
} else { } else {
return $localize`:@@mempool-block.block.no:Mempool block ${this.mempoolBlockIndex + 1}:INTERPOLATION:`; return $localize`:@@mempool-block.block.no:Mempool block ${this.mempoolBlockIndex + 1}:INTERPOLATION:`;

View File

@ -1,5 +1,6 @@
import { Component, Input, AfterViewInit, OnDestroy, ViewChild, ElementRef } from '@angular/core'; import { Component, Input, AfterViewInit, OnDestroy, ViewChild, ElementRef } from '@angular/core';
import * as QRCode from 'qrcode/build/qrcode.js'; import * as QRCode from 'qrcode/build/qrcode.js';
import { StateService } from 'src/app/services/state.service';
@Component({ @Component({
selector: 'app-qrcode', selector: 'app-qrcode',
@ -14,9 +15,14 @@ export class QrcodeComponent implements AfterViewInit {
qrcodeObject: any; qrcodeObject: any;
constructor() { } constructor(
private stateService: StateService,
) { }
ngAfterViewInit() { ngAfterViewInit() {
if (!this.stateService.isBrowser) {
return;
}
const opts = { const opts = {
errorCorrectionLevel: 'H', errorCorrectionLevel: 'H',
margin: 0, margin: 0,

View File

@ -1,14 +1,18 @@
import { import {
Component, Component,
ElementRef, ElementRef,
Inject,
Input, Input,
OnChanges, OnChanges,
OnDestroy, OnDestroy,
OnInit, OnInit,
PLATFORM_ID,
SimpleChanges, SimpleChanges,
ViewEncapsulation ViewEncapsulation
} from '@angular/core'; } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
import * as Chartist from '@mempool/chartist'; import * as Chartist from '@mempool/chartist';
/** /**
@ -63,16 +67,25 @@ export class ChartistComponent implements OnInit, OnChanges, OnDestroy {
// @ts-ignore // @ts-ignore
@Input() public events: ChartEvent; @Input() public events: ChartEvent;
isBrowser: boolean = isPlatformBrowser(this.platformId);
// @ts-ignore // @ts-ignore
public chart: ChartInterfaces; public chart: ChartInterfaces;
private element: HTMLElement; private element: HTMLElement;
constructor(element: ElementRef) { constructor(
element: ElementRef,
@Inject(PLATFORM_ID) private platformId: any,
) {
this.element = element.nativeElement; this.element = element.nativeElement;
} }
public ngOnInit(): Promise<ChartInterfaces> { public ngOnInit(): Promise<ChartInterfaces> {
if (!this.isBrowser) {
return;
}
if (!this.type || !this.data) { if (!this.type || !this.data) {
Promise.reject('Expected at least type and data.'); Promise.reject('Expected at least type and data.');
} }
@ -87,6 +100,10 @@ export class ChartistComponent implements OnInit, OnChanges, OnDestroy {
} }
public ngOnChanges(changes: SimpleChanges): void { public ngOnChanges(changes: SimpleChanges): void {
if (!this.isBrowser) {
return;
}
this.update(changes); this.update(changes);
} }
@ -194,7 +211,7 @@ export class ChartistComponent implements OnInit, OnChanges, OnDestroy {
}; };
}; };
}(window, document, Chartist)); }(null, null, Chartist));
/** /**
@ -278,7 +295,7 @@ export class ChartistComponent implements OnInit, OnChanges, OnDestroy {
}; };
}; };
}(window, document, Chartist)); }(null, null, Chartist));
const defaultOptions = { const defaultOptions = {
className: '', className: '',

View File

@ -1,4 +1,5 @@
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, Input, ChangeDetectorRef, OnChanges } from '@angular/core'; import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, Input, ChangeDetectorRef, OnChanges, PLATFORM_ID, Inject } from '@angular/core';
import { StateService } from 'src/app/services/state.service';
@Component({ @Component({
selector: 'app-time-since', selector: 'app-time-since',
@ -14,7 +15,8 @@ export class TimeSinceComponent implements OnInit, OnChanges, OnDestroy {
@Input() fastRender = false; @Input() fastRender = false;
constructor( constructor(
private ref: ChangeDetectorRef private ref: ChangeDetectorRef,
private stateService: StateService,
) { ) {
this.intervals = { this.intervals = {
year: 31536000, year: 31536000,
@ -28,6 +30,11 @@ export class TimeSinceComponent implements OnInit, OnChanges, OnDestroy {
} }
ngOnInit() { ngOnInit() {
if (!this.stateService.isBrowser) {
this.text = this.calculate();
this.ref.markForCheck();
return;
}
this.interval = window.setInterval(() => { this.interval = window.setInterval(() => {
this.text = this.calculate(); this.text = this.calculate();
this.ref.markForCheck(); this.ref.markForCheck();

View File

@ -1,57 +1,60 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http'; import { HttpClient, HttpParams } from '@angular/common/http';
import { OptimizedMempoolStats } from '../interfaces/node-api.interface'; import { OptimizedMempoolStats } from '../interfaces/node-api.interface';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { StateService } from './state.service'; import { StateService } from './state.service';
import { env } from '../app.constants'; import { WebsocketResponse } from '../interfaces/websocket.interface';
const API_BASE_URL = '{network}/api/v1';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class ApiService { export class ApiService {
apiBaseUrl: string; private apiBaseUrl: string; // base URL is protocol, hostname, and port
private apiBasePath: string; // network path is /testnet, etc. or '' for mainnet
constructor( constructor(
private httpClient: HttpClient, private httpClient: HttpClient,
private stateService: StateService, private stateService: StateService,
) { ) {
this.apiBaseUrl = API_BASE_URL.replace('{network}', ''); this.apiBaseUrl = ''; // use relative URL by default
if (!stateService.isBrowser) { // except when inside AU SSR process
this.apiBaseUrl = this.stateService.env.NGINX_PROTOCOL + '://' + this.stateService.env.NGINX_HOSTNAME + ':' + this.stateService.env.NGINX_PORT;
}
this.apiBasePath = ''; // assume mainnet by default
this.stateService.networkChanged$.subscribe((network) => { this.stateService.networkChanged$.subscribe((network) => {
if (network === 'bisq' && !env.BISQ_SEPARATE_BACKEND) { if (network === 'bisq' && !this.stateService.env.BISQ_SEPARATE_BACKEND) {
network = ''; network = '';
} }
this.apiBaseUrl = API_BASE_URL.replace('{network}', network ? '/' + network : ''); this.apiBasePath = network ? '/' + network : '';
}); });
} }
list2HStatistics$(): Observable<OptimizedMempoolStats[]> { list2HStatistics$(): Observable<OptimizedMempoolStats[]> {
return this.httpClient.get<OptimizedMempoolStats[]>(this.apiBaseUrl + '/statistics/2h'); return this.httpClient.get<OptimizedMempoolStats[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/statistics/2h');
} }
list24HStatistics$(): Observable<OptimizedMempoolStats[]> { list24HStatistics$(): Observable<OptimizedMempoolStats[]> {
return this.httpClient.get<OptimizedMempoolStats[]>(this.apiBaseUrl + '/statistics/24h'); return this.httpClient.get<OptimizedMempoolStats[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/statistics/24h');
} }
list1WStatistics$(): Observable<OptimizedMempoolStats[]> { list1WStatistics$(): Observable<OptimizedMempoolStats[]> {
return this.httpClient.get<OptimizedMempoolStats[]>(this.apiBaseUrl + '/statistics/1w'); return this.httpClient.get<OptimizedMempoolStats[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/statistics/1w');
} }
list1MStatistics$(): Observable<OptimizedMempoolStats[]> { list1MStatistics$(): Observable<OptimizedMempoolStats[]> {
return this.httpClient.get<OptimizedMempoolStats[]>(this.apiBaseUrl + '/statistics/1m'); return this.httpClient.get<OptimizedMempoolStats[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/statistics/1m');
} }
list3MStatistics$(): Observable<OptimizedMempoolStats[]> { list3MStatistics$(): Observable<OptimizedMempoolStats[]> {
return this.httpClient.get<OptimizedMempoolStats[]>(this.apiBaseUrl + '/statistics/3m'); return this.httpClient.get<OptimizedMempoolStats[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/statistics/3m');
} }
list6MStatistics$(): Observable<OptimizedMempoolStats[]> { list6MStatistics$(): Observable<OptimizedMempoolStats[]> {
return this.httpClient.get<OptimizedMempoolStats[]>(this.apiBaseUrl + '/statistics/6m'); return this.httpClient.get<OptimizedMempoolStats[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/statistics/6m');
} }
list1YStatistics$(): Observable<OptimizedMempoolStats[]> { list1YStatistics$(): Observable<OptimizedMempoolStats[]> {
return this.httpClient.get<OptimizedMempoolStats[]>(this.apiBaseUrl + '/statistics/1y'); return this.httpClient.get<OptimizedMempoolStats[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/statistics/1y');
} }
getTransactionTimes$(txIds: string[]): Observable<number[]> { getTransactionTimes$(txIds: string[]): Observable<number[]> {
@ -59,7 +62,7 @@ export class ApiService {
txIds.forEach((txId: string) => { txIds.forEach((txId: string) => {
params = params.append('txId[]', txId); params = params.append('txId[]', txId);
}); });
return this.httpClient.get<number[]>(this.apiBaseUrl + '/transaction-times', { params }); return this.httpClient.get<number[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/transaction-times', { params });
} }
requestDonation$(amount: number, orderId: string): Observable<any> { requestDonation$(amount: number, orderId: string): Observable<any> {
@ -67,10 +70,14 @@ export class ApiService {
amount: amount, amount: amount,
orderId: orderId, orderId: orderId,
}; };
return this.httpClient.post<any>(this.apiBaseUrl + '/donations', params); return this.httpClient.post<any>(this.apiBaseUrl + this.apiBasePath + '/api/v1/donations', params);
} }
getDonation$(): Observable<any[]> { getDonation$(): Observable<any[]> {
return this.httpClient.get<any[]>(this.apiBaseUrl + '/donations'); return this.httpClient.get<any[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/donations');
}
getInitData$(): Observable<WebsocketResponse> {
return this.httpClient.get<WebsocketResponse>(this.apiBaseUrl + this.apiBasePath + '/api/v1/init-data');
} }
} }

View File

@ -2,6 +2,7 @@ import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { shareReplay } from 'rxjs/operators'; import { shareReplay } from 'rxjs/operators';
import { StateService } from './state.service';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -13,9 +14,15 @@ export class AssetsService {
constructor( constructor(
private httpClient: HttpClient, private httpClient: HttpClient,
private stateService: StateService,
) { ) {
this.getAssetsJson$ = this.httpClient.get('/resources/assets.json').pipe(shareReplay()); let apiBaseUrl = '';
this.getAssetsMinimalJson$ = this.httpClient.get('/resources/assets.minimal.json').pipe(shareReplay()); if (!this.stateService.isBrowser) {
this.getMiningPools$ = this.httpClient.get('/resources/pools.json').pipe(shareReplay()); apiBaseUrl = this.stateService.env.NGINX_PROTOCOL + '://' + this.stateService.env.NGINX_HOSTNAME + ':' + this.stateService.env.NGINX_PORT;
}
this.getAssetsJson$ = this.httpClient.get(apiBaseUrl + '/resources/assets.json').pipe(shareReplay());
this.getAssetsMinimalJson$ = this.httpClient.get(apiBaseUrl + '/resources/assets.minimal.json').pipe(shareReplay());
this.getMiningPools$ = this.httpClient.get(apiBaseUrl + '/resources/pools.json').pipe(shareReplay());
} }
} }

View File

@ -4,13 +4,17 @@ import { Injectable } from '@angular/core';
providedIn: 'root' providedIn: 'root'
}) })
export class AudioService { export class AudioService {
audio = new Audio(); audio: HTMLAudioElement;
isPlaying = false; isPlaying = false;
constructor() { } constructor() {
try {
this.audio = new Audio();
} catch (e) {}
}
public playSound(name: 'magic' | 'chime' | 'cha-ching' | 'bright-harmony') { public playSound(name: 'magic' | 'chime' | 'cha-ching' | 'bright-harmony') {
if (this.isPlaying) { if (this.isPlaying || !this.audio) {
return; return;
} }
this.isPlaying = true; this.isPlaying = true;

View File

@ -3,86 +3,88 @@ import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { Block, Transaction, Address, Outspend, Recent, Asset } from '../interfaces/electrs.interface'; import { Block, Transaction, Address, Outspend, Recent, Asset } from '../interfaces/electrs.interface';
import { StateService } from './state.service'; import { StateService } from './state.service';
import { env } from '../app.constants';
const API_BASE_URL = '{network}/api';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class ElectrsApiService { export class ElectrsApiService {
apiBaseUrl: string; private apiBaseUrl: string; // base URL is protocol, hostname, and port
private apiBasePath: string; // network path is /testnet, etc. or '' for mainnet
constructor( constructor(
private httpClient: HttpClient, private httpClient: HttpClient,
private stateService: StateService, private stateService: StateService,
) { ) {
this.apiBaseUrl = API_BASE_URL.replace('{network}', ''); this.apiBaseUrl = ''; // use relative URL by default
if (!stateService.isBrowser) { // except when inside AU SSR process
this.apiBaseUrl = this.stateService.env.NGINX_PROTOCOL + '://' + this.stateService.env.NGINX_HOSTNAME + ':' + this.stateService.env.NGINX_PORT;
}
this.apiBasePath = ''; // assume mainnet by default
this.stateService.networkChanged$.subscribe((network) => { this.stateService.networkChanged$.subscribe((network) => {
if (network === 'bisq') { if (network === 'bisq') {
network = ''; network = '';
} }
this.apiBaseUrl = API_BASE_URL.replace('{network}', network ? '/' + network : ''); this.apiBasePath = network ? '/' + network : '';
}); });
} }
getBlock$(hash: string): Observable<Block> { getBlock$(hash: string): Observable<Block> {
return this.httpClient.get<Block>(this.apiBaseUrl + '/block/' + hash); return this.httpClient.get<Block>(this.apiBaseUrl + this.apiBasePath + '/api/block/' + hash);
} }
listBlocks$(height?: number): Observable<Block[]> { listBlocks$(height?: number): Observable<Block[]> {
return this.httpClient.get<Block[]>(this.apiBaseUrl + '/blocks/' + (height || '')); return this.httpClient.get<Block[]>(this.apiBaseUrl + this.apiBasePath + '/api/blocks/' + (height || ''));
} }
getTransaction$(txId: string): Observable<Transaction> { getTransaction$(txId: string): Observable<Transaction> {
return this.httpClient.get<Transaction>(this.apiBaseUrl + '/tx/' + txId); return this.httpClient.get<Transaction>(this.apiBaseUrl + this.apiBasePath + '/api/tx/' + txId);
} }
getRecentTransaction$(): Observable<Recent[]> { getRecentTransaction$(): Observable<Recent[]> {
return this.httpClient.get<Recent[]>(this.apiBaseUrl + '/mempool/recent'); return this.httpClient.get<Recent[]>(this.apiBaseUrl + this.apiBasePath + '/api/mempool/recent');
} }
getOutspend$(hash: string, vout: number): Observable<Outspend> { getOutspend$(hash: string, vout: number): Observable<Outspend> {
return this.httpClient.get<Outspend>(this.apiBaseUrl + '/tx/' + hash + '/outspend/' + vout); return this.httpClient.get<Outspend>(this.apiBaseUrl + this.apiBasePath + '/api/tx/' + hash + '/outspend/' + vout);
} }
getOutspends$(hash: string): Observable<Outspend[]> { getOutspends$(hash: string): Observable<Outspend[]> {
return this.httpClient.get<Outspend[]>(this.apiBaseUrl + '/tx/' + hash + '/outspends'); return this.httpClient.get<Outspend[]>(this.apiBaseUrl + this.apiBasePath + '/api/tx/' + hash + '/outspends');
} }
getBlockTransactions$(hash: string, index: number = 0): Observable<Transaction[]> { getBlockTransactions$(hash: string, index: number = 0): Observable<Transaction[]> {
return this.httpClient.get<Transaction[]>(this.apiBaseUrl + '/block/' + hash + '/txs/' + index); return this.httpClient.get<Transaction[]>(this.apiBaseUrl + this.apiBasePath + '/api/block/' + hash + '/txs/' + index);
} }
getBlockHashFromHeight$(height: number): Observable<string> { getBlockHashFromHeight$(height: number): Observable<string> {
return this.httpClient.get(this.apiBaseUrl + '/block-height/' + height, {responseType: 'text'}); return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/block-height/' + height, {responseType: 'text'});
} }
getAddress$(address: string): Observable<Address> { getAddress$(address: string): Observable<Address> {
return this.httpClient.get<Address>(this.apiBaseUrl + '/address/' + address); return this.httpClient.get<Address>(this.apiBaseUrl + this.apiBasePath + '/api/address/' + address);
} }
getAddressTransactions$(address: string): Observable<Transaction[]> { getAddressTransactions$(address: string): Observable<Transaction[]> {
return this.httpClient.get<Transaction[]>(this.apiBaseUrl + '/address/' + address + '/txs'); return this.httpClient.get<Transaction[]>(this.apiBaseUrl + this.apiBasePath + '/api/address/' + address + '/txs');
} }
getAddressTransactionsFromHash$(address: string, txid: string): Observable<Transaction[]> { getAddressTransactionsFromHash$(address: string, txid: string): Observable<Transaction[]> {
return this.httpClient.get<Transaction[]>(this.apiBaseUrl + '/address/' + address + '/txs/chain/' + txid); return this.httpClient.get<Transaction[]>(this.apiBaseUrl + this.apiBasePath + '/api/address/' + address + '/txs/chain/' + txid);
} }
getAsset$(assetId: string): Observable<Asset> { getAsset$(assetId: string): Observable<Asset> {
return this.httpClient.get<Asset>(this.apiBaseUrl + '/asset/' + assetId); return this.httpClient.get<Asset>(this.apiBaseUrl + this.apiBasePath + '/api/asset/' + assetId);
} }
getAssetTransactions$(assetId: string): Observable<Transaction[]> { getAssetTransactions$(assetId: string): Observable<Transaction[]> {
return this.httpClient.get<Transaction[]>(this.apiBaseUrl + '/asset/' + assetId + '/txs'); return this.httpClient.get<Transaction[]>(this.apiBaseUrl + this.apiBasePath + '/api/asset/' + assetId + '/txs');
} }
getAssetTransactionsFromHash$(assetId: string, txid: string): Observable<Transaction[]> { getAssetTransactionsFromHash$(assetId: string, txid: string): Observable<Transaction[]> {
return this.httpClient.get<Transaction[]>(this.apiBaseUrl + '/asset/' + assetId + '/txs/chain/' + txid); return this.httpClient.get<Transaction[]>(this.apiBaseUrl + this.apiBasePath + '/api/asset/' + assetId + '/txs/chain/' + txid);
} }
getAddressesByPrefix$(prefix: string): Observable<string[]> { getAddressesByPrefix$(prefix: string): Observable<string[]> {
return this.httpClient.get<string[]>(this.apiBaseUrl + '/address-prefix/' + prefix); return this.httpClient.get<string[]>(this.apiBaseUrl + this.apiBasePath + '/api/address-prefix/' + prefix);
} }
} }

View File

@ -0,0 +1,42 @@
import { Inject, Injectable, PLATFORM_ID } from '@angular/core';
import { HttpInterceptor, HttpEvent, HttpRequest, HttpHandler, HttpResponse } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators';
import { TransferState, makeStateKey } from '@angular/platform-browser';
import { isPlatformBrowser } from '@angular/common';
@Injectable()
export class HttpCacheInterceptor implements HttpInterceptor {
isBrowser: boolean = isPlatformBrowser(this.platformId);
constructor(
private transferState: TransferState,
@Inject(PLATFORM_ID) private platformId: any,
) { }
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (this.isBrowser && request.method === 'GET') {
const cachedResponse = this.transferState.get(makeStateKey(request.url), null);
if (cachedResponse) {
const modifiedResponse = new HttpResponse<any>({
headers: cachedResponse.headers,
body: cachedResponse.body,
status: cachedResponse.status,
statusText: cachedResponse.statusText,
url: cachedResponse.url
});
this.transferState.remove(makeStateKey(request.url));
return of(modifiedResponse);
}
}
return next.handle(request)
.pipe(tap((event: HttpEvent<any>) => {
if (!this.isBrowser && event instanceof HttpResponse) {
let keyId = request.url.split('/').slice(3).join('/');
this.transferState.set(makeStateKey('/' + keyId), event);
}
}));
}
}

View File

@ -1,5 +1,5 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Title } from '@angular/platform-browser'; import { Title, Meta } from '@angular/platform-browser';
import { StateService } from './state.service'; import { StateService } from './state.service';
@Injectable({ @Injectable({
@ -10,6 +10,7 @@ export class SeoService {
constructor( constructor(
private titleService: Title, private titleService: Title,
private metaService: Meta,
private stateService: StateService, private stateService: StateService,
) { ) {
this.stateService.networkChanged$.subscribe((network) => this.network = network); this.stateService.networkChanged$.subscribe((network) => this.network = network);
@ -17,10 +18,12 @@ export class SeoService {
setTitle(newTitle: string): void { setTitle(newTitle: string): void {
this.titleService.setTitle(newTitle + ' - ' + this.getTitle()); this.titleService.setTitle(newTitle + ' - ' + this.getTitle());
this.metaService.updateTag({ property: 'og:title', content: newTitle});
} }
resetTitle(): void { resetTitle(): void {
this.titleService.setTitle(this.getTitle()); this.titleService.setTitle(this.getTitle());
this.metaService.updateTag({ property: 'og:title', content: this.getTitle()});
} }
getTitle(): string { getTitle(): string {

View File

@ -1,11 +1,11 @@
import { Injectable } from '@angular/core'; import { Inject, Injectable, PLATFORM_ID } from '@angular/core';
import { ReplaySubject, BehaviorSubject, Subject, fromEvent } from 'rxjs'; import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable } from 'rxjs';
import { Block, Transaction } from '../interfaces/electrs.interface'; import { Block, Transaction } from '../interfaces/electrs.interface';
import { MempoolBlock, MempoolInfo, TransactionStripped } from '../interfaces/websocket.interface'; import { MempoolBlock, MempoolInfo, TransactionStripped } from '../interfaces/websocket.interface';
import { OptimizedMempoolStats } from '../interfaces/node-api.interface'; import { OptimizedMempoolStats } from '../interfaces/node-api.interface';
import { Router, NavigationStart } from '@angular/router'; import { Router, NavigationStart } from '@angular/router';
import { env } from '../app.constants'; import { isPlatformBrowser } from '@angular/common';
import { shareReplay, map } from 'rxjs/operators'; import { map, shareReplay } from 'rxjs/operators';
interface MarkBlockState { interface MarkBlockState {
blockHeight?: number; blockHeight?: number;
@ -13,15 +13,43 @@ interface MarkBlockState {
txFeePerVSize?: number; txFeePerVSize?: number;
} }
export interface Env {
TESTNET_ENABLED: boolean;
LIQUID_ENABLED: boolean;
BISQ_ENABLED: boolean;
BISQ_SEPARATE_BACKEND: boolean;
SPONSORS_ENABLED: boolean;
ELECTRS_ITEMS_PER_PAGE: number;
KEEP_BLOCKS_AMOUNT: number;
NGINX_PROTOCOL?: string;
NGINX_HOSTNAME?: string;
NGINX_PORT?: string;
}
const defaultEnv: Env = {
'TESTNET_ENABLED': false,
'LIQUID_ENABLED': false,
'BISQ_ENABLED': false,
'BISQ_SEPARATE_BACKEND': false,
'SPONSORS_ENABLED': false,
'ELECTRS_ITEMS_PER_PAGE': 25,
'KEEP_BLOCKS_AMOUNT': 8,
'NGINX_PROTOCOL': 'http',
'NGINX_HOSTNAME': '127.0.0.1',
'NGINX_PORT': '81',
};
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class StateService { export class StateService {
isBrowser: boolean = isPlatformBrowser(this.platformId);
network = ''; network = '';
env: Env;
latestBlockHeight = 0; latestBlockHeight = 0;
networkChanged$ = new ReplaySubject<string>(1); networkChanged$ = new ReplaySubject<string>(1);
blocks$ = new ReplaySubject<[Block, boolean]>(env.KEEP_BLOCKS_AMOUNT); blocks$: ReplaySubject<[Block, boolean]>;
transactions$ = new ReplaySubject<TransactionStripped>(6); transactions$ = new ReplaySubject<TransactionStripped>(6);
conversions$ = new ReplaySubject<any>(1); conversions$ = new ReplaySubject<any>(1);
bsqPrice$ = new ReplaySubject<number>(1); bsqPrice$ = new ReplaySubject<number>(1);
@ -40,21 +68,35 @@ export class StateService {
viewFiat$ = new BehaviorSubject<boolean>(false); viewFiat$ = new BehaviorSubject<boolean>(false);
connectionState$ = new BehaviorSubject<0 | 1 | 2>(2); connectionState$ = new BehaviorSubject<0 | 1 | 2>(2);
isTabHidden$ = fromEvent(document, 'visibilitychange').pipe(map((event) => this.isHidden()), shareReplay()); isTabHidden$: Observable<boolean>;
markBlock$ = new ReplaySubject<MarkBlockState>(); markBlock$ = new ReplaySubject<MarkBlockState>();
keyNavigation$ = new Subject<KeyboardEvent>(); keyNavigation$ = new Subject<KeyboardEvent>();
constructor( constructor(
@Inject(PLATFORM_ID) private platformId: any,
private router: Router, private router: Router,
) { ) {
this.setNetworkBasedonUrl(window.location.pathname);
this.router.events.subscribe((event) => { this.router.events.subscribe((event) => {
if (event instanceof NavigationStart) { if (event instanceof NavigationStart) {
this.setNetworkBasedonUrl(event.url); this.setNetworkBasedonUrl(event.url);
} }
}); });
if (this.isBrowser) {
this.setNetworkBasedonUrl(window.location.pathname);
this.isTabHidden$ = fromEvent(document, 'visibilitychange').pipe(map(() => this.isHidden()), shareReplay());
} else {
this.setNetworkBasedonUrl('/');
this.isTabHidden$ = new BehaviorSubject(false);
}
const browserWindow = window || {};
// @ts-ignore
const browserWindowEnv = browserWindow.__env || {};
this.env = Object.assign(defaultEnv, browserWindowEnv);
this.blocks$ = new ReplaySubject<[Block, boolean]>(this.env.KEEP_BLOCKS_AMOUNT);
} }
setNetworkBasedonUrl(url: string) { setNetworkBasedonUrl(url: string) {

View File

@ -4,19 +4,23 @@ import { WebsocketResponse } from '../interfaces/websocket.interface';
import { StateService } from './state.service'; import { StateService } from './state.service';
import { Block, Transaction } from '../interfaces/electrs.interface'; import { Block, Transaction } from '../interfaces/electrs.interface';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { env } from '../app.constants'; import { ApiService } from './api.service';
import { take } from 'rxjs/operators';
const WEB_SOCKET_PROTOCOL = (document.location.protocol === 'https:') ? 'wss:' : 'ws:'; import { TransferState, makeStateKey } from '@angular/platform-browser';
const WEB_SOCKET_URL = WEB_SOCKET_PROTOCOL + '//' + document.location.hostname + ':' + document.location.port + '{network}/api/v1/ws';
const OFFLINE_RETRY_AFTER_MS = 10000; const OFFLINE_RETRY_AFTER_MS = 10000;
const OFFLINE_PING_CHECK_AFTER_MS = 30000; const OFFLINE_PING_CHECK_AFTER_MS = 30000;
const EXPECT_PING_RESPONSE_AFTER_MS = 4000; const EXPECT_PING_RESPONSE_AFTER_MS = 4000;
const initData = makeStateKey('/api/v1/init-data');
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class WebsocketService { export class WebsocketService {
private webSocketProtocol = (document.location.protocol === 'https:') ? 'wss:' : 'ws:';
private webSocketUrl = this.webSocketProtocol + '//' + document.location.hostname + ':' + document.location.port + '{network}/api/v1/ws';
private websocketSubject: WebSocketSubject<WebsocketResponse>; private websocketSubject: WebSocketSubject<WebsocketResponse>;
private goneOffline = false; private goneOffline = false;
private lastWant: string[] | null = null; private lastWant: string[] | null = null;
@ -29,135 +33,64 @@ export class WebsocketService {
constructor( constructor(
private stateService: StateService, private stateService: StateService,
private apiService: ApiService,
private transferState: TransferState,
) { ) {
this.network = this.stateService.network === 'bisq' && !env.BISQ_SEPARATE_BACKEND ? '' : this.stateService.network; if (!this.stateService.isBrowser) {
this.websocketSubject = webSocket<WebsocketResponse>(WEB_SOCKET_URL.replace('{network}', this.network ? '/' + this.network : '')); // @ts-ignore
this.startSubscription(); this.websocketSubject = { next: () => {}};
this.stateService.isLoadingWebSocket$.next(false);
this.apiService.getInitData$()
.pipe(take(1))
.subscribe((response) => this.handleResponse(response));
} else {
this.network = this.stateService.network === 'bisq' && !this.stateService.env.BISQ_SEPARATE_BACKEND ? '' : this.stateService.network;
this.websocketSubject = webSocket<WebsocketResponse>(this.webSocketUrl.replace('{network}', this.network ? '/' + this.network : ''));
this.stateService.networkChanged$.subscribe((network) => { const theInitData = this.transferState.get(initData, null);
if (network === 'bisq' && !env.BISQ_SEPARATE_BACKEND) { if (theInitData) {
network = ''; this.handleResponse(theInitData.body);
this.startSubscription(false, true);
} else {
this.startSubscription();
} }
if (network === this.network) {
return;
}
this.network = network;
clearTimeout(this.onlineCheckTimeout);
clearTimeout(this.onlineCheckTimeoutTwo);
this.stateService.latestBlockHeight = 0; this.stateService.networkChanged$.subscribe((network) => {
if (network === 'bisq' && !this.stateService.env.BISQ_SEPARATE_BACKEND) {
network = '';
}
if (network === this.network) {
return;
}
this.network = network;
clearTimeout(this.onlineCheckTimeout);
clearTimeout(this.onlineCheckTimeoutTwo);
this.websocketSubject.complete(); this.stateService.latestBlockHeight = 0;
this.subscription.unsubscribe();
this.websocketSubject = webSocket<WebsocketResponse>(WEB_SOCKET_URL.replace('{network}', this.network ? '/' + this.network : ''));
this.startSubscription(); this.websocketSubject.complete();
}); this.subscription.unsubscribe();
this.websocketSubject = webSocket<WebsocketResponse>(
this.webSocketUrl.replace('{network}', this.network ? '/' + this.network : '')
);
this.startSubscription();
});
}
} }
startSubscription(retrying = false) { startSubscription(retrying = false, hasInitData = false) {
this.stateService.isLoadingWebSocket$.next(true); if (!hasInitData) {
this.stateService.isLoadingWebSocket$.next(true);
this.websocketSubject.next({'action': 'init'});
}
if (retrying) { if (retrying) {
this.stateService.connectionState$.next(1); this.stateService.connectionState$.next(1);
} }
this.websocketSubject.next({'action': 'init'});
this.subscription = this.websocketSubject this.subscription = this.websocketSubject
.subscribe((response: WebsocketResponse) => { .subscribe((response: WebsocketResponse) => {
this.stateService.isLoadingWebSocket$.next(false); this.stateService.isLoadingWebSocket$.next(false);
if (response.blocks && response.blocks.length) { this.handleResponse(response);
const blocks = response.blocks;
blocks.forEach((block: Block) => {
if (block.height > this.stateService.latestBlockHeight) {
this.stateService.latestBlockHeight = block.height;
this.stateService.blocks$.next([block, false]);
}
});
}
if (response.tx) {
this.stateService.mempoolTransactions$.next(response.tx);
}
if (response.block) {
if (response.block.height > this.stateService.latestBlockHeight) {
this.stateService.latestBlockHeight = response.block.height;
this.stateService.blocks$.next([response.block, !!response.txConfirmed]);
}
if (response.txConfirmed) {
this.isTrackingTx = false;
}
}
if (response.conversions) {
this.stateService.conversions$.next(response.conversions);
}
if (response.rbfTransaction) {
this.stateService.txReplaced$.next(response.rbfTransaction);
}
if (response['mempool-blocks']) {
this.stateService.mempoolBlocks$.next(response['mempool-blocks']);
}
if (response.transactions) {
response.transactions.forEach((tx) => this.stateService.transactions$.next(tx));
}
if (response['bsq-price']) {
this.stateService.bsqPrice$.next(response['bsq-price']);
}
if (response['git-commit']) {
this.stateService.gitCommit$.next(response['git-commit']);
if (!this.latestGitCommit) {
this.latestGitCommit = response['git-commit'];
} else {
if (this.latestGitCommit !== response['git-commit']) {
setTimeout(() => {
window.location.reload();
}, Math.floor(Math.random() * 60000) + 60000);
}
}
}
if (response['address-transactions']) {
response['address-transactions'].forEach((addressTransaction: Transaction) => {
this.stateService.mempoolTransactions$.next(addressTransaction);
});
}
if (response['block-transactions']) {
response['block-transactions'].forEach((addressTransaction: Transaction) => {
this.stateService.blockTransactions$.next(addressTransaction);
});
}
if (response['live-2h-chart']) {
this.stateService.live2Chart$.next(response['live-2h-chart']);
}
if (response.mempoolInfo) {
this.stateService.mempoolInfo$.next(response.mempoolInfo);
}
if (response.vBytesPerSecond !== undefined) {
this.stateService.vbytesPerSecond$.next(response.vBytesPerSecond);
}
if (response.lastDifficultyAdjustment !== undefined) {
this.stateService.lastDifficultyAdjustment$.next(response.lastDifficultyAdjustment);
}
if (response['git-commit']) {
this.stateService.gitCommit$.next(response['git-commit']);
}
if (response.donationConfirmed) {
this.stateService.donationConfirmed$.next(true);
}
if (this.goneOffline === true) { if (this.goneOffline === true) {
this.goneOffline = false; this.goneOffline = false;
@ -226,6 +159,9 @@ export class WebsocketService {
} }
want(data: string[], force = false) { want(data: string[], force = false) {
if (!this.stateService.isBrowser) {
return;
}
if (data === this.lastWant && !force) { if (data === this.lastWant && !force) {
return; return;
} }
@ -257,4 +193,101 @@ export class WebsocketService {
}, EXPECT_PING_RESPONSE_AFTER_MS); }, EXPECT_PING_RESPONSE_AFTER_MS);
}, OFFLINE_PING_CHECK_AFTER_MS); }, OFFLINE_PING_CHECK_AFTER_MS);
} }
handleResponse(response: WebsocketResponse) {
if (response.blocks && response.blocks.length) {
const blocks = response.blocks;
blocks.forEach((block: Block) => {
if (block.height > this.stateService.latestBlockHeight) {
this.stateService.latestBlockHeight = block.height;
this.stateService.blocks$.next([block, false]);
}
});
}
if (response.tx) {
this.stateService.mempoolTransactions$.next(response.tx);
}
if (response.block) {
if (response.block.height > this.stateService.latestBlockHeight) {
this.stateService.latestBlockHeight = response.block.height;
this.stateService.blocks$.next([response.block, !!response.txConfirmed]);
}
if (response.txConfirmed) {
this.isTrackingTx = false;
}
}
if (response.conversions) {
this.stateService.conversions$.next(response.conversions);
}
if (response.rbfTransaction) {
this.stateService.txReplaced$.next(response.rbfTransaction);
}
if (response['mempool-blocks']) {
this.stateService.mempoolBlocks$.next(response['mempool-blocks']);
}
if (response.transactions) {
response.transactions.forEach((tx) => this.stateService.transactions$.next(tx));
}
if (response['bsq-price']) {
this.stateService.bsqPrice$.next(response['bsq-price']);
}
if (response['git-commit']) {
this.stateService.gitCommit$.next(response['git-commit']);
if (!this.latestGitCommit) {
this.latestGitCommit = response['git-commit'];
} else {
if (this.latestGitCommit !== response['git-commit']) {
setTimeout(() => {
window.location.reload();
}, Math.floor(Math.random() * 60000) + 60000);
}
}
}
if (response['address-transactions']) {
response['address-transactions'].forEach((addressTransaction: Transaction) => {
this.stateService.mempoolTransactions$.next(addressTransaction);
});
}
if (response['block-transactions']) {
response['block-transactions'].forEach((addressTransaction: Transaction) => {
this.stateService.blockTransactions$.next(addressTransaction);
});
}
if (response['live-2h-chart']) {
this.stateService.live2Chart$.next(response['live-2h-chart']);
}
if (response.mempoolInfo) {
this.stateService.mempoolInfo$.next(response.mempoolInfo);
}
if (response.vBytesPerSecond !== undefined) {
this.stateService.vbytesPerSecond$.next(response.vBytesPerSecond);
}
if (response.lastDifficultyAdjustment !== undefined) {
this.stateService.lastDifficultyAdjustment$.next(response.lastDifficultyAdjustment);
}
if (response['git-commit']) {
this.stateService.gitCommit$.next(response['git-commit']);
}
if (response.donationConfirmed) {
this.stateService.donationConfirmed$.next(true);
}
}
} }

View File

@ -32,10 +32,6 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
</head> </head>
<body> <body>
<noscript>
<div style="font-size: 36px">&#128680; mempool requires javascript&#128680;</div>
<div style="font-size: 20px">&nbsp; You need to enable JS in your browser to use this website.</div>
</noscript>
<app-root></app-root> <app-root></app-root>
<script type="text/javascript"> <script type="text/javascript">
if (document.location.hostname === "mempool.space") if (document.location.hostname === "mempool.space")

View File

@ -0,0 +1,16 @@
/***************************************************************************************************
* Load `$localize` onto the global scope - used if i18n tags appear in Angular templates.
*/
import '@angular/localize/init';
import { enableProdMode } from '@angular/core';
import { environment } from './environments/environment';
if (environment.production) {
enableProdMode();
}
export { AppServerModule } from './app/app.server.module';
export { renderModule, renderModuleFactory } from '@angular/platform-server';

View File

@ -8,5 +8,7 @@ if (environment.production) {
enableProdMode(); enableProdMode();
} }
platformBrowserDynamic().bootstrapModule(AppModule) document.addEventListener('DOMContentLoaded', () => {
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.error(err)); .catch(err => console.error(err));
});

View File

@ -12,6 +12,9 @@
}, },
{ {
"path": "./tsconfig.spec.json" "path": "./tsconfig.spec.json"
},
{
"path": "./tsconfig.server.json"
} }
] ]
} }

View File

@ -0,0 +1,18 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "./tsconfig.app.json",
"compilerOptions": {
"outDir": "./out-tsc/server",
"target": "es2015",
"types": [
"node"
]
},
"files": [
"src/main.server.ts",
"server.ts"
],
"angularCompilerOptions": {
"entryModule": "./src/app/app.server.module#AppServerModule"
}
}

View File

@ -18,6 +18,6 @@ do
cd "${HOME}/${site}/frontend/" cd "${HOME}/${site}/frontend/"
npm install npm install
npm run build npm run build
rsync -av ./dist/mempool/* "${HOME}/public_html/${site}/" rsync -av ./dist/mempool/browser/* "${HOME}/public_html/${site}/"
fi fi
done done

View File

@ -36,7 +36,7 @@ do
cd "$HOME/${site}/frontend" cd "$HOME/${site}/frontend"
npm install npm install
npm run build npm run build
rsync -av ./dist/mempool/* "${HOME}/public_html/${site}/" rsync -av ./dist/mempool/browser/* "${HOME}/public_html/${site}/"
fi fi
cd "$HOME/${site}/backend" cd "$HOME/${site}/backend"

View File

@ -0,0 +1,212 @@
root /mempool/public_html/mainnet/;
index index.html;
add_header Onion-Location http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion$request_uri;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload";
set $frameOptions "DENY";
set $contentSecurityPolicy "frame-ancestors 'none'";
if ($http_referer ~ ^https://mempool.space/)
{
set $frameOptions "ALLOW-FROM https://mempool.space";
set $contentSecurityPolicy "frame-ancestors https://mempool.space";
}
if ($http_referer ~ ^https://mempool.ninja/)
{
set $frameOptions "ALLOW-FROM https://mempool.ninja";
set $contentSecurityPolicy "frame-ancestors https://mempool.ninja";
}
if ($http_referer ~ ^https://node100.bitcoin.wiz.biz/)
{
set $frameOptions "ALLOW-FROM https://node100.bitcoin.wiz.biz";
set $contentSecurityPolicy "frame-ancestors https://node100.bitcoin.wiz.biz";
}
if ($http_referer ~ ^https://wiz.biz/)
{
set $frameOptions "ALLOW-FROM https://wiz.biz";
set $contentSecurityPolicy "frame-ancestors https://wiz.biz";
}
add_header X-Frame-Options $frameOptions;
add_header Content-Security-Policy $contentSecurityPolicy;
# fallback for all URLs i.e. /address/foo /tx/foo /block/000
location / {
#try_files /$lang/$uri /$lang/$uri/ $uri $uri/ /en-US/$uri @index-redirect;
proxy_pass http://127.0.0.1:4000;
}
location @index-redirect {
add_header vary accept-language;
rewrite (.*) /$lang/index.html;
}
# location block using regex are matched in order
# used to rewrite resources from /<lang>/ to /en-US/
location ~ ^/(ar|bg|bs|ca|cs|da|de|et|el|es|eo|eu|fa|fr|gl|ko|hr|id|it|he|ka|lv|lt|hu|mk|ms|nl|ja|ka|no|nb|nn|pl|pt|pt-BR|ro|ru|sk|sl|sr|sh|fi|sv|th|tr|uk|vi|zh)/resources/ {
#rewrite ^/[a-zA-Z-]*/resources/(.*) /resources/$1;
try_files $uri /en-US/$uri =404;
}
location /resources/ {
try_files $uri /en-US/$uri =404;
}
# used for cookie override
location ~ ^/(ar|bg|bs|ca|cs|da|de|et|el|es|eo|eu|fa|fr|gl|ko|hr|id|it|he|ka|lv|lt|hu|mk|ms|nl|ja|ka|no|nb|nn|pl|pt|pt-BR|ro|ru|sk|sl|sr|sh|fi|sv|th|tr|uk|vi|zh)/ {
#try_files $uri $uri/ /$1/index.html =404;
proxy_pass http://127.0.0.1:4000;
}
# add /sitemap for production SEO
location /sitemap {
try_files $uri =410;
}
# old /explorer redirect from v1 days
location /explorer {
rewrite /explorer/(.*) https://$host/$1 permanent;
}
# static API docs
location = /api {
#return 302 https://mempool.space/$request_uri;
#try_files $uri $uri/ /en-US/index.html =404;
proxy_pass http://127.0.0.1:4000;
}
location = /api/ {
#return 302 https://mempool.space/$request_uri;
#try_files $uri $uri/ /en-US/index.html =404;
proxy_pass http://127.0.0.1:4000;
}
location = /liquid/api {
#return 302 https://mempool.space/$request_uri;
#try_files $uri $uri/ /en-US/index.html =404;
proxy_pass http://127.0.0.1:4000;
}
location = /liquid/api/ {
#return 302 https://mempool.space/$request_uri;
#try_files $uri $uri/ /en-US/index.html =404;
proxy_pass http://127.0.0.1:4000;
}
location = /testnet/api {
#return 302 https://mempool.space/$request_uri;
#try_files $uri $uri/ /en-US/index.html =404;
proxy_pass http://127.0.0.1:4000;
}
location = /testnet/api/ {
#return 302 https://mempool.space/$request_uri;
#try_files $uri $uri/ /en-US/index.html =404;
proxy_pass http://127.0.0.1:4000;
}
location = /bisq/api {
#return 302 https://mempool.space/$request_uri;
#try_files $uri $uri/ /en-US/index.html =404;
proxy_pass http://127.0.0.1:4000;
}
location = /bisq/api/ {
#return 302 https://mempool.space/$request_uri;
#try_files $uri $uri/ /en-US/index.html =404;
proxy_pass http://127.0.0.1:4000;
}
# mainnet API
location /api/v1/donations {
proxy_pass http://127.0.0.1:8999;
# don't rate limit this API prefix
}
location /api/v1/donations/images {
proxy_pass http://127.0.0.1:8999;
proxy_cache cache;
proxy_cache_valid 200 1d;
}
location /api/v1/ws {
proxy_pass http://127.0.0.1:8999/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
}
location /api/v1 {
proxy_pass http://127.0.0.1:8999/api/v1;
limit_req burst=50 nodelay zone=api;
}
location /api/ {
proxy_pass http://[::1]:3000/;
limit_req burst=50 nodelay zone=electrs;
}
# liquid API
location /liquid/api/v1/ws {
proxy_pass http://127.0.0.1:8998/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
}
location /liquid/api/v1 {
proxy_pass http://127.0.0.1:8998/api/v1;
limit_req burst=50 nodelay zone=api;
}
location /liquid/api/ {
proxy_pass http://[::1]:3001/;
limit_req burst=50 nodelay zone=electrs;
}
# testnet API
location /testnet/api/v1/ws {
proxy_pass http://127.0.0.1:8997/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
}
location /testnet/api/v1 {
proxy_pass http://127.0.0.1:8997/api/v1;
limit_req burst=50 nodelay zone=api;
}
location /testnet/api/ {
proxy_pass http://[::1]:3002/;
limit_req burst=50 nodelay zone=electrs;
}
# bisq API
location /bisq/api/v1/ws {
proxy_pass http://127.0.0.1:8996/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
}
location /bisq/api/v1/markets {
proxy_pass http://127.0.0.1:8996/api/v1/bisq/markets;
#limit_req burst=50 nodelay zone=api;
}
location /bisq/api/v1 {
proxy_pass http://127.0.0.1:8996/api/v1;
limit_req burst=50 nodelay zone=api;
}
location /bisq/api {
proxy_pass http://127.0.0.1:8996/api/v1/bisq;
limit_req burst=50 nodelay zone=api;
}
# mainnet API
location /ws {
proxy_pass http://127.0.0.1:8999/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
}
location /ws/mainnet {
proxy_pass http://127.0.0.1:8999/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
}
location /ws/liquid {
proxy_pass http://127.0.0.1:8998/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
}
location /ws/testnet {
proxy_pass http://127.0.0.1:8997/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
}