From c5b9dc020e68e88aeb586a3a20e2f4a9258f06cd Mon Sep 17 00:00:00 2001 From: mononaut Date: Mon, 25 May 2026 16:37:35 +0000 Subject: [PATCH] use paginated esplora registry api for liquid assets list --- .../components/assets/assets.component.html | 2 +- .../app/components/assets/assets.component.ts | 55 +++++------------ .../src/app/interfaces/electrs.interface.ts | 8 +++ frontend/src/app/services/assets.service.ts | 60 ++++++++++++++++--- .../src/app/services/electrs-api.service.ts | 14 ++++- 5 files changed, 86 insertions(+), 53 deletions(-) diff --git a/frontend/src/app/components/assets/assets.component.html b/frontend/src/app/components/assets/assets.component.html index 30c6b7255..4685d7b49 100644 --- a/frontend/src/app/components/assets/assets.component.html +++ b/frontend/src/app/components/assets/assets.component.html @@ -18,7 +18,7 @@
- +

diff --git a/frontend/src/app/components/assets/assets.component.ts b/frontend/src/app/components/assets/assets.component.ts index f0f081f58..c34463ed7 100644 --- a/frontend/src/app/components/assets/assets.component.ts +++ b/frontend/src/app/components/assets/assets.component.ts @@ -1,13 +1,11 @@ import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core'; import { AssetsService } from '@app/services/assets.service'; -import { environment } from '@environments/environment'; import { UntypedFormGroup } from '@angular/forms'; -import { filter, map, switchMap, take } from 'rxjs/operators'; +import { map, switchMap } from 'rxjs/operators'; import { ActivatedRoute, Router } from '@angular/router'; -import { combineLatest, Observable } from 'rxjs'; -import { AssetExtended } from '@interfaces/electrs.interface'; +import { Observable } from 'rxjs'; import { SeoService } from '@app/services/seo.service'; -import { StateService } from '@app/services/state.service'; +import { AssetRegistryItem } from '@interfaces/electrs.interface'; @Component({ selector: 'app-assets', @@ -17,16 +15,14 @@ import { StateService } from '@app/services/state.service'; standalone: false, }) export class AssetsComponent implements OnInit { - nativeAssetId = this.stateService.network === 'liquidtestnet' ? environment.nativeTestAssetId : environment.nativeAssetId; paginationMaxSize = window.matchMedia('(max-width: 670px)').matches ? 4 : 6; ellipses = window.matchMedia('(max-width: 670px)').matches ? false : true; - assets: AssetExtended[]; - assetsCache: AssetExtended[]; searchForm: UntypedFormGroup; - assets$: Observable; + assets$: Observable; page = 1; + totalAssets = 0; error: any; itemsPerPage: number; @@ -38,46 +34,23 @@ export class AssetsComponent implements OnInit { private route: ActivatedRoute, private router: Router, private seoService: SeoService, - private stateService: StateService, ) { } ngOnInit() { this.seoService.setTitle($localize`:@@ee8f8008bae6ce3a49840c4e1d39b4af23d4c263:Assets`); this.itemsPerPage = Math.max(Math.round(this.contentSpace / this.fiveItemsPxSize) * 5, 10); - this.assets$ = combineLatest([ - this.assetsService.getAssetsJson$, - this.route.queryParams, - ]) + this.assets$ = this.route.queryParams .pipe( - take(1), - switchMap(([assets, qp]) => { - this.assets = assets.array; - - return this.route.queryParams - .pipe( - filter((queryParams) => { - const newPage = parseInt(queryParams.page, 10); - if (newPage !== this.page) { - return true; - } - return false; - }), - map((queryParams) => { - if (queryParams.page) { - const newPage = parseInt(queryParams.page, 10); - this.page = newPage; - } else { - this.page = 1; - } - return ''; - }) - ); - }), - map(() => { + switchMap((queryParams) => { + this.page = queryParams.page ? parseInt(queryParams.page, 10) : 1; const start = (this.page - 1) * this.itemsPerPage; - return this.assets.slice(start, this.itemsPerPage + start); - }) + return this.assetsService.getLiquidAssetsPage$(start, this.itemsPerPage); + }), + map((result) => { + this.totalAssets = result.total; + return result.assets; + }), ); } diff --git a/frontend/src/app/interfaces/electrs.interface.ts b/frontend/src/app/interfaces/electrs.interface.ts index ee4ef08af..a8e6d4483 100644 --- a/frontend/src/app/interfaces/electrs.interface.ts +++ b/frontend/src/app/interfaces/electrs.interface.ts @@ -225,6 +225,14 @@ export interface Entity { domain: string; } +export interface AssetRegistryItem { + asset_id: string; + name: string; + ticker?: string; + domain?: string; + entity?: Entity; +} + interface IssuanceTxin { txid: string; vin: number; diff --git a/frontend/src/app/services/assets.service.ts b/frontend/src/app/services/assets.service.ts index 3320a52f6..6d51c7f53 100644 --- a/frontend/src/app/services/assets.service.ts +++ b/frontend/src/app/services/assets.service.ts @@ -1,10 +1,11 @@ import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; -import { Observable, of } from 'rxjs'; -import { map, shareReplay, switchMap } from 'rxjs/operators'; +import { Observable, of, throwError } from 'rxjs'; +import { catchError, map, shareReplay, switchMap } from 'rxjs/operators'; import { StateService } from '@app/services/state.service'; import { environment } from '@environments/environment'; -import { Asset, AssetExtended } from '@interfaces/electrs.interface'; +import { ElectrsApiService } from '@app/services/electrs-api.service'; +import { Asset, AssetExtended, AssetRegistryItem } from '@interfaces/electrs.interface'; @Injectable({ providedIn: 'root' @@ -15,19 +16,25 @@ export class AssetsService { getAssetsJson$: Observable<{ array: AssetExtended[]; objects: any}>; getAssetsMinimalJson$: Observable; getWorldMapJson$: Observable; + registryAvailable = true; + private apiBaseUrl = ''; constructor( private httpClient: HttpClient, private stateService: StateService, + private electrsApiService: ElectrsApiService, ) { - let apiBaseUrl = ''; - if (!this.stateService.isBrowser) { - apiBaseUrl = this.stateService.env.NGINX_PROTOCOL + '://' + this.stateService.env.NGINX_HOSTNAME + ':' + this.stateService.env.NGINX_PORT; + 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.stateService.networkChanged$.subscribe(() => { + this.registryAvailable = true; + }); this.getAssetsJson$ = this.stateService.networkChanged$ .pipe( - switchMap(() => this.httpClient.get(`${apiBaseUrl}/resources/assets${this.stateService.network === 'liquidtestnet' ? '-testnet' : ''}.json`)), + switchMap(() => this.httpClient.get(`${this.apiBaseUrl}/resources/assets${this.stateService.network === 'liquidtestnet' ? '-testnet' : ''}.json`)), map((rawAssets) => { const assets: AssetExtended[] = Object.values(rawAssets); @@ -56,7 +63,7 @@ export class AssetsService { ); this.getAssetsMinimalJson$ = this.stateService.networkChanged$ .pipe( - switchMap(() => this.httpClient.get(`${apiBaseUrl}/resources/assets${this.stateService.network === 'liquidtestnet' ? '-testnet' : ''}.minimal.json`)), + switchMap(() => this.httpClient.get(`${this.apiBaseUrl}/resources/assets${this.stateService.network === 'liquidtestnet' ? '-testnet' : ''}.minimal.json`)), map((assetsMinimal) => { if (this.stateService.network === 'liquidtestnet') { // Hard coding the Liquid Testnet native asset @@ -67,7 +74,7 @@ export class AssetsService { shareReplay(1), ); - this.getWorldMapJson$ = this.httpClient.get(apiBaseUrl + '/resources/worldmap.json').pipe(shareReplay()); + this.getWorldMapJson$ = this.httpClient.get(this.apiBaseUrl + '/resources/worldmap.json').pipe(shareReplay()); } public enrichLiquidAsset$(asset: Asset): Observable { @@ -83,4 +90,39 @@ export class AssetsService { ); } } + + public getLiquidAssetsPage$(startIndex: number, limit: number): Observable<{ assets: AssetRegistryItem[]; total: number }> { + return (this.registryAvailable ? this.electrsApiService.getLiquidAssetsRegistry$(startIndex, limit).pipe( + map((response) => { + const assets = response.body || []; + const total = parseInt(response.headers.get('X-Total-Results') || `${assets.length}`, 10); + if (!total && !assets.length) { + this.registryAvailable = false; + return null; + } + return { assets, total }; + }), + catchError((error) => { + if (![404, 501].includes(error.status)) { + return throwError(() => error); + } + this.registryAvailable = false; + return of(null); + }), + ) : of(null)).pipe( + switchMap((registryPage) => registryPage ? of(registryPage) : this.getAssetsJson$.pipe( + map((assets) => ({ + assets: assets.array.slice(startIndex, startIndex + limit), + total: assets.array.length, + })), + )), + map((page) => ({ + ...page, + assets: page.assets.map((asset) => ({ + ...asset, + entity: asset.entity || (asset.domain ? { domain: asset.domain } : undefined), + })), + })), + ); + } } diff --git a/frontend/src/app/services/electrs-api.service.ts b/frontend/src/app/services/electrs-api.service.ts index 831c9f167..687156f44 100644 --- a/frontend/src/app/services/electrs-api.service.ts +++ b/frontend/src/app/services/electrs-api.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core'; -import { HttpClient, HttpParams } from '@angular/common/http'; +import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http'; import { BehaviorSubject, Observable, catchError, filter, from, of, shareReplay, switchMap, take, tap } from 'rxjs'; -import { Transaction, Address, Outspend, Recent, Asset, ScriptHash, AddressTxSummary, Utxo } from '@interfaces/electrs.interface'; +import { Transaction, Address, Outspend, Recent, Asset, ScriptHash, AddressTxSummary, Utxo, AssetRegistryItem } from '@interfaces/electrs.interface'; import { StateService } from '@app/services/state.service'; import { BlockExtended } from '@interfaces/node-api.interface'; import { calcScriptHash$ } from '@app/bitcoin.utils'; @@ -228,6 +228,16 @@ export class ElectrsApiService { return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/asset/' + assetId); } + getLiquidAssetsRegistry$(startIndex: number, limit: number): Observable> { + const params = new HttpParams() + .set('start_index', startIndex) + .set('limit', limit) + .set('sort_field', 'name') + .set('sort_dir', 'asc'); + + return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/assets/registry', { params, observe: 'response' }); + } + getAssetTransactions$(assetId: string): Observable { return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/asset/' + assetId + '/txs'); }