use paginated esplora registry api for liquid assets list

This commit is contained in:
mononaut 2026-05-25 16:37:35 +00:00
parent 73b64cc5fb
commit c5b9dc020e
No known key found for this signature in database
GPG key ID: BFD16BE592A9CD8D
5 changed files with 86 additions and 53 deletions

View file

@ -18,7 +18,7 @@
<br>
<ngb-pagination class="pagination-container" [collectionSize]="assets.length" [rotate]="true" [pageSize]="itemsPerPage" [(page)]="page" (pageChange)="pageChange(page)" [maxSize]="paginationMaxSize" [boundaryLinks]="true" [ellipses]="ellipses"></ngb-pagination>
<ngb-pagination class="pagination-container" [collectionSize]="totalAssets" [rotate]="true" [pageSize]="itemsPerPage" [(page)]="page" (pageChange)="pageChange(page)" [maxSize]="paginationMaxSize" [boundaryLinks]="true" [ellipses]="ellipses"></ngb-pagination>
<div class="clearfix"></div>
<br>

View file

@ -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<AssetExtended[]>;
assets$: Observable<AssetRegistryItem[]>;
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;
}),
);
}

View file

@ -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;

View file

@ -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<any>;
getWorldMapJson$: Observable<any>;
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<Asset> {
@ -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),
})),
})),
);
}
}

View file

@ -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<Asset>(this.apiBaseUrl + this.apiBasePath + '/api/asset/' + assetId);
}
getLiquidAssetsRegistry$(startIndex: number, limit: number): Observable<HttpResponse<AssetRegistryItem[]>> {
const params = new HttpParams()
.set('start_index', startIndex)
.set('limit', limit)
.set('sort_field', 'name')
.set('sort_dir', 'asc');
return this.httpClient.get<AssetRegistryItem[]>(this.apiBaseUrl + this.apiBasePath + '/api/assets/registry', { params, observe: 'response' });
}
getAssetTransactions$(assetId: string): Observable<Transaction[]> {
return this.httpClient.get<Transaction[]>(this.apiBaseUrl + this.apiBasePath + '/api/asset/' + assetId + '/txs');
}