Merge branch 'master' into fee-visibility
This commit is contained in:
@@ -10,7 +10,7 @@ import { TelevisionComponent } from './components/television/television.componen
|
||||
import { StatisticsComponent } from './components/statistics/statistics.component';
|
||||
import { MempoolBlockComponent } from './components/mempool-block/mempool-block.component';
|
||||
import { AssetComponent } from './components/asset/asset.component';
|
||||
import { AssetsComponent } from './assets/assets.component';
|
||||
import { AssetsNavComponent } from './components/assets/assets-nav/assets-nav.component';
|
||||
import { StatusViewComponent } from './components/status-view/status-view.component';
|
||||
import { DashboardComponent } from './dashboard/dashboard.component';
|
||||
import { LatestBlocksComponent } from './components/latest-blocks/latest-blocks.component';
|
||||
@@ -23,6 +23,9 @@ import { SponsorComponent } from './components/sponsor/sponsor.component';
|
||||
import { LiquidMasterPageComponent } from './components/liquid-master-page/liquid-master-page.component';
|
||||
import { PushTransactionComponent } from './components/push-transaction/push-transaction.component';
|
||||
import { PoolRankingComponent } from './components/pool-ranking/pool-ranking.component';
|
||||
import { AssetGroupComponent } from './components/assets/asset-group/asset-group.component';
|
||||
import { AssetsFeaturedComponent } from './components/assets/assets-featured/assets-featured.component';
|
||||
import { AssetsComponent } from './components/assets/assets.component';
|
||||
|
||||
let routes: Routes = [
|
||||
{
|
||||
@@ -343,13 +346,31 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
||||
path: 'address/:id',
|
||||
component: AddressComponent
|
||||
},
|
||||
{
|
||||
path: 'asset/:id',
|
||||
component: AssetComponent
|
||||
},
|
||||
{
|
||||
path: 'assets',
|
||||
component: AssetsComponent,
|
||||
component: AssetsNavComponent,
|
||||
children: [
|
||||
{
|
||||
path: 'featured',
|
||||
component: AssetsFeaturedComponent,
|
||||
},
|
||||
{
|
||||
path: 'all',
|
||||
component: AssetsComponent,
|
||||
},
|
||||
{
|
||||
path: 'asset/:id',
|
||||
component: AssetComponent
|
||||
},
|
||||
{
|
||||
path: 'group/:id',
|
||||
component: AssetGroupComponent
|
||||
},
|
||||
{
|
||||
path: '**',
|
||||
redirectTo: 'featured'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'docs/api/:type',
|
||||
@@ -434,13 +455,27 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
||||
path: 'address/:id',
|
||||
component: AddressComponent
|
||||
},
|
||||
{
|
||||
path: 'asset/:id',
|
||||
component: AssetComponent
|
||||
},
|
||||
{
|
||||
path: 'assets',
|
||||
component: AssetsComponent,
|
||||
component: AssetsNavComponent,
|
||||
children: [
|
||||
{
|
||||
path: 'all',
|
||||
component: AssetsComponent,
|
||||
},
|
||||
{
|
||||
path: 'asset/:id',
|
||||
component: AssetComponent
|
||||
},
|
||||
{
|
||||
path: 'group/:id',
|
||||
component: AssetGroupComponent
|
||||
},
|
||||
{
|
||||
path: '**',
|
||||
redirectTo: 'all'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'docs/api/:type',
|
||||
|
||||
@@ -40,7 +40,8 @@ import { MempoolGraphComponent } from './components/mempool-graph/mempool-graph.
|
||||
import { PoolRankingComponent } from './components/pool-ranking/pool-ranking.component';
|
||||
import { LbtcPegsGraphComponent } from './components/lbtc-pegs-graph/lbtc-pegs-graph.component';
|
||||
import { AssetComponent } from './components/asset/asset.component';
|
||||
import { AssetsComponent } from './assets/assets.component';
|
||||
import { AssetsComponent } from './components/assets/assets.component';
|
||||
import { AssetsNavComponent } from './components/assets/assets-nav/assets-nav.component';
|
||||
import { StatusViewComponent } from './components/status-view/status-view.component';
|
||||
import { MinerComponent } from './components/miner/miner.component';
|
||||
import { SharedModule } from './shared/shared.module';
|
||||
@@ -64,6 +65,8 @@ import { LanguageService } from './services/language.service';
|
||||
import { SponsorComponent } from './components/sponsor/sponsor.component';
|
||||
import { PushTransactionComponent } from './components/push-transaction/push-transaction.component';
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { AssetsFeaturedComponent } from './components/assets/assets-featured/assets-featured.component';
|
||||
import { AssetGroupComponent } from './components/assets/asset-group/asset-group.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@@ -110,6 +113,9 @@ import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
PushTransactionComponent,
|
||||
DocsComponent,
|
||||
ApiDocsNavComponent,
|
||||
AssetsNavComponent,
|
||||
AssetsFeaturedComponent,
|
||||
AssetGroupComponent,
|
||||
],
|
||||
imports: [
|
||||
BrowserModule.withServerTransition({ appId: 'serverApp' }),
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
<div class="container-xl">
|
||||
<div class="title-asset">
|
||||
<h1 i18n="Registered assets page header">Registered assets</h1>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<form [formGroup]="searchForm" class="form-inline">
|
||||
<div class="input-group mb-2">
|
||||
<input style="width: 250px;" formControlName="searchText" type="text" class="form-control" i18n-placeholder="Search Assets Placeholder Text" placeholder="Search asset">
|
||||
<div class="input-group-append">
|
||||
<button [disabled]="!searchForm.get('searchText')?.value.length" class="btn btn-secondary" type="button" (click)="searchForm.get('searchText')?.setValue('');" autocomplete="off" i18n="Search Clear Button">Clear</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<ng-container *ngIf="(assets$ | async) as filteredAssets; else isLoading">
|
||||
<table class="table table-borderless table-striped">
|
||||
<thead>
|
||||
<th class="td-name" i18n="Asset name header">Name</th>
|
||||
<th i18n="Asset ticker header">Ticker</th>
|
||||
<th class="d-none d-md-block" i18n="Asset Issuer Domain header">Issuer domain</th>
|
||||
<th i18n="Asset ID header">Asset ID</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let asset of filteredAssets; trackBy: trackByAsset">
|
||||
<td class="td-name"><a [routerLink]="['/asset/' | relativeUrl, asset.asset_id]">{{ asset.name }}</a></td>
|
||||
<td>{{ asset.ticker }}</td>
|
||||
<td class="d-none d-md-block">{{ asset.entity && asset.entity.domain }}</td>
|
||||
<td><a [routerLink]="['/asset/' | relativeUrl, asset.asset_id]">{{ asset.asset_id | shortenString : 13 }}</a> <app-clipboard class="d-none d-sm-inline-block" [text]="asset.asset_id"></app-clipboard></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<br>
|
||||
|
||||
<ngb-pagination [collectionSize]="assets.length" [rotate]="true" [pageSize]="itemsPerPage" [(page)]="page" (pageChange)="pageChange(page)" [maxSize]="5" [boundaryLinks]="true"></ngb-pagination>
|
||||
|
||||
</ng-container>
|
||||
|
||||
<ng-template #isLoading>
|
||||
|
||||
<table class="table table-borderless table-striped">
|
||||
<thead>
|
||||
<th i18n="Asset name header">Name</th>
|
||||
<th i18n="Asset ticker header">Ticker</th>
|
||||
<th class="d-none d-md-block" i18n="Asset Issuer Domain header">Issuer domain</th>
|
||||
<th i18n="Asset ID header">Asset ID</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let dummy of [0,0,0,0,0,0,0,0,0,0]">
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
<td class="d-none d-md-block"><span class="skeleton-loader"></span></td>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</ng-template>
|
||||
|
||||
<ng-template [ngIf]="error">
|
||||
<div class="text-center">
|
||||
<ng-container i18n="Asset data load error">Error loading assets data.</ng-container>
|
||||
<br>
|
||||
<i>{{ error.error }}</i>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
</div>
|
||||
|
||||
<br>
|
||||
@@ -1,25 +0,0 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AssetsComponent } from './assets.component';
|
||||
|
||||
describe('AssetsComponent', () => {
|
||||
let component: AssetsComponent;
|
||||
let fixture: ComponentFixture<AssetsComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ AssetsComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(AssetsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,168 +0,0 @@
|
||||
import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { AssetsService } from '../services/assets.service';
|
||||
import { environment } from 'src/environments/environment';
|
||||
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
|
||||
import { distinctUntilChanged, map, filter, mergeMap, tap, take } from 'rxjs/operators';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { merge, combineLatest, Observable } from 'rxjs';
|
||||
import { AssetExtended } from '../interfaces/electrs.interface';
|
||||
import { SeoService } from '../services/seo.service';
|
||||
import { StateService } from '../services/state.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-assets',
|
||||
templateUrl: './assets.component.html',
|
||||
styleUrls: ['./assets.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class AssetsComponent implements OnInit {
|
||||
nativeAssetId = this.stateService.network === 'liquidtestnet' ? environment.nativeTestAssetId : environment.nativeAssetId;
|
||||
|
||||
assets: AssetExtended[];
|
||||
assetsCache: AssetExtended[];
|
||||
searchForm: FormGroup;
|
||||
assets$: Observable<AssetExtended[]>;
|
||||
|
||||
error: any;
|
||||
|
||||
page = 1;
|
||||
itemsPerPage: number;
|
||||
contentSpace = window.innerHeight - (250 + 200);
|
||||
fiveItemsPxSize = 250;
|
||||
|
||||
constructor(
|
||||
private assetsService: AssetsService,
|
||||
private formBuilder: FormBuilder,
|
||||
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.searchForm = this.formBuilder.group({
|
||||
searchText: [{ value: '', disabled: true }, Validators.required]
|
||||
});
|
||||
|
||||
this.assets$ = combineLatest([
|
||||
this.assetsService.getAssetsJson$,
|
||||
this.route.queryParams
|
||||
])
|
||||
.pipe(
|
||||
take(1),
|
||||
mergeMap(([assets, qp]) => {
|
||||
this.assets = Object.values(assets);
|
||||
if (this.stateService.network === 'liquid') {
|
||||
// @ts-ignore
|
||||
this.assets.push({
|
||||
name: 'Liquid Bitcoin',
|
||||
ticker: 'L-BTC',
|
||||
asset_id: this.nativeAssetId,
|
||||
});
|
||||
} else if (this.stateService.network === 'liquidtestnet') {
|
||||
// @ts-ignore
|
||||
this.assets.push({
|
||||
name: 'Test Liquid Bitcoin',
|
||||
ticker: 'tL-BTC',
|
||||
asset_id: this.nativeAssetId,
|
||||
});
|
||||
}
|
||||
|
||||
this.assets = this.assets.sort((a: any, b: any) => a.name.localeCompare(b.name));
|
||||
this.assetsCache = this.assets;
|
||||
this.searchForm.get('searchText').enable();
|
||||
|
||||
if (qp.search) {
|
||||
this.searchForm.get('searchText').setValue(qp.search, { emitEvent: false });
|
||||
}
|
||||
|
||||
return merge(
|
||||
this.searchForm.get('searchText').valueChanges
|
||||
.pipe(
|
||||
distinctUntilChanged(),
|
||||
tap((text) => {
|
||||
this.page = 1;
|
||||
this.searchTextChanged(text);
|
||||
})
|
||||
),
|
||||
this.route.queryParams
|
||||
.pipe(
|
||||
filter((queryParams) => {
|
||||
const newPage = parseInt(queryParams.page, 10);
|
||||
if (newPage !== this.page || queryParams.search !== this.searchForm.get('searchText').value) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}),
|
||||
map((queryParams) => {
|
||||
if (queryParams.page) {
|
||||
const newPage = parseInt(queryParams.page, 10);
|
||||
this.page = newPage;
|
||||
} else {
|
||||
this.page = 1;
|
||||
}
|
||||
if (this.searchForm.get('searchText').value !== (queryParams.search || '')) {
|
||||
this.searchTextChanged(queryParams.search);
|
||||
}
|
||||
if (queryParams.search) {
|
||||
this.searchForm.get('searchText').setValue(queryParams.search, { emitEvent: false });
|
||||
return queryParams.search;
|
||||
}
|
||||
return '';
|
||||
})
|
||||
),
|
||||
);
|
||||
}),
|
||||
map((searchText) => {
|
||||
const start = (this.page - 1) * this.itemsPerPage;
|
||||
if (searchText.length ) {
|
||||
const filteredAssets = this.assetsCache.filter((asset) => asset.name.toLowerCase().indexOf(searchText.toLowerCase()) > -1
|
||||
|| (asset.ticker || '').toLowerCase().indexOf(searchText.toLowerCase()) > -1);
|
||||
this.assets = filteredAssets;
|
||||
return filteredAssets.slice(start, this.itemsPerPage + start);
|
||||
} else {
|
||||
this.assets = this.assetsCache;
|
||||
return this.assets.slice(start, this.itemsPerPage + start);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
pageChange(page: number) {
|
||||
const queryParams = { page: page, search: this.searchForm.get('searchText').value };
|
||||
if (queryParams.search === '') {
|
||||
queryParams.search = null;
|
||||
}
|
||||
if (queryParams.page === 1) {
|
||||
queryParams.page = null;
|
||||
}
|
||||
this.page = -1;
|
||||
this.router.navigate([], {
|
||||
relativeTo: this.route,
|
||||
queryParams: queryParams,
|
||||
queryParamsHandling: 'merge',
|
||||
});
|
||||
}
|
||||
|
||||
searchTextChanged(text: string) {
|
||||
const queryParams = { search: text, page: 1 };
|
||||
if (queryParams.search === '') {
|
||||
queryParams.search = null;
|
||||
}
|
||||
if (queryParams.page === 1) {
|
||||
queryParams.page = null;
|
||||
}
|
||||
this.router.navigate([], {
|
||||
relativeTo: this.route,
|
||||
queryParams: queryParams,
|
||||
queryParamsHandling: 'merge',
|
||||
});
|
||||
}
|
||||
|
||||
trackByAsset(index: number, asset: any) {
|
||||
return asset.asset_id;
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="title-asset">
|
||||
<h1 i18n="asset|Liquid Asset page title">Asset</h1>
|
||||
<div class="tx-link">
|
||||
<a [routerLink]="['/asset/' | relativeUrl, assetString]">
|
||||
<a [routerLink]="['/assets/asset/' | relativeUrl, assetString]">
|
||||
<span class="d-inline d-lg-none">{{ assetString | shortenString : 24 }}</span>
|
||||
<span class="d-none d-lg-inline">{{ assetString }}</span>
|
||||
</a>
|
||||
@@ -20,7 +20,7 @@
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td i18n="asset.name|Liquid Asset name">Name</td>
|
||||
<td i18n="Asset name header">Name</td>
|
||||
<td class="assetName">{{ assetContract[2] }} ({{ assetContract[1] }})</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
||||
@@ -63,6 +63,7 @@ export class AssetComponent implements OnInit, OnDestroy {
|
||||
.pipe(
|
||||
switchMap((params: ParamMap) => {
|
||||
this.error = undefined;
|
||||
this.imageError = false;
|
||||
this.isLoadingAsset = true;
|
||||
this.loadedConfirmedTxCount = 0;
|
||||
this.asset = null;
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<div *ngIf="group$ | async as group; else loading">
|
||||
|
||||
<div class="main-title">
|
||||
<h2>{{ group.group.name }}</h2>
|
||||
|
||||
<div class="sub-title" i18n>Group of {{ group.group.assets.length | number }} assets</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<br>
|
||||
|
||||
<div class="featuredBox">
|
||||
<div *ngFor="let asset of group.assets">
|
||||
<div class="card">
|
||||
<a [routerLink]="['/assets/asset' | relativeUrl, asset.asset_id]">
|
||||
<img class="assetIcon" [src]="'https://liquid.network/api/v1/asset/' + asset.asset_id + '/icon'">
|
||||
</a>
|
||||
<div class="title">
|
||||
<a [routerLink]="['/assets/asset/' | relativeUrl, asset.asset_id]">{{ asset.name }}</a>
|
||||
</div>
|
||||
<div class="ticker">{{ asset.ticker }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<ng-template #loading>
|
||||
<br>
|
||||
<div class="text-center loadingGraphs">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
</ng-template>
|
||||
@@ -0,0 +1,60 @@
|
||||
.image {
|
||||
width: 150px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.main-title {
|
||||
float: left
|
||||
}
|
||||
|
||||
.sub-title {
|
||||
color: grey;
|
||||
}
|
||||
|
||||
.featuredBox {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
justify-content: center;
|
||||
gap: 27px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: #1d1f31;
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
@media (max-width: 767.98px) {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
margin-top: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sub-title {
|
||||
color: grey;
|
||||
}
|
||||
|
||||
.assetIcon {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
@media (max-width: 767.98px) {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
.view-link {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.ticker {
|
||||
color: grey;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, ParamMap } from '@angular/router';
|
||||
import { combineLatest, Observable } from 'rxjs';
|
||||
import { map, switchMap } from 'rxjs/operators';
|
||||
import { ApiService } from 'src/app/services/api.service';
|
||||
import { AssetsService } from 'src/app/services/assets.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-asset-group',
|
||||
templateUrl: './asset-group.component.html',
|
||||
styleUrls: ['./asset-group.component.scss']
|
||||
})
|
||||
export class AssetGroupComponent implements OnInit {
|
||||
group$: Observable<any>;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private apiService: ApiService,
|
||||
private assetsService: AssetsService,
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.group$ = this.route.paramMap
|
||||
.pipe(
|
||||
switchMap((params: ParamMap) => {
|
||||
return combineLatest([
|
||||
this.assetsService.getAssetsJson$,
|
||||
this.apiService.getAssetGroup$(params.get('id')),
|
||||
]);
|
||||
}),
|
||||
map(([assets, group]) => {
|
||||
const items = [];
|
||||
// @ts-ignore
|
||||
for (const item of group.assets) {
|
||||
items.push(assets.objects[item]);
|
||||
}
|
||||
return {
|
||||
group: group,
|
||||
assets: items
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<div *ngIf="featuredAssets$ | async as featured; else loading" class="featuredBox">
|
||||
|
||||
<div class="card" *ngFor="let group of featured">
|
||||
<ng-template [ngIf]="group.assets" [ngIfElse]="singleAsset">
|
||||
<a [routerLink]="['/assets/group' | relativeUrl, group.id]">
|
||||
<img class="assetIcon" [src]="'https://liquid.network/api/v1/asset/' + group.assets[0] + '/icon'">
|
||||
</a>
|
||||
<div class="title"><a [routerLink]="['/assets/group' | relativeUrl, group.id]">{{ group.name }}</a></div>
|
||||
<div class="sub-title" i18n>Group of {{ group.assets.length | number }} assets</div>
|
||||
</ng-template>
|
||||
<ng-template #singleAsset>
|
||||
<a [routerLink]="['/assets/asset/' | relativeUrl, group.asset]">
|
||||
<img class="assetIcon" [src]="'https://liquid.network/api/v1/asset/' + group.asset + '/icon'">
|
||||
</a>
|
||||
<div class="title">
|
||||
<a [routerLink]="['/assets/asset/' | relativeUrl, group.asset]">{{ group.name }}</a>
|
||||
</div>
|
||||
<div class="ticker">{{ group.ticker }}</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<ng-template #loading>
|
||||
<br>
|
||||
<div class="text-center loadingGraphs">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
</ng-template>
|
||||
@@ -0,0 +1,49 @@
|
||||
|
||||
.featuredBox {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
justify-content: center;
|
||||
gap: 27px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: #1d1f31;
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
@media (max-width: 767.98px) {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
margin-top: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sub-title {
|
||||
color: grey;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.assetIcon {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
@media (max-width: 767.98px) {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
.view-link {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.ticker {
|
||||
color: grey;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { ApiService } from 'src/app/services/api.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-assets-featured',
|
||||
templateUrl: './assets-featured.component.html',
|
||||
styleUrls: ['./assets-featured.component.scss']
|
||||
})
|
||||
export class AssetsFeaturedComponent implements OnInit {
|
||||
featuredAssets$: Observable<any>;
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.featuredAssets$ = this.apiService.listFeaturedAssets$();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<div class="container-xl">
|
||||
<div class="title-asset">
|
||||
<h1 i18n="Assets page header">Assets</h1>
|
||||
</div>
|
||||
|
||||
<div class="nav-container">
|
||||
<ul class="nav nav-pills">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" [routerLink]="['/assets/featured' | relativeUrl]" routerLinkActive="active" i18n>Featured</a>
|
||||
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" [routerLink]="['/assets/all' | relativeUrl]" routerLinkActive="active" i18n>All</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<form [formGroup]="searchForm">
|
||||
<div class="input-group mb-2">
|
||||
<input #instance="ngbTypeahead" [ngbTypeahead]="typeaheadSearchFn" [resultFormatter]="formatterFn" (selectItem)="itemSelected()" (focus)="focus$.next($any($event).target.value)" (click)="click$.next($any($event).target.value)" formControlName="searchText" type="text" class="form-control" i18n-placeholder="Search Assets Placeholder Text" placeholder="Search asset">
|
||||
<div class="input-group-append">
|
||||
<button [disabled]="!searchForm.get('searchText')?.value.length" class="btn btn-secondary" type="button" (click)="searchForm.get('searchText')?.setValue('');" autocomplete="off" i18n="Search Clear Button">Clear</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<router-outlet></router-outlet>
|
||||
|
||||
</div>
|
||||
|
||||
<br>
|
||||
@@ -0,0 +1,24 @@
|
||||
ul {
|
||||
margin-bottom: 20px;
|
||||
float: left;
|
||||
|
||||
}
|
||||
|
||||
form {
|
||||
float: right;
|
||||
width: 300px;
|
||||
@media (max-width: 767.98px) {
|
||||
width: 90%;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.nav-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import { Component, OnInit, ViewChild } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, 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 'src/app/interfaces/electrs.interface';
|
||||
import { AssetsService } from 'src/app/services/assets.service';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { StateService } from 'src/app/services/state.service';
|
||||
import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe';
|
||||
import { environment } from 'src/environments/environment';
|
||||
|
||||
@Component({
|
||||
selector: 'app-assets-nav',
|
||||
templateUrl: './assets-nav.component.html',
|
||||
styleUrls: ['./assets-nav.component.scss']
|
||||
})
|
||||
export class AssetsNavComponent implements OnInit {
|
||||
@ViewChild('instance', {static: true}) instance: NgbTypeahead;
|
||||
nativeAssetId = this.stateService.network === 'liquidtestnet' ? environment.nativeTestAssetId : environment.nativeAssetId;
|
||||
searchForm: FormGroup;
|
||||
assetsCache: AssetExtended[];
|
||||
|
||||
typeaheadSearchFn: ((text: Observable<string>) => Observable<readonly any[]>);
|
||||
formatterFn = (asset: AssetExtended) => asset.name + ' (' + asset.ticker + ')';
|
||||
focus$ = new Subject<string>();
|
||||
click$ = new Subject<string>();
|
||||
|
||||
itemsPerPage = 15;
|
||||
|
||||
constructor(
|
||||
private formBuilder: FormBuilder,
|
||||
private seoService: SeoService,
|
||||
private router: Router,
|
||||
private assetsService: AssetsService,
|
||||
private stateService: StateService,
|
||||
private relativeUrlPipe: RelativeUrlPipe,
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.seoService.setTitle($localize`:@@ee8f8008bae6ce3a49840c4e1d39b4af23d4c263:Assets`);
|
||||
this.typeaheadSearchFn = this.typeaheadSearch;
|
||||
|
||||
this.searchForm = this.formBuilder.group({
|
||||
searchText: [{ value: '', disabled: false }, Validators.required]
|
||||
});
|
||||
}
|
||||
|
||||
typeaheadSearch = (text$: Observable<string>) => {
|
||||
const debouncedText$ = text$.pipe(
|
||||
distinctUntilChanged()
|
||||
);
|
||||
const clicksWithClosedPopup$ = this.click$.pipe(filter(() => !this.instance.isPopupOpen()));
|
||||
const inputFocus$ = this.focus$;
|
||||
|
||||
return merge(debouncedText$, inputFocus$, clicksWithClosedPopup$)
|
||||
.pipe(
|
||||
switchMap((searchText) => {
|
||||
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);
|
||||
}
|
||||
})
|
||||
)
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
itemSelected() {
|
||||
setTimeout(() => this.search());
|
||||
}
|
||||
|
||||
search() {
|
||||
const searchText = this.searchForm.value.searchText;
|
||||
this.navigate('/assets/asset/', searchText.asset_id);
|
||||
}
|
||||
|
||||
navigate(url: string, searchText: string, extras?: any) {
|
||||
this.router.navigate([this.relativeUrlPipe.transform(url), searchText], extras);
|
||||
this.searchForm.setValue({
|
||||
searchText: '',
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
52
frontend/src/app/components/assets/assets.component.html
Normal file
52
frontend/src/app/components/assets/assets.component.html
Normal file
@@ -0,0 +1,52 @@
|
||||
<ng-container *ngIf="(assets$ | async) as filteredAssets; else isLoading">
|
||||
<table class="table table-borderless table-striped">
|
||||
<thead>
|
||||
<th class="td-name" i18n="Asset name header">Name</th>
|
||||
<th i18n="Asset ticker header">Ticker</th>
|
||||
<th class="d-none d-md-table-cell" i18n="Asset Issuer Domain header">Issuer domain</th>
|
||||
<th class="d-none d-lg-table-cell" i18n="Asset ID header">Asset ID</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let asset of filteredAssets; trackBy: trackByAsset">
|
||||
<td class="td-name"><a [routerLink]="['/assets/asset/' | relativeUrl, asset.asset_id]">{{ asset.name }}</a></td>
|
||||
<td>{{ asset.ticker }}</td>
|
||||
<td class="d-none d-md-table-cell">{{ asset.entity && asset.entity.domain }}</td>
|
||||
<td class="d-none d-lg-table-cell"><a [routerLink]="['/assets/asset/' | relativeUrl, asset.asset_id]">{{ asset.asset_id | shortenString : 13 }}</a> <app-clipboard class="d-none d-sm-inline-block" [text]="asset.asset_id"></app-clipboard></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<br>
|
||||
|
||||
<ngb-pagination [collectionSize]="assets.length" [rotate]="true" [pageSize]="itemsPerPage" [(page)]="page" (pageChange)="pageChange(page)" [maxSize]="paginationMaxSize" [boundaryLinks]="true" [ellipses]="ellipses"></ngb-pagination>
|
||||
|
||||
</ng-container>
|
||||
|
||||
<ng-template #isLoading>
|
||||
|
||||
<table class="table table-borderless table-striped">
|
||||
<thead>
|
||||
<th i18n="Asset name header">Name</th>
|
||||
<th i18n="Asset ticker header">Ticker</th>
|
||||
<th class="d-none d-md-table-cell" i18n="Asset Issuer Domain header">Issuer domain</th>
|
||||
<th class="d-none d-lg-table-cell" i18n="Asset ID header">Asset ID</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let dummy of [0,0,0,0,0,0,0,0,0,0]">
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
<td><span class="skeleton-loader"></span></td>
|
||||
<td class="d-none d-md-table-cell"><span class="skeleton-loader"></span></td>
|
||||
<td class="d-none d-lg-table-cell"><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</ng-template>
|
||||
|
||||
<ng-template [ngIf]="error">
|
||||
<div class="text-center">
|
||||
<ng-container i18n="Asset data load error">Error loading assets data.</ng-container>
|
||||
<br>
|
||||
<i>{{ error.error }}</i>
|
||||
</div>
|
||||
</ng-template>
|
||||
99
frontend/src/app/components/assets/assets.component.ts
Normal file
99
frontend/src/app/components/assets/assets.component.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { AssetsService } from 'src/app/services/assets.service';
|
||||
import { environment } from 'src/environments/environment';
|
||||
import { FormGroup } from '@angular/forms';
|
||||
import { filter, map, switchMap, take } from 'rxjs/operators';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { combineLatest, Observable } from 'rxjs';
|
||||
import { AssetExtended } from 'src/app/interfaces/electrs.interface';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { StateService } from 'src/app/services/state.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-assets',
|
||||
templateUrl: './assets.component.html',
|
||||
styleUrls: ['./assets.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
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: FormGroup;
|
||||
assets$: Observable<AssetExtended[]>;
|
||||
|
||||
page = 1;
|
||||
error: any;
|
||||
|
||||
itemsPerPage: number;
|
||||
contentSpace = window.innerHeight - (250 + 200);
|
||||
fiveItemsPxSize = 250;
|
||||
|
||||
constructor(
|
||||
private assetsService: AssetsService,
|
||||
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,
|
||||
])
|
||||
.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(() => {
|
||||
const start = (this.page - 1) * this.itemsPerPage;
|
||||
return this.assets.slice(start, this.itemsPerPage + start);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
pageChange(page: number) {
|
||||
const queryParams = { page: page };
|
||||
if (queryParams.page === 1) {
|
||||
queryParams.page = null;
|
||||
}
|
||||
this.page = -1;
|
||||
this.router.navigate([], {
|
||||
relativeTo: this.route,
|
||||
queryParams: queryParams,
|
||||
queryParamsHandling: 'merge',
|
||||
});
|
||||
}
|
||||
|
||||
trackByAsset(index: number, asset: any) {
|
||||
return asset.asset_id;
|
||||
}
|
||||
}
|
||||
@@ -74,9 +74,9 @@
|
||||
<div class="col-sm">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr *ngIf="block.medianFee !== undefined">
|
||||
<tr *ngIf="block?.extras?.medianFee != undefined">
|
||||
<td class="td-width" i18n="block.median-fee">Median fee</td>
|
||||
<td>~{{ block.medianFee | number:'1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span> <span class="fiat"><app-fiat [value]="block.medianFee * 140" digitsInfo="1.2-2" i18n-ngbTooltip="Transaction fee tooltip" ngbTooltip="Based on average native segwit transaction of 140 vBytes" placement="bottom"></app-fiat></span></td>
|
||||
<td>~{{ block?.extras?.medianFee | number:'1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span> <span class="fiat"><app-fiat [value]="block?.extras?.medianFee * 140" digitsInfo="1.2-2" i18n-ngbTooltip="Transaction fee tooltip" ngbTooltip="Based on average native segwit transaction of 140 vBytes" placement="bottom"></app-fiat></span></td>
|
||||
</tr>
|
||||
<ng-template [ngIf]="fees !== undefined" [ngIfElse]="loadingFees">
|
||||
<tr>
|
||||
|
||||
@@ -3,12 +3,13 @@ import { Location } from '@angular/common';
|
||||
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
|
||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
||||
import { switchMap, tap, debounceTime, catchError, map } from 'rxjs/operators';
|
||||
import { Block, Transaction, Vout } from '../../interfaces/electrs.interface';
|
||||
import { Transaction, Vout } from '../../interfaces/electrs.interface';
|
||||
import { Observable, of, Subscription } from 'rxjs';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { WebsocketService } from 'src/app/services/websocket.service';
|
||||
import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe';
|
||||
import { BlockExtended } from 'src/app/interfaces/node-api.interface';
|
||||
|
||||
@Component({
|
||||
selector: 'app-block',
|
||||
@@ -17,13 +18,13 @@ import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.
|
||||
})
|
||||
export class BlockComponent implements OnInit, OnDestroy {
|
||||
network = '';
|
||||
block: Block;
|
||||
block: BlockExtended;
|
||||
blockHeight: number;
|
||||
nextBlockHeight: number;
|
||||
blockHash: string;
|
||||
isLoadingBlock = true;
|
||||
latestBlock: Block;
|
||||
latestBlocks: Block[] = [];
|
||||
latestBlock: BlockExtended;
|
||||
latestBlocks: BlockExtended[] = [];
|
||||
transactions: Transaction[];
|
||||
isLoadingTransactions = true;
|
||||
error: any;
|
||||
@@ -76,7 +77,9 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
|
||||
if (block.id === this.blockHash) {
|
||||
this.block = block;
|
||||
this.fees = block.reward / 100000000 - this.blockSubsidy;
|
||||
if (block?.extras?.reward != undefined) {
|
||||
this.fees = block.extras.reward / 100000000 - this.blockSubsidy;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -108,7 +111,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
} else {
|
||||
this.isLoadingBlock = true;
|
||||
|
||||
let blockInCache: Block;
|
||||
let blockInCache: BlockExtended;
|
||||
if (isBlockHeight) {
|
||||
blockInCache = this.latestBlocks.find((block) => block.height === parseInt(blockHash, 10));
|
||||
if (blockInCache) {
|
||||
@@ -134,7 +137,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
return this.electrsApiService.getBlock$(blockHash);
|
||||
}
|
||||
}),
|
||||
tap((block: Block) => {
|
||||
tap((block: BlockExtended) => {
|
||||
this.block = block;
|
||||
this.blockHeight = block.height;
|
||||
this.nextBlockHeight = block.height + 1;
|
||||
@@ -142,12 +145,10 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
|
||||
this.seoService.setTitle($localize`:@@block.component.browser-title:Block ${block.height}:BLOCK_HEIGHT:: ${block.id}:BLOCK_ID:`);
|
||||
this.isLoadingBlock = false;
|
||||
if (block.coinbaseTx) {
|
||||
this.coinbaseTx = block.coinbaseTx;
|
||||
}
|
||||
this.coinbaseTx = block?.extras?.coinbaseTx;
|
||||
this.setBlockSubsidy();
|
||||
if (block.reward !== undefined) {
|
||||
this.fees = block.reward / 100000000 - this.blockSubsidy;
|
||||
if (block?.extras?.reward !== undefined) {
|
||||
this.fees = block.extras.reward / 100000000 - this.blockSubsidy;
|
||||
}
|
||||
this.stateService.markBlock$.next({ blockHeight: this.blockHeight });
|
||||
this.isLoadingTransactions = true;
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
</div>
|
||||
<div class="block-body">
|
||||
<div class="fees">
|
||||
~{{ block.medianFee | number:feeRounding }} <ng-container i18n="shared.sat-vbyte|sat/vB">sat/vB</ng-container>
|
||||
~{{ block?.extras?.medianFee | number:feeRounding }} <ng-container i18n="shared.sat-vbyte|sat/vB">sat/vB</ng-container>
|
||||
</div>
|
||||
<div class="fee-span">
|
||||
{{ block.feeRange[1] | number:feeRounding }} - {{ block.feeRange[block.feeRange.length - 1] | number:feeRounding }} <ng-container i18n="shared.sat-vbyte|sat/vB">sat/vB</ng-container>
|
||||
{{ block?.extras?.feeRange[1] | number:feeRounding }} - {{ block?.extras?.feeRange[block?.extras?.feeRange.length - 1] | number:feeRounding }} <ng-container i18n="shared.sat-vbyte|sat/vB">sat/vB</ng-container>
|
||||
</div>
|
||||
<div class="block-size" [innerHTML]="'‎' + (block.size | bytes: 2)"></div>
|
||||
<div class="transaction-count">
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
|
||||
import { Observable, Subscription } from 'rxjs';
|
||||
import { Block } from 'src/app/interfaces/electrs.interface';
|
||||
import { StateService } from 'src/app/services/state.service';
|
||||
import { Router } from '@angular/router';
|
||||
import { specialBlocks } from 'src/app/app.constants';
|
||||
import { BlockExtended } from 'src/app/interfaces/node-api.interface';
|
||||
|
||||
@Component({
|
||||
selector: 'app-blockchain-blocks',
|
||||
@@ -14,8 +14,8 @@ import { specialBlocks } from 'src/app/app.constants';
|
||||
export class BlockchainBlocksComponent implements OnInit, OnDestroy {
|
||||
specialBlocks = specialBlocks;
|
||||
network = '';
|
||||
blocks: Block[] = [];
|
||||
emptyBlocks: Block[] = this.mountEmptyBlocks();
|
||||
blocks: BlockExtended[] = [];
|
||||
emptyBlocks: BlockExtended[] = this.mountEmptyBlocks();
|
||||
markHeight: number;
|
||||
blocksSubscription: Subscription;
|
||||
networkSubscription: Subscription;
|
||||
@@ -69,8 +69,8 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy {
|
||||
this.blocks.unshift(block);
|
||||
this.blocks = this.blocks.slice(0, this.stateService.env.KEEP_BLOCKS_AMOUNT);
|
||||
|
||||
if (this.blocksFilled && !this.tabHidden) {
|
||||
block.stage = block.matchRate >= 66 ? 1 : 2;
|
||||
if (this.blocksFilled && !this.tabHidden && block.extras) {
|
||||
block.extras.stage = block.extras.matchRate >= 66 ? 1 : 2;
|
||||
}
|
||||
|
||||
if (txConfirmed) {
|
||||
@@ -143,16 +143,16 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
trackByBlocksFn(index: number, item: Block) {
|
||||
trackByBlocksFn(index: number, item: BlockExtended) {
|
||||
return item.height;
|
||||
}
|
||||
|
||||
getStyleForBlock(block: Block) {
|
||||
getStyleForBlock(block: BlockExtended) {
|
||||
const greenBackgroundHeight = 100 - (block.weight / this.stateService.env.BLOCK_WEIGHT_UNITS) * 100;
|
||||
let addLeft = 0;
|
||||
|
||||
if (block.stage === 1) {
|
||||
block.stage = 2;
|
||||
if (block?.extras?.stage === 1) {
|
||||
block.extras.stage = 2;
|
||||
addLeft = -205;
|
||||
}
|
||||
|
||||
@@ -167,11 +167,11 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy {
|
||||
};
|
||||
}
|
||||
|
||||
getStyleForEmptyBlock(block: Block) {
|
||||
getStyleForEmptyBlock(block: BlockExtended) {
|
||||
let addLeft = 0;
|
||||
|
||||
if (block.stage === 1) {
|
||||
block.stage = 2;
|
||||
if (block?.extras?.stage === 1) {
|
||||
block.extras.stage = 2;
|
||||
addLeft = -205;
|
||||
}
|
||||
|
||||
|
||||
@@ -153,7 +153,7 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
|
||||
|
||||
this.blockSubscription = this.stateService.blocks$
|
||||
.subscribe(([block]) => {
|
||||
if (block.matchRate >= 66 && !this.tabHidden) {
|
||||
if (block?.extras?.matchRate >= 66 && !this.tabHidden) {
|
||||
this.blockIndex++;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -105,11 +105,11 @@ export class SearchFormComponent implements OnInit {
|
||||
const matches = this.regexTransaction.exec(searchText);
|
||||
if (this.network === 'liquid' || this.network === 'liquidtestnet') {
|
||||
if (this.assets[matches[1]]) {
|
||||
this.navigate('/asset/', matches[1]);
|
||||
this.navigate('/assets/asset/', matches[1]);
|
||||
}
|
||||
this.electrsApiService.getAsset$(matches[1])
|
||||
.subscribe(
|
||||
() => { this.navigate('/asset/', matches[1]); },
|
||||
() => { this.navigate('/assets/asset/', matches[1]); },
|
||||
() => {
|
||||
this.electrsApiService.getBlock$(matches[1])
|
||||
.subscribe(
|
||||
|
||||
@@ -9,14 +9,14 @@ import {
|
||||
delay,
|
||||
map
|
||||
} from 'rxjs/operators';
|
||||
import { Transaction, Block } from '../../interfaces/electrs.interface';
|
||||
import { Transaction } from '../../interfaces/electrs.interface';
|
||||
import { of, merge, Subscription, Observable, Subject, timer, combineLatest, from } from 'rxjs';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { WebsocketService } from '../../services/websocket.service';
|
||||
import { AudioService } from 'src/app/services/audio.service';
|
||||
import { ApiService } from 'src/app/services/api.service';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { CpfpInfo } from 'src/app/interfaces/node-api.interface';
|
||||
import { BlockExtended, CpfpInfo } from 'src/app/interfaces/node-api.interface';
|
||||
import { LiquidUnblinding } from './liquid-ublinding';
|
||||
|
||||
@Component({
|
||||
@@ -33,7 +33,7 @@ export class TransactionComponent implements OnInit, OnDestroy {
|
||||
error: any = undefined;
|
||||
errorUnblinded: any = undefined;
|
||||
waitingForTransaction = false;
|
||||
latestBlock: Block;
|
||||
latestBlock: BlockExtended;
|
||||
transactionTime = -1;
|
||||
subscription: Subscription;
|
||||
fetchCpfpSubscription: Subscription;
|
||||
|
||||
@@ -274,5 +274,5 @@
|
||||
<br />
|
||||
{{ assetsMinimal[item.asset][0] }}
|
||||
<br />
|
||||
<a [routerLink]="['/asset/' | relativeUrl, item.asset]">{{ item.asset | shortenString : 13 }}</a>
|
||||
<a [routerLink]="['/assets/asset/' | relativeUrl, item.asset]">{{ item.asset | shortenString : 13 }}</a>
|
||||
</ng-template>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Component, OnInit, Input, ChangeDetectionStrategy, OnChanges, ChangeDetectorRef, Output, EventEmitter } from '@angular/core';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { Observable, forkJoin } from 'rxjs';
|
||||
import { Block, Outspend, Transaction } from '../../interfaces/electrs.interface';
|
||||
import { Outspend, Transaction } from '../../interfaces/electrs.interface';
|
||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
||||
import { environment } from 'src/environments/environment';
|
||||
import { AssetsService } from 'src/app/services/assets.service';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { BlockExtended } from 'src/app/interfaces/node-api.interface';
|
||||
|
||||
@Component({
|
||||
selector: 'app-transactions-list',
|
||||
@@ -26,7 +27,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
||||
|
||||
@Output() loadMore = new EventEmitter();
|
||||
|
||||
latestBlock$: Observable<Block>;
|
||||
latestBlock$: Observable<BlockExtended>;
|
||||
outspends: Outspend[] = [];
|
||||
assetsMinimal: any;
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Component, ChangeDetectionStrategy, OnChanges, Input, OnInit, ChangeDetectorRef, OnDestroy } from '@angular/core';
|
||||
import { Transaction, Block } from 'src/app/interfaces/electrs.interface';
|
||||
import { Transaction } from 'src/app/interfaces/electrs.interface';
|
||||
import { StateService } from 'src/app/services/state.service';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { BlockExtended } from 'src/app/interfaces/node-api.interface';
|
||||
|
||||
@Component({
|
||||
selector: 'app-tx-fee-rating',
|
||||
@@ -18,7 +19,7 @@ export class TxFeeRatingComponent implements OnInit, OnChanges, OnDestroy {
|
||||
overpaidTimes: number;
|
||||
feeRating: number;
|
||||
|
||||
blocks: Block[] = [];
|
||||
blocks: BlockExtended[] = [];
|
||||
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
@@ -28,7 +29,7 @@ export class TxFeeRatingComponent implements OnInit, OnChanges, OnDestroy {
|
||||
ngOnInit() {
|
||||
this.blocksSubscription = this.stateService.blocks$.subscribe(([block]) => {
|
||||
this.blocks.push(block);
|
||||
if (this.tx.status.confirmed && this.tx.status.block_height === block.height && block.medianFee > 0) {
|
||||
if (this.tx.status.confirmed && this.tx.status.block_height === block.height && block?.extras?.medianFee > 0) {
|
||||
this.calculateRatings(block);
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
@@ -42,7 +43,7 @@ export class TxFeeRatingComponent implements OnInit, OnChanges, OnDestroy {
|
||||
}
|
||||
|
||||
const foundBlock = this.blocks.find((b) => b.height === this.tx.status.block_height);
|
||||
if (foundBlock && foundBlock.medianFee > 0) {
|
||||
if (foundBlock && foundBlock?.extras?.medianFee > 0) {
|
||||
this.calculateRatings(foundBlock);
|
||||
}
|
||||
}
|
||||
@@ -51,9 +52,9 @@ export class TxFeeRatingComponent implements OnInit, OnChanges, OnDestroy {
|
||||
this.blocksSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
calculateRatings(block: Block) {
|
||||
calculateRatings(block: BlockExtended) {
|
||||
const feePervByte = this.tx.effectiveFeePerVsize || this.tx.fee / (this.tx.weight / 4);
|
||||
this.medianFeeNeeded = block.medianFee;
|
||||
this.medianFeeNeeded = block?.extras?.medianFee;
|
||||
|
||||
// Block not filled
|
||||
if (block.weight < this.stateService.env.BLOCK_WEIGHT_UNITS * 0.95) {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { ChangeDetectionStrategy, Component, Inject, LOCALE_ID, OnInit } from '@angular/core';
|
||||
import { combineLatest, merge, Observable, of, timer } from 'rxjs';
|
||||
import { filter, map, scan, share, switchMap, tap } from 'rxjs/operators';
|
||||
import { Block } from '../interfaces/electrs.interface';
|
||||
import { OptimizedMempoolStats } from '../interfaces/node-api.interface';
|
||||
import { BlockExtended, OptimizedMempoolStats } from '../interfaces/node-api.interface';
|
||||
import { MempoolInfo, TransactionStripped } from '../interfaces/websocket.interface';
|
||||
import { ApiService } from '../services/api.service';
|
||||
import { StateService } from '../services/state.service';
|
||||
@@ -40,7 +39,7 @@ export class DashboardComponent implements OnInit {
|
||||
mempoolInfoData$: Observable<MempoolInfoData>;
|
||||
mempoolLoadingStatus$: Observable<number>;
|
||||
vBytesPerSecondLimit = 1667;
|
||||
blocks$: Observable<Block[]>;
|
||||
blocks$: Observable<BlockExtended[]>;
|
||||
transactions$: Observable<TransactionStripped[]>;
|
||||
latestBlockHeight: number;
|
||||
mempoolTransactionsWeightPerSecondData: any;
|
||||
@@ -199,7 +198,7 @@ export class DashboardComponent implements OnInit {
|
||||
};
|
||||
}
|
||||
|
||||
trackByBlock(index: number, block: Block) {
|
||||
trackByBlock(index: number, block: BlockExtended) {
|
||||
return block.height;
|
||||
}
|
||||
|
||||
|
||||
@@ -107,14 +107,6 @@ export interface Block {
|
||||
size: number;
|
||||
weight: number;
|
||||
previousblockhash: string;
|
||||
|
||||
// Custom properties
|
||||
medianFee?: number;
|
||||
feeRange?: number[];
|
||||
reward?: number;
|
||||
coinbaseTx?: Transaction;
|
||||
matchRate: number;
|
||||
stage: number;
|
||||
}
|
||||
|
||||
export interface Address {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Block, Transaction } from "./electrs.interface";
|
||||
|
||||
export interface OptimizedMempoolStats {
|
||||
added: number;
|
||||
vbytes_per_second: number;
|
||||
@@ -80,3 +82,17 @@ export interface MiningStats {
|
||||
pools: SinglePoolStats[],
|
||||
}
|
||||
|
||||
export interface BlockExtension {
|
||||
medianFee?: number;
|
||||
feeRange?: number[];
|
||||
reward?: number;
|
||||
coinbaseTx?: Transaction;
|
||||
matchRate?: number;
|
||||
|
||||
stage?: number; // Frontend only
|
||||
}
|
||||
|
||||
export interface BlockExtended extends Block {
|
||||
extras?: BlockExtension;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { ILoadingIndicators } from '../services/state.service';
|
||||
import { Block, Transaction } from './electrs.interface';
|
||||
import { Transaction } from './electrs.interface';
|
||||
import { BlockExtended } from './node-api.interface';
|
||||
|
||||
export interface WebsocketResponse {
|
||||
block?: Block;
|
||||
blocks?: Block[];
|
||||
block?: BlockExtended;
|
||||
blocks?: BlockExtended[];
|
||||
conversions?: any;
|
||||
txConfirmed?: boolean;
|
||||
historicalDate?: string;
|
||||
|
||||
@@ -117,6 +117,14 @@ export class ApiService {
|
||||
return this.httpClient.get<LiquidPegs[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/pegs/month');
|
||||
}
|
||||
|
||||
listFeaturedAssets$(): Observable<any[]> {
|
||||
return this.httpClient.get<any[]>(this.apiBaseUrl + '/api/v1/assets/featured');
|
||||
}
|
||||
|
||||
getAssetGroup$(id: string): Observable<any> {
|
||||
return this.httpClient.get<any[]>(this.apiBaseUrl + '/api/v1/assets/group/' + id);
|
||||
}
|
||||
|
||||
postTransaction$(hexPayload: string): Observable<any> {
|
||||
return this.httpClient.post<any>(this.apiBaseUrl + this.apiBasePath + '/api/tx', hexPayload, { responseType: 'text' as 'json'});
|
||||
}
|
||||
|
||||
@@ -3,12 +3,16 @@ import { HttpClient } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map, shareReplay, switchMap } from 'rxjs/operators';
|
||||
import { StateService } from './state.service';
|
||||
import { environment } from 'src/environments/environment';
|
||||
import { AssetExtended } from '../interfaces/electrs.interface';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AssetsService {
|
||||
getAssetsJson$: Observable<any>;
|
||||
nativeAssetId = this.stateService.network === 'liquidtestnet' ? environment.nativeTestAssetId : environment.nativeAssetId;
|
||||
|
||||
getAssetsJson$: Observable<{ array: AssetExtended[]; objects: any}>;
|
||||
getAssetsMinimalJson$: Observable<any>;
|
||||
getMiningPools$: Observable<any>;
|
||||
|
||||
@@ -24,6 +28,30 @@ export class AssetsService {
|
||||
this.getAssetsJson$ = this.stateService.networkChanged$
|
||||
.pipe(
|
||||
switchMap(() => this.httpClient.get(`${apiBaseUrl}/resources/assets${this.stateService.network === 'liquidtestnet' ? '-testnet' : ''}.json`)),
|
||||
map((rawAssets) => {
|
||||
const assets: AssetExtended[] = Object.values(rawAssets);
|
||||
|
||||
if (this.stateService.network === 'liquid') {
|
||||
// @ts-ignore
|
||||
assets.push({
|
||||
name: 'Liquid Bitcoin',
|
||||
ticker: 'L-BTC',
|
||||
asset_id: this.nativeAssetId,
|
||||
});
|
||||
} else if (this.stateService.network === 'liquidtestnet') {
|
||||
// @ts-ignore
|
||||
assets.push({
|
||||
name: 'Test Liquid Bitcoin',
|
||||
ticker: 'tL-BTC',
|
||||
asset_id: this.nativeAssetId,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
objects: rawAssets,
|
||||
array: assets.sort((a: any, b: any) => a.name.localeCompare(b.name)),
|
||||
};
|
||||
}),
|
||||
shareReplay(1),
|
||||
);
|
||||
this.getAssetsMinimalJson$ = this.stateService.networkChanged$
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { Block, Transaction, Address, Outspend, Recent, Asset } from '../interfaces/electrs.interface';
|
||||
import { Transaction, Address, Outspend, Recent, Asset } from '../interfaces/electrs.interface';
|
||||
import { StateService } from './state.service';
|
||||
import { BlockExtended } from '../interfaces/node-api.interface';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
@@ -28,12 +29,12 @@ export class ElectrsApiService {
|
||||
});
|
||||
}
|
||||
|
||||
getBlock$(hash: string): Observable<Block> {
|
||||
return this.httpClient.get<Block>(this.apiBaseUrl + this.apiBasePath + '/api/block/' + hash);
|
||||
getBlock$(hash: string): Observable<BlockExtended> {
|
||||
return this.httpClient.get<BlockExtended>(this.apiBaseUrl + this.apiBasePath + '/api/block/' + hash);
|
||||
}
|
||||
|
||||
listBlocks$(height?: number): Observable<Block[]> {
|
||||
return this.httpClient.get<Block[]>(this.apiBaseUrl + this.apiBasePath + '/api/blocks/' + (height || ''));
|
||||
listBlocks$(height?: number): Observable<BlockExtended[]> {
|
||||
return this.httpClient.get<BlockExtended[]>(this.apiBaseUrl + this.apiBasePath + '/api/blocks/' + (height || ''));
|
||||
}
|
||||
|
||||
getTransaction$(txId: string): Observable<Transaction> {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Inject, Injectable, PLATFORM_ID } from '@angular/core';
|
||||
import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable } from 'rxjs';
|
||||
import { Block, Transaction } from '../interfaces/electrs.interface';
|
||||
import { Transaction } from '../interfaces/electrs.interface';
|
||||
import { IBackendInfo, MempoolBlock, MempoolInfo, TransactionStripped } from '../interfaces/websocket.interface';
|
||||
import { OptimizedMempoolStats } from '../interfaces/node-api.interface';
|
||||
import { BlockExtended, OptimizedMempoolStats } from '../interfaces/node-api.interface';
|
||||
import { Router, NavigationStart } from '@angular/router';
|
||||
import { isPlatformBrowser } from '@angular/common';
|
||||
import { map, shareReplay } from 'rxjs/operators';
|
||||
@@ -72,7 +72,7 @@ export class StateService {
|
||||
latestBlockHeight = 0;
|
||||
|
||||
networkChanged$ = new ReplaySubject<string>(1);
|
||||
blocks$: ReplaySubject<[Block, boolean]>;
|
||||
blocks$: ReplaySubject<[BlockExtended, boolean]>;
|
||||
transactions$ = new ReplaySubject<TransactionStripped>(6);
|
||||
conversions$ = new ReplaySubject<any>(1);
|
||||
bsqPrice$ = new ReplaySubject<number>(1);
|
||||
@@ -122,7 +122,7 @@ export class StateService {
|
||||
}
|
||||
});
|
||||
|
||||
this.blocks$ = new ReplaySubject<[Block, boolean]>(this.env.KEEP_BLOCKS_AMOUNT);
|
||||
this.blocks$ = new ReplaySubject<[BlockExtended, boolean]>(this.env.KEEP_BLOCKS_AMOUNT);
|
||||
|
||||
if (this.env.BASE_MODULE === 'bisq') {
|
||||
this.network = this.env.BASE_MODULE;
|
||||
|
||||
@@ -2,11 +2,12 @@ import { Injectable } from '@angular/core';
|
||||
import { webSocket, WebSocketSubject } from 'rxjs/webSocket';
|
||||
import { WebsocketResponse, IBackendInfo } from '../interfaces/websocket.interface';
|
||||
import { StateService } from './state.service';
|
||||
import { Block, Transaction } from '../interfaces/electrs.interface';
|
||||
import { Transaction } from '../interfaces/electrs.interface';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { ApiService } from './api.service';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { TransferState, makeStateKey } from '@angular/platform-browser';
|
||||
import { BlockExtended } from '../interfaces/node-api.interface';
|
||||
|
||||
const OFFLINE_RETRY_AFTER_MS = 10000;
|
||||
const OFFLINE_PING_CHECK_AFTER_MS = 30000;
|
||||
@@ -207,7 +208,7 @@ export class WebsocketService {
|
||||
handleResponse(response: WebsocketResponse) {
|
||||
if (response.blocks && response.blocks.length) {
|
||||
const blocks = response.blocks;
|
||||
blocks.forEach((block: Block) => {
|
||||
blocks.forEach((block: BlockExtended) => {
|
||||
if (block.height > this.stateService.latestBlockHeight) {
|
||||
this.stateService.latestBlockHeight = block.height;
|
||||
this.stateService.blocks$.next([block, false]);
|
||||
|
||||
Reference in New Issue
Block a user