import { Injectable } from '@angular/core'; import { HttpClient, HttpParams } from '@angular/common/http'; import { BehaviorSubject, Observable, catchError, filter, of, shareReplay, take, tap } from 'rxjs'; import { StateService } from '../services/state.service'; import { IChannel, INodesRanking, IOldestNodes, ITopNodesPerCapacity, ITopNodesPerChannels } from '../interfaces/node-api.interface'; @Injectable({ providedIn: 'root' }) export class LightningApiService { private apiBaseUrl: string; // base URL is protocol, hostname, and port private apiBasePath = ''; // network path is /testnet, etc. or '' for mainnet private requestCache = new Map, expiry: number }>; constructor( private httpClient: HttpClient, private stateService: StateService, ) { 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.apiBasePath = network ? '/' + network : ''; }); } private generateCacheKey(functionName: string, params: any[]): string { return functionName + JSON.stringify(params); } // delete expired cache entries private cleanExpiredCache(): void { this.requestCache.forEach((value, key) => { if (value.expiry < Date.now()) { this.requestCache.delete(key); } }); } cachedRequest Observable>( apiFunction: F, expireAfter: number, // in ms ...params: Parameters ): Observable { this.cleanExpiredCache(); const cacheKey = this.generateCacheKey(apiFunction.name, params); if (!this.requestCache.has(cacheKey)) { const subject = new BehaviorSubject(null); this.requestCache.set(cacheKey, { subject, expiry: Date.now() + expireAfter }); apiFunction.bind(this)(...params).pipe( tap(data => { subject.next(data as T); }), catchError((error) => { subject.error(error); return of(null); }), shareReplay(1), ).subscribe(); } return this.requestCache.get(cacheKey).subject.asObservable().pipe(filter(val => val !== null), take(1)); } getNode$(publicKey: string): Observable { return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/nodes/' + publicKey); } getNodeGroup$(name: string): Observable { return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/nodes/group/' + name); } getChannel$(shortId: string): Observable { return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/channels/' + shortId); } getChannelsByNodeId$(publicKey: string, index: number = 0, status = 'open'): Observable { const params = new HttpParams() .set('public_key', publicKey) .set('index', index) .set('status', status) ; return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/channels', { params, observe: 'response' }); } getLatestStatistics$(): Observable { return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/statistics/latest'); } listNodeStats$(publicKey: string): Observable { return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/nodes/' + publicKey + '/statistics'); } getNodeFeeHistogram$(publicKey: string): Observable { return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/nodes/' + publicKey + '/fees/histogram'); } getNodesRanking$(): Observable { return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/nodes/rankings'); } listChannelStats$(publicKey: string): Observable { return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/channels/' + publicKey + '/statistics'); } listStatistics$(interval: string | undefined): Observable { return this.httpClient.get( this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/statistics' + (interval !== undefined ? `/${interval}` : ''), { observe: 'response' } ); } getTopNodesByCapacity$(): Observable { return this.httpClient.get( this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/nodes/rankings/liquidity' ); } getTopNodesByChannels$(): Observable { return this.httpClient.get( this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/nodes/rankings/connectivity' ); } getPenaltyClosedChannels$(): Observable { return this.httpClient.get( this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/penalties' ); } getOldestNodes$(): Observable { return this.httpClient.get( this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/nodes/rankings/age' ); } }