import { Injectable } from '@angular/core'; import { HttpClient, HttpParams } from '@angular/common/http'; import { BehaviorSubject, Observable, catchError, filter, from, of, shareReplay, switchMap, take, tap } from 'rxjs'; import { Transaction, Address, Outspend, Recent, Asset, ScriptHash } from '../interfaces/electrs.interface'; import { StateService } from './state.service'; import { BlockExtended } from '../interfaces/node-api.interface'; import { calcScriptHash$ } from '../bitcoin.utils'; @Injectable({ providedIn: 'root' }) export class ElectrsApiService { private apiBaseUrl: string; // base URL is protocol, hostname, and port private apiBasePath: string; // 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) => { if (network === 'bisq') { 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)); } getBlock$(hash: string): Observable { return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/block/' + hash); } listBlocks$(height?: number): Observable { return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/blocks/' + (height || '')); } getTransaction$(txId: string): Observable { return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/tx/' + txId); } getRecentTransaction$(): Observable { return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/mempool/recent'); } getOutspend$(hash: string, vout: number): Observable { return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/tx/' + hash + '/outspend/' + vout); } getOutspends$(hash: string): Observable { return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/tx/' + hash + '/outspends'); } getOutspendsBatched$(txids: string[]): Observable { let params = new HttpParams(); params = params.append('txids', txids.join(',')); return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/txs/outspends', { params }); } getBlockTransactions$(hash: string, index: number = 0): Observable { return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/block/' + hash + '/txs/' + index); } getBlockHashFromHeight$(height: number): Observable { return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/block-height/' + height, {responseType: 'text'}); } getAddress$(address: string): Observable
{ return this.httpClient.get
(this.apiBaseUrl + this.apiBasePath + '/api/address/' + address); } getPubKeyAddress$(pubkey: string): Observable
{ const scriptpubkey = (pubkey.length === 130 ? '41' : '21') + pubkey + 'ac'; return this.getScriptHash$(scriptpubkey).pipe( switchMap((scripthash: ScriptHash) => { return of({ ...scripthash, address: pubkey, is_pubkey: true, }); }) ); } getScriptHash$(script: string): Observable { return from(calcScriptHash$(script)).pipe( switchMap(scriptHash => this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/scripthash/' + scriptHash)) ); } getAddressTransactions$(address: string, txid?: string): Observable { let params = new HttpParams(); if (txid) { params = params.append('after_txid', txid); } return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/address/' + address + '/txs', { params }); } getScriptHashTransactions$(script: string, txid?: string): Observable { let params = new HttpParams(); if (txid) { params = params.append('after_txid', txid); } return from(calcScriptHash$(script)).pipe( switchMap(scriptHash => this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/scripthash/' + scriptHash + '/txs', { params })), ); } getAsset$(assetId: string): Observable { return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/asset/' + assetId); } getAssetTransactions$(assetId: string): Observable { return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/asset/' + assetId + '/txs'); } getAssetTransactionsFromHash$(assetId: string, txid: string): Observable { return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/asset/' + assetId + '/txs/chain/' + txid); } getAddressesByPrefix$(prefix: string): Observable { if (prefix.toLowerCase().indexOf('bc1') === 0) { prefix = prefix.toLowerCase(); } return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/address-prefix/' + prefix); } }