diff --git a/frontend/src/app/components/asset/asset.component.ts b/frontend/src/app/components/asset/asset.component.ts index 5afcdbd1d..70793266b 100644 --- a/frontend/src/app/components/asset/asset.component.ts +++ b/frontend/src/app/components/asset/asset.component.ts @@ -7,7 +7,7 @@ import { WebsocketService } from '@app/services/websocket.service'; import { StateService } from '@app/services/state.service'; import { AudioService } from '@app/services/audio.service'; import { ApiService } from '@app/services/api.service'; -import { of, merge, Subscription, combineLatest } from 'rxjs'; +import { of, merge, Subscription, EMPTY } from 'rxjs'; import { SeoService } from '@app/services/seo.service'; import { environment } from '@environments/environment'; import { AssetsService } from '@app/services/assets.service'; @@ -82,30 +82,26 @@ export class AssetComponent implements OnInit, OnDestroy { ) .pipe( switchMap(() => { - return combineLatest([this.electrsApiService.getAsset$(this.assetString) + return this.electrsApiService.getAsset$(this.assetString) .pipe( catchError((err) => { this.isLoadingAsset = false; this.error = err; this.seoService.logSoft404(); console.log(err); - return of(null); - }) - ), this.assetsService.getAssetsMinimalJson$]) - .pipe( - take(1) - ); + return EMPTY; + }), + switchMap((asset) => this.assetsService.enrichLiquidAsset$(asset)), + take(1) + ); }) ); }) ) .pipe( - switchMap(([asset, assetsData]) => { + switchMap((asset) => { this.asset = asset; - this.assetContract = assetsData[this.asset.asset_id]; - if (!this.assetContract) { - this.assetContract = [null, '?', 'Unknown', 0]; - } + this.assetContract = [asset.entity?.domain || null, asset.ticker || '?', asset.name || 'Unknown', asset.precision || 0]; this.seoService.setDescription($localize`:@@meta.description.liquid.asset:Browse an overview of the Liquid asset ${this.assetContract[2]}:INTERPOLATION: (${this.assetContract[1]}:INTERPOLATION:): see issued amount, burned amount, circulating amount, related transactions, and more.`); this.blindedIssuance = this.asset.chain_stats.has_blinded_issuances || this.asset.mempool_stats.has_blinded_issuances; this.isNativeAsset = asset.asset_id === this.nativeAssetId; diff --git a/frontend/src/app/components/assets/asset-group/asset-group.component.ts b/frontend/src/app/components/assets/asset-group/asset-group.component.ts index 4c3d45639..cb1932908 100644 --- a/frontend/src/app/components/assets/asset-group/asset-group.component.ts +++ b/frontend/src/app/components/assets/asset-group/asset-group.component.ts @@ -1,6 +1,6 @@ import { Component, OnInit } from '@angular/core'; import { ActivatedRoute, ParamMap } from '@angular/router'; -import { combineLatest, Observable } from 'rxjs'; +import { from, Observable } from 'rxjs'; import { map, switchMap } from 'rxjs/operators'; import { ApiService } from '@app/services/api.service'; import { AssetsService } from '@app/services/assets.service'; @@ -24,22 +24,17 @@ export class AssetGroupComponent implements OnInit { this.group$ = this.route.paramMap .pipe( switchMap((params: ParamMap) => { - return combineLatest([ - this.assetsService.getAssetsJson$, - this.apiService.getAssetGroup$(params.get('id')), - ]); + return this.apiService.getAssetGroup$(params.get('id')); + }), + switchMap((group) => { + return from(Promise.all(group.assets.map((assetId) => this.assetsService.getLiquidAssetData(assetId).catch(() => ({ asset_id: assetId }))))) + .pipe( + map((assets) => ({ + group: group, + assets: assets, + })) + ); }), - map(([assets, group]) => { - const items = []; - // @ts-ignore - for (const item of group.assets) { - items.push(assets.objects[item]); - } - return { - group: group, - assets: items - }; - }) ); } } diff --git a/frontend/src/app/components/assets/assets-nav/assets-nav.component.ts b/frontend/src/app/components/assets/assets-nav/assets-nav.component.ts index bc5c40f3f..c31e6cbaa 100644 --- a/frontend/src/app/components/assets/assets-nav/assets-nav.component.ts +++ b/frontend/src/app/components/assets/assets-nav/assets-nav.component.ts @@ -3,13 +3,13 @@ import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms import { Router } from '@angular/router'; import { NgbTypeahead } from '@ng-bootstrap/ng-bootstrap'; import { merge, Observable, of, Subject } from 'rxjs'; -import { distinctUntilChanged, filter, map, switchMap } from 'rxjs/operators'; -import { AssetExtended } from '@interfaces/electrs.interface'; +import { debounceTime, distinctUntilChanged, filter, switchMap } from 'rxjs/operators'; import { AssetsService } from '@app/services/assets.service'; import { SeoService } from '@app/services/seo.service'; import { StateService } from '@app/services/state.service'; import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe'; import { environment } from '@environments/environment'; +import { AssetRegistryItem } from '@interfaces/electrs.interface'; @Component({ selector: 'app-assets-nav', @@ -21,10 +21,10 @@ export class AssetsNavComponent implements OnInit { @ViewChild('instance', {static: true}) instance: NgbTypeahead; nativeAssetId = this.stateService.network === 'liquidtestnet' ? environment.nativeTestAssetId : environment.nativeAssetId; searchForm: UntypedFormGroup; - assetsCache: AssetExtended[]; + assetsCache: AssetRegistryItem[]; typeaheadSearchFn: ((text: Observable) => Observable); - formatterFn = (asset: AssetExtended) => asset.name + ' (' + asset.ticker + ')'; + formatterFn = (asset: AssetRegistryItem) => asset.name + ' (' + asset.ticker + ')'; focus$ = new Subject(); click$ = new Subject(); @@ -51,6 +51,7 @@ export class AssetsNavComponent implements OnInit { typeaheadSearch = (text$: Observable) => { const debouncedText$ = text$.pipe( + debounceTime(200), distinctUntilChanged() ); const clicksWithClosedPopup$ = this.click$.pipe(filter(() => !this.instance.isPopupOpen())); @@ -62,18 +63,7 @@ export class AssetsNavComponent implements OnInit { if (!searchText.length) { return of([]); } - return this.assetsService.getAssetsJson$.pipe( - map((assets) => { - if (searchText.length ) { - const filteredAssets = assets.array.filter((asset) => asset.name.toLowerCase().indexOf(searchText.toLowerCase()) > -1 - || (asset.ticker || '').toLowerCase().indexOf(searchText.toLowerCase()) > -1 - || (asset.entity && asset.entity.domain || '').toLowerCase().indexOf(searchText.toLowerCase()) > -1); - return filteredAssets.slice(0, this.itemsPerPage); - } else { - return assets.array.slice(0, this.itemsPerPage); - } - }) - ); + return this.assetsService.searchLiquidAssets$(searchText, this.itemsPerPage); }), ); }; 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/components/search-form/search-form.component.ts b/frontend/src/app/components/search-form/search-form.component.ts index 5f2896b66..739d513c2 100644 --- a/frontend/src/app/components/search-form/search-form.component.ts +++ b/frontend/src/app/components/search-form/search-form.component.ts @@ -22,7 +22,6 @@ export class SearchFormComponent implements OnInit { @Input() hamburgerOpen = false; env: Env; network = ''; - assets: object = {}; pools: object[] = []; isSearching = false; isTypeaheading$ = new BehaviorSubject(false); @@ -96,13 +95,6 @@ export class SearchFormComponent implements OnInit { searchText: ['', Validators.required], }); - if (this.network === 'liquid' || this.network === 'liquidtestnet') { - this.assetsService.getAssetsMinimalJson$ - .subscribe((assets) => { - this.assets = assets; - }); - } - const searchText$ = this.searchForm.get('searchText').valueChanges .pipe( map((text) => { @@ -121,7 +113,8 @@ export class SearchFormComponent implements OnInit { return of([ [], { nodes: [], channels: [] }, - this.pools + this.pools, + [], ]); } this.isTypeaheading$.next(true); @@ -129,7 +122,8 @@ export class SearchFormComponent implements OnInit { return zip( this.electrsApiService.getAddressesByPrefix$(text).pipe(catchError(() => of([]))), [{ nodes: [], channels: [] }], - this.getMiningPools() + this.getMiningPools(), + this.getLiquidAssetSearch$(text), ); } return zip( @@ -138,7 +132,8 @@ export class SearchFormComponent implements OnInit { nodes: [], channels: [], }))), - this.getMiningPools() + this.getMiningPools(), + this.getLiquidAssetSearch$(text), ); }), map((result: any[]) => { @@ -159,7 +154,8 @@ export class SearchFormComponent implements OnInit { nodes: [], channels: [], }, - this.pools + this.pools, + [], ])) ] ).pipe( @@ -178,7 +174,7 @@ export class SearchFormComponent implements OnInit { addresses: [], nodes: [], channels: [], - liquidAsset: [], + liquidAssets: [], pools: [] }; } @@ -186,6 +182,7 @@ export class SearchFormComponent implements OnInit { const result = latestData[1]; const addressPrefixSearchResults = result[0]; const lightningResults = result[1]; + const liquidAssets = result[3]; // Do not show date and timestamp results for liquid const isNetworkBitcoin = this.network === '' || this.network === 'testnet' || this.network === 'testnet4' || this.network === 'signet'; @@ -198,8 +195,8 @@ export class SearchFormComponent implements OnInit { const matchesAddress = !matchesTxId && this.regexAddress.test(searchText); const publicKey = matchesAddress && searchText.startsWith('0'); const otherNetworks = findOtherNetworks(searchText, this.network as any || 'mainnet', this.env); - const liquidAsset = this.assets ? (this.assets[searchText] || []) : []; const pools = this.pools.filter(pool => pool['name'].toLowerCase().includes(searchText.toLowerCase())).slice(0, 10); + const hashQuickMatch = +(matchesBlockHeight || matchesBlockHash || (matchesTxId && !liquidAssets.length) || matchesAddress || matchesUnixTimestamp || matchesDateTime); if (matchesDateTime && searchText.indexOf('/') !== -1) { searchText = searchText.replace(/\//g, '-'); @@ -211,7 +208,7 @@ export class SearchFormComponent implements OnInit { return { searchText: searchText, - hashQuickMatch: +(matchesBlockHeight || matchesBlockHash || matchesTxId || matchesAddress || matchesUnixTimestamp || matchesDateTime), + hashQuickMatch: hashQuickMatch, blockHeight: matchesBlockHeight, dateTime: matchesDateTime, unixTimestamp: matchesUnixTimestamp, @@ -223,7 +220,7 @@ export class SearchFormComponent implements OnInit { otherNetworks: otherNetworks, nodes: lightningResults.nodes, channels: lightningResults.channels, - liquidAsset: liquidAsset, + liquidAssets: liquidAssets, pools: pools }; }) @@ -258,6 +255,8 @@ export class SearchFormComponent implements OnInit { } } else if (result.slug) { this.navigate('/mining/pool/', result.slug); + } else if (result.asset_id) { + this.navigate('/assets/asset/', result.asset_id); } } @@ -275,19 +274,24 @@ export class SearchFormComponent implements OnInit { } else if (this.regexTransaction.test(searchText)) { const matches = this.regexTransaction.exec(searchText); if (this.network === 'liquid' || this.network === 'liquidtestnet') { - if (this.assets[matches[0]]) { - this.navigate('/assets/asset/', matches[0]); - } - this.electrsApiService.getAsset$(matches[0]) - .subscribe( - () => { this.navigate('/assets/asset/', matches[0]); }, - () => { - this.electrsApiService.getBlock$(matches[0]) + this.assetsService.searchLiquidAssets$(matches[0], 1) + .pipe(catchError(() => of([]))) + .subscribe((assets) => { + if (assets[0]?.asset_id === matches[0]) { + this.navigate('/assets/asset/', matches[0]); + } else { + this.electrsApiService.getAsset$(matches[0]) .subscribe( - (block) => { this.navigate('/block/', matches[0], { state: { data: { block } } }); }, - () => { this.navigate('/tx/', matches[0]); }); + () => { this.navigate('/assets/asset/', matches[0]); }, + () => { + this.electrsApiService.getBlock$(matches[0]) + .subscribe( + (block) => { this.navigate('/block/', matches[0], { state: { data: { block } } }); }, + () => { this.navigate('/tx/', matches[0]); }); + } + ); } - ); + }); } else { this.navigate('/tx/', matches[0]); } @@ -347,4 +351,11 @@ export class SearchFormComponent implements OnInit { catchError(() => of([])) ); } + + getLiquidAssetSearch$(searchText: string): Observable { + if (this.network !== 'liquid' && this.network !== 'liquidtestnet') { + return of([]); + } + return this.assetsService.searchLiquidAssets$(searchText, 10).pipe(catchError(() => of([]))); + } } diff --git a/frontend/src/app/components/search-form/search-results/search-results.component.html b/frontend/src/app/components/search-form/search-results/search-results.component.html index 22e823265..79b4ad9bf 100644 --- a/frontend/src/app/components/search-form/search-results/search-results.component.html +++ b/frontend/src/app/components/search-form/search-results/search-results.component.html @@ -1,4 +1,4 @@ -