mirror of
https://github.com/mempool/mempool.git
synced 2026-08-13 12:33:11 +02:00
Merge pull request #6525 from mempool/mononaut/use-new-liquid-assets-apis
This commit is contained in:
commit
e11630315d
13 changed files with 350 additions and 148 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string>) => Observable<readonly any[]>);
|
||||
formatterFn = (asset: AssetExtended) => asset.name + ' (' + asset.ticker + ')';
|
||||
formatterFn = (asset: AssetRegistryItem) => asset.name + ' (' + asset.ticker + ')';
|
||||
focus$ = new Subject<string>();
|
||||
click$ = new Subject<string>();
|
||||
|
||||
|
|
@ -51,6 +51,7 @@ export class AssetsNavComponent implements OnInit {
|
|||
|
||||
typeaheadSearch = (text$: Observable<string>) => {
|
||||
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);
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ export class SearchFormComponent implements OnInit {
|
|||
@Input() hamburgerOpen = false;
|
||||
env: Env;
|
||||
network = '';
|
||||
assets: object = {};
|
||||
pools: object[] = [];
|
||||
isSearching = false;
|
||||
isTypeaheading$ = new BehaviorSubject<boolean>(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<any[]> {
|
||||
if (this.network !== 'liquid' && this.network !== 'liquidtestnet') {
|
||||
return of([]);
|
||||
}
|
||||
return this.assetsService.searchLiquidAssets$(searchText, 10).pipe(catchError(() => of([])));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<div class="dropdown-menu show" *ngIf="results" [hidden]="!results.hashQuickMatch && !results.otherNetworks.length && !results.addresses.length && !results.nodes.length && !results.channels.length && !results.liquidAsset.length && !results.pools.length">
|
||||
<div class="dropdown-menu show" *ngIf="results" [hidden]="!results.hashQuickMatch && !results.otherNetworks.length && !results.addresses.length && !results.nodes.length && !results.channels.length && !results.liquidAssets.length && !results.pools.length">
|
||||
<ng-template [ngIf]="results.blockHeight">
|
||||
<div class="card-title" i18n="search.bitcoin-block-height">{{ networkName }} Block Height</div>
|
||||
<button (click)="clickItem(0)" [class.active]="0 === activeIdx" type="button" role="option" class="dropdown-item">
|
||||
|
|
@ -17,7 +17,7 @@
|
|||
<ng-container *ngTemplateOutlet="goTo; context: { $implicit: results.searchText }"></ng-container>
|
||||
</button>
|
||||
</ng-template>
|
||||
<ng-template [ngIf]="results.txId && !results.liquidAsset.length">
|
||||
<ng-template [ngIf]="results.txId && !results.liquidAssets.length">
|
||||
<div class="card-title" i18n="search.bitcoin-transaction">{{ networkName }} Transaction</div>
|
||||
<button (click)="clickItem(0)" [class.active]="0 === activeIdx" type="button" role="option" class="dropdown-item">
|
||||
<ng-container *ngTemplateOutlet="goTo; context: { $implicit: results.searchText | shortenString : 13 }"></ng-container>
|
||||
|
|
@ -81,11 +81,13 @@
|
|||
<ng-container *ngTemplateOutlet="goTo; context: { $implicit: results.searchText | shortenString : isMobile ? 17 : 30 }"></ng-container>
|
||||
</button>
|
||||
</ng-template>
|
||||
<ng-template [ngIf]="results.liquidAsset.length">
|
||||
<ng-template [ngIf]="results.liquidAssets.length">
|
||||
<div class="card-title" i18n="search.liquid-asset">Liquid Asset</div>
|
||||
<button (click)="clickItem(0)" [class.active]="0 === activeIdx" type="button" role="option" class="dropdown-item">
|
||||
<ng-container *ngTemplateOutlet="goTo; context: { $implicit: results.searchText | shortenString : 11 }"></ng-container> <b>({{ results.liquidAsset[1] }})</b>
|
||||
</button>
|
||||
<ng-template ngFor [ngForOf]="results.liquidAssets" let-asset let-i="index">
|
||||
<button (click)="clickItem(results.hashQuickMatch + results.addresses.length + results.pools.length + results.nodes.length + results.channels.length + results.otherNetworks.length + i)" [class.active]="results.hashQuickMatch + results.addresses.length + results.pools.length + results.nodes.length + results.channels.length + results.otherNetworks.length + i === activeIdx" type="button" role="option" class="dropdown-item">
|
||||
<ng-container *ngTemplateOutlet="goTo; context: { $implicit: asset.name }"></ng-container> <b>({{ asset.ticker }})</b>
|
||||
</button>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ export class SearchResultsComponent implements OnChanges {
|
|||
ngOnChanges() {
|
||||
this.activeIdx = 0;
|
||||
if (this.results) {
|
||||
this.resultsFlattened = [...(this.results.hashQuickMatch ? [this.results.searchText] : []), ...this.results.addresses, ...this.results.pools, ...this.results.nodes, ...this.results.channels, ...this.results.otherNetworks];
|
||||
this.resultsFlattened = [...(this.results.hashQuickMatch ? [this.results.searchText] : []), ...this.results.addresses, ...this.results.pools, ...this.results.nodes, ...this.results.channels, ...this.results.otherNetworks, ...this.results.liquidAssets];
|
||||
// If searchText is a public key corresponding to a node, select it by default
|
||||
if (this.results.publicKey && this.results.nodes.length > 0) {
|
||||
this.activeIdx = 1;
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ export class TransactionsListComponent implements OnInit, OnChanges, OnDestroy {
|
|||
refreshOutspends$: ReplaySubject<string[]> = new ReplaySubject();
|
||||
refreshChannels$: ReplaySubject<string[]> = new ReplaySubject();
|
||||
showDetails$ = new BehaviorSubject<boolean>(false);
|
||||
assetsMinimal: any;
|
||||
assetsMinimal: any = {};
|
||||
transactionsLength: number = 0;
|
||||
inputRowLimit: number = 12;
|
||||
outputRowLimit: number = 12;
|
||||
|
|
@ -118,12 +118,6 @@ export class TransactionsListComponent implements OnInit, OnChanges, OnDestroy {
|
|||
}
|
||||
});
|
||||
|
||||
if (this.network === 'liquid' || this.network === 'liquidtestnet') {
|
||||
this.assetsService.getAssetsMinimalJson$.subscribe((assets) => {
|
||||
this.assetsMinimal = assets;
|
||||
});
|
||||
}
|
||||
|
||||
this.outspendsSubscription = merge(
|
||||
this.refreshOutspends$
|
||||
.pipe(
|
||||
|
|
@ -238,6 +232,7 @@ export class TransactionsListComponent implements OnInit, OnChanges, OnDestroy {
|
|||
}
|
||||
|
||||
this.transactionsLength = this.transactions.length;
|
||||
this.loadLiquidAssetData();
|
||||
|
||||
if (!this.txPreview) {
|
||||
this.cacheService.setTxCache(this.transactions);
|
||||
|
|
@ -370,6 +365,19 @@ export class TransactionsListComponent implements OnInit, OnChanges, OnDestroy {
|
|||
}
|
||||
}
|
||||
|
||||
private loadLiquidAssetData(): void {
|
||||
if (!this.isLiquid) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.assetsService.getLiquidAssetsMinimalData(this.transactions).then((assets) => {
|
||||
this.assetsMinimal = assets;
|
||||
this.ref.markForCheck();
|
||||
}).catch(() => {
|
||||
this.ref.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
updateAddressSimilarities(): void {
|
||||
if (!this.transactions || !this.transactions.length) {
|
||||
return;
|
||||
|
|
@ -549,6 +557,7 @@ export class TransactionsListComponent implements OnInit, OnChanges, OnDestroy {
|
|||
for (const [index, vin] of temp.entries()) {
|
||||
newTx.vin[index].isInscription = vin.isInscription;
|
||||
}
|
||||
this.loadLiquidAssetData();
|
||||
this.ref.markForCheck();
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
|
|||
zeroValueWidth = 60;
|
||||
zeroValueThickness = 20;
|
||||
hasLine: boolean;
|
||||
assetsMinimal: any;
|
||||
assetsMinimal: any = {};
|
||||
nativeAssetId = this.stateService.network === 'liquidtestnet' ? environment.nativeTestAssetId : environment.nativeAssetId;
|
||||
|
||||
outspendsSubscription: Subscription;
|
||||
|
|
@ -116,12 +116,6 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
|
|||
ngOnInit(): void {
|
||||
this.initGraph();
|
||||
|
||||
if (this.network === 'liquid' || this.network === 'liquidtestnet') {
|
||||
this.assetsService.getAssetsMinimalJson$.subscribe((assets) => {
|
||||
this.assetsMinimal = assets;
|
||||
});
|
||||
}
|
||||
|
||||
this.outspendsSubscription = merge(
|
||||
this.refreshOutspends$
|
||||
.pipe(
|
||||
|
|
@ -156,11 +150,22 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
|
|||
|
||||
ngOnChanges(): void {
|
||||
this.initGraph();
|
||||
this.loadLiquidAssetData();
|
||||
if (!this.cached) {
|
||||
this.refreshOutspends$.next(this.tx.txid);
|
||||
}
|
||||
}
|
||||
|
||||
private loadLiquidAssetData(): void {
|
||||
if (!this.isLiquid || !this.tx) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.assetsService.getLiquidAssetsMinimalData([this.tx]).then((assets) => {
|
||||
this.assetsMinimal = assets;
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
initGraph(): void {
|
||||
this.isLiquid = (this.network === 'liquid' || this.network === 'liquidtestnet');
|
||||
this.gradient = this.gradientColors[this.network];
|
||||
|
|
|
|||
|
|
@ -206,6 +206,10 @@ export interface Asset {
|
|||
status: Status;
|
||||
chain_stats: AssetStats;
|
||||
mempool_stats: AssetStats;
|
||||
name?: string;
|
||||
ticker?: string;
|
||||
precision?: number;
|
||||
entity?: Entity;
|
||||
}
|
||||
|
||||
export interface AssetExtended extends Asset {
|
||||
|
|
@ -221,6 +225,15 @@ export interface Entity {
|
|||
domain: string;
|
||||
}
|
||||
|
||||
export interface AssetRegistryItem {
|
||||
asset_id: string;
|
||||
name: string;
|
||||
ticker?: string;
|
||||
precision?: number;
|
||||
domain?: string;
|
||||
entity?: Entity;
|
||||
}
|
||||
|
||||
interface IssuanceTxin {
|
||||
txid: string;
|
||||
vin: number;
|
||||
|
|
|
|||
|
|
@ -1,33 +1,46 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map, shareReplay, switchMap } from 'rxjs/operators';
|
||||
import { firstValueFrom, 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 { AssetExtended } from '@interfaces/electrs.interface';
|
||||
import { ElectrsApiService } from '@app/services/electrs-api.service';
|
||||
import { Asset, AssetExtended, AssetRegistryItem, Transaction } from '@interfaces/electrs.interface';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AssetsService {
|
||||
nativeAssetId = this.stateService.network === 'liquidtestnet' ? environment.nativeTestAssetId : environment.nativeAssetId;
|
||||
|
||||
getAssetsJson$: Observable<{ array: AssetExtended[]; objects: any}>;
|
||||
getAssetsMinimalJson$: Observable<any>;
|
||||
getWorldMapJson$: Observable<any>;
|
||||
registryAvailable = true;
|
||||
private apiBaseUrl = '';
|
||||
private nativeAssetId = this.stateService.network === 'liquidtestnet' ? environment.nativeTestAssetId : environment.nativeAssetId;
|
||||
private assetsMinimalCache: any = {
|
||||
[this.nativeAssetId]: this.stateService.network === 'liquid' ? [null, 'LBTC', 'Liquid Bitcoin', 8] : [null, 'tLBTC', 'Test Liquid Bitcoin', 8],
|
||||
};
|
||||
|
||||
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.nativeAssetId = this.stateService.network === 'liquidtestnet' ? environment.nativeTestAssetId : environment.nativeAssetId;
|
||||
this.assetsMinimalCache = {
|
||||
[this.nativeAssetId]: this.stateService.network === 'liquid' ? [null, 'LBTC', 'Liquid Bitcoin', 8] : [null, 'tLBTC', 'Test Liquid Bitcoin', 8],
|
||||
};
|
||||
});
|
||||
|
||||
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 +69,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,6 +80,182 @@ 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 async getLiquidAssetData(assetId: string): Promise<Partial<Asset>> {
|
||||
if (this.stateService.network === 'liquid' && assetId === environment.nativeAssetId) {
|
||||
const asset = { asset_id: assetId, name: 'Liquid Bitcoin', ticker: 'LBTC', precision: 8 };
|
||||
return asset;
|
||||
} else if (this.stateService.network === 'liquidtestnet' && assetId === environment.nativeTestAssetId) {
|
||||
const asset = { asset_id: assetId, name: 'Test Liquid Bitcoin', ticker: 'tLBTC', precision: 8 };
|
||||
return asset;
|
||||
} else if (this.registryAvailable) {
|
||||
try {
|
||||
const apiAsset = await firstValueFrom(this.electrsApiService.getLiquidAssetRegistry$(assetId));
|
||||
if (apiAsset.name || apiAsset.ticker || apiAsset.precision != null) {
|
||||
const asset = { ...apiAsset, asset_id: assetId };
|
||||
this.assetsMinimalCache[assetId] = [asset.entity?.domain || asset.domain || null, asset.ticker, asset.name, asset.precision || 0];
|
||||
return asset;
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error?.status === 501) {
|
||||
this.registryAvailable = false;
|
||||
} else if (error?.status !== 404) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const assets = await firstValueFrom(this.getAssetsJson$);
|
||||
const asset: any = assets.objects[assetId] || { asset_id: assetId };
|
||||
if (asset.name || asset.ticker || asset.precision != null) {
|
||||
this.assetsMinimalCache[assetId] = [asset.entity?.domain || asset.domain || null, asset.ticker, asset.name, asset.precision || 0];
|
||||
}
|
||||
return asset;
|
||||
}
|
||||
|
||||
private cacheLiquidAssetMinimalData(asset: Partial<Asset>): void {
|
||||
if (asset.asset_id && (asset.name || asset.ticker || asset.precision != null)) {
|
||||
this.assetsMinimalCache[asset.asset_id] = [asset.entity?.domain || (asset as any).domain || null, asset.ticker, asset.name, asset.precision || 0];
|
||||
}
|
||||
}
|
||||
|
||||
public async getLiquidAssetMinimalData(assetId: string): Promise<any[]> {
|
||||
if (this.assetsMinimalCache[assetId]) {
|
||||
return this.assetsMinimalCache[assetId];
|
||||
}
|
||||
|
||||
const asset: any = await this.getLiquidAssetData(assetId);
|
||||
if (asset.name || asset.ticker || asset.precision != null) {
|
||||
return this.assetsMinimalCache[assetId];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public async getLiquidAssetsMinimalData(transactions: Transaction[]): Promise<any> {
|
||||
const assetIds = new Set<string>();
|
||||
for (const tx of transactions || []) {
|
||||
for (const vin of tx.vin || []) {
|
||||
if (vin.prevout?.asset && vin.prevout.asset !== this.nativeAssetId) {
|
||||
assetIds.add(vin.prevout.asset);
|
||||
}
|
||||
}
|
||||
for (const vout of tx.vout || []) {
|
||||
if (vout.asset && vout.asset !== this.nativeAssetId) {
|
||||
assetIds.add(vout.asset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const missingAssetIds = Array.from(assetIds).filter((assetId) => !this.assetsMinimalCache[assetId]);
|
||||
await Promise.all(missingAssetIds.map((assetId) => this.getLiquidAssetMinimalData(assetId)));
|
||||
|
||||
return this.assetsMinimalCache;
|
||||
}
|
||||
|
||||
public enrichLiquidAsset$(asset: Asset): Observable<Asset> {
|
||||
if (asset.name || asset.ticker || asset.precision != null) {
|
||||
this.cacheLiquidAssetMinimalData(asset);
|
||||
return of(asset);
|
||||
} else if (this.stateService.network === 'liquid' && asset.asset_id === environment.nativeAssetId) {
|
||||
const nativeAsset = { ...asset, name: 'Liquid Bitcoin', ticker: 'LBTC', precision: 8 };
|
||||
return of(nativeAsset);
|
||||
} else if (this.stateService.network === 'liquidtestnet' && asset.asset_id === environment.nativeTestAssetId) {
|
||||
const nativeAsset = { ...asset, name: 'Test Liquid Bitcoin', ticker: 'tLBTC', precision: 8 };
|
||||
return of(nativeAsset);
|
||||
} else {
|
||||
return this.getAssetsJson$.pipe(
|
||||
map((assets) => {
|
||||
const enrichedAsset = assets.objects[asset.asset_id] ? { ...asset, ...assets.objects[asset.asset_id] } : asset;
|
||||
this.cacheLiquidAssetMinimalData(enrichedAsset);
|
||||
return enrichedAsset;
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
})),
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
public searchLiquidAssets$(searchText: string, limit: number): Observable<AssetRegistryItem[]> {
|
||||
const lowerSearchText = searchText.toLowerCase();
|
||||
return (this.registryAvailable ? this.electrsApiService.getLiquidAssetsRegistrySearch$(searchText).pipe(
|
||||
catchError((error) => {
|
||||
if (![404, 501].includes(error.status)) {
|
||||
return throwError(() => error);
|
||||
}
|
||||
this.registryAvailable = false;
|
||||
return of(null);
|
||||
}),
|
||||
) : of(null)).pipe(
|
||||
switchMap((registryAssets) => registryAssets ? of(registryAssets) : this.getAssetsMinimalJson$.pipe(
|
||||
map((assets) => Object.entries(assets).map(([assetId, assetData]: [string, any[]]) => ({
|
||||
asset_id: assetId,
|
||||
entity: assetData[0] ? { domain: assetData[0] } : undefined,
|
||||
ticker: assetData[1] || '',
|
||||
name: assetData[2] || '',
|
||||
})).filter((asset) => asset.asset_id.indexOf(lowerSearchText) > -1
|
||||
|| asset.name.toLowerCase().indexOf(lowerSearchText) > -1
|
||||
|| (asset.ticker || '').toLowerCase().indexOf(lowerSearchText) > -1
|
||||
|| (asset.entity && asset.entity.domain || '').toLowerCase().indexOf(lowerSearchText) > -1)),
|
||||
)),
|
||||
switchMap((assets) => {
|
||||
if (!/^[0-9a-f]{64}$/i.test(searchText) || assets.some((asset) => asset.asset_id.toLowerCase() === lowerSearchText)) {
|
||||
return of(assets);
|
||||
}
|
||||
return this.electrsApiService.getLiquidAssetRegistry$(searchText).pipe(
|
||||
map((asset) => {
|
||||
if (asset.name || asset.ticker || asset.precision != null) {
|
||||
this.assetsMinimalCache[asset.asset_id] = [asset.entity?.domain || asset.domain || null, asset.ticker, asset.name, asset.precision || 0];
|
||||
return [{
|
||||
asset_id: asset.asset_id,
|
||||
entity: asset.entity,
|
||||
ticker: asset.ticker || '',
|
||||
name: asset.name || asset.asset_id,
|
||||
}, ...assets];
|
||||
}
|
||||
return assets;
|
||||
}),
|
||||
catchError(() => of(assets)),
|
||||
);
|
||||
}),
|
||||
map((assets) => assets.map((asset) => ({
|
||||
...asset,
|
||||
entity: asset.entity || (asset.domain ? { domain: asset.domain } : undefined),
|
||||
})).slice(0, limit)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,25 @@ 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' });
|
||||
}
|
||||
|
||||
getLiquidAssetsRegistrySearch$(query: string): Observable<AssetRegistryItem[]> {
|
||||
const params = new HttpParams().set('q', query);
|
||||
return this.httpClient.get<AssetRegistryItem[]>(this.apiBaseUrl + this.apiBasePath + '/api/assets/registry/search', { params });
|
||||
}
|
||||
|
||||
getLiquidAssetRegistry$(assetId: string): Observable<AssetRegistryItem> {
|
||||
return this.httpClient.get<AssetRegistryItem>(this.apiBaseUrl + this.apiBasePath + '/api/assets/registry/' + assetId);
|
||||
}
|
||||
|
||||
getAssetTransactions$(assetId: string): Observable<Transaction[]> {
|
||||
return this.httpClient.get<Transaction[]>(this.apiBaseUrl + this.apiBasePath + '/api/asset/' + assetId + '/txs');
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue