mirror of
https://github.com/mempool/mempool.git
synced 2024-11-20 10:21:52 +01:00
Basic Liquid Asset support.
This commit is contained in:
parent
7e7b536acb
commit
b2d2fd225c
@ -10,6 +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 { LatestBlocksComponent } from './components/latest-blocks/latest-blocks.component';
|
||||
import { AssetComponent } from './components/asset/asset.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@ -36,6 +37,10 @@ const routes: Routes = [
|
||||
path: 'mempool-block/:id',
|
||||
component: MempoolBlockComponent
|
||||
},
|
||||
{
|
||||
path: 'asset/:id',
|
||||
component: AssetComponent
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@ -45,6 +45,8 @@ import { FeeDistributionGraphComponent } from './components/fee-distribution-gra
|
||||
import { TimespanComponent } from './components/timespan/timespan.component';
|
||||
import { SeoService } from './services/seo.service';
|
||||
import { MempoolGraphComponent } from './components/mempool-graph/mempool-graph.component';
|
||||
import { AssetComponent } from './components/asset/asset.component';
|
||||
import { ScriptpubkeyTypePipe } from './pipes/scriptpubkey-type-pipe/scriptpubkey-type.pipe';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@ -80,6 +82,8 @@ import { MempoolGraphComponent } from './components/mempool-graph/mempool-graph.
|
||||
MempoolBlockComponent,
|
||||
FeeDistributionGraphComponent,
|
||||
MempoolGraphComponent,
|
||||
AssetComponent,
|
||||
ScriptpubkeyTypePipe,
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
|
124
frontend/src/app/components/asset/asset.component.html
Normal file
124
frontend/src/app/components/asset/asset.component.html
Normal file
@ -0,0 +1,124 @@
|
||||
<div class="container-xl">
|
||||
<h1 style="float: left;">Asset</h1>
|
||||
<a [routerLink]="['/asset/', assetString]" style="line-height: 56px; margin-left: 10px;">
|
||||
<span class="d-inline d-lg-none">{{ assetString | shortenString : 24 }}</span>
|
||||
<span class="d-none d-lg-inline">{{ assetString }}</span>
|
||||
</a>
|
||||
<app-clipboard [text]="assetString"></app-clipboard>
|
||||
<br>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<ng-template [ngIf]="!isLoadingAsset && !error">
|
||||
<div class="box">
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>{{ asset.name }} ({{ asset.ticker }})</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Precision</td>
|
||||
<td>{{ asset.precision }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Issuer</td>
|
||||
<td><a target="_blank" href="{{ 'http://' + asset.contract.entity.domain }}">{{ asset.contract.entity.domain }}</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Issuance tx</td>
|
||||
<td><a [routerLink]="['/tx/', asset.issuance_txin.txid]">{{ asset.issuance_txin.txid | shortenString : 13 }}</a> <app-clipboard class="d-none d-sm-inline-block" [text]="asset.issuance_txin.txid"></app-clipboard></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Circulating amount</td>
|
||||
<td>{{ (asset.chain_stats.issued_amount - asset.chain_stats.burned_amount) / 100000000 | number: '1.0-' + asset.precision }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Issued amount</td>
|
||||
<td>{{ asset.chain_stats.issued_amount / 100000000 | number: '1.0-' + asset.precision }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Burned amount</td>
|
||||
<td>{{ asset.chain_stats.burned_amount / 100000000 | number: '1.0-' + asset.precision }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<h2><ng-template [ngIf]="transactions?.length">{{ (transactions?.length | number) || '?' }} of </ng-template>{{ txCount | number }} transactions</h2>
|
||||
|
||||
<app-transactions-list [transactions]="transactions" [showConfirmations]="true" (loadMore)="loadMore()"></app-transactions-list>
|
||||
|
||||
<div class="text-center">
|
||||
<ng-template [ngIf]="isLoadingTransactions">
|
||||
<div class="header-bg box" style="padding: 10px; margin-bottom: 10px;">
|
||||
<span class="skeleton-loader"></span>
|
||||
</div>
|
||||
|
||||
<div class="header-bg box">
|
||||
<div class="row" style="height: 107px;">
|
||||
<div class="col-sm">
|
||||
<span class="skeleton-loader"></span>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<span class="skeleton-loader"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
||||
</ng-template>
|
||||
|
||||
<ng-template [ngIf]="isLoadingAsset && !error">
|
||||
|
||||
<div class="box">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colspan="2"><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2"><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2"><span class="skeleton-loader"></span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</ng-template>
|
||||
|
||||
<ng-template [ngIf]="error">
|
||||
<div class="text-center">
|
||||
Error loading asset data.
|
||||
<br>
|
||||
<i>{{ error.error }}</i>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
</div>
|
||||
|
||||
<br>
|
23
frontend/src/app/components/asset/asset.component.scss
Normal file
23
frontend/src/app/components/asset/asset.component.scss
Normal file
@ -0,0 +1,23 @@
|
||||
.qr-wrapper {
|
||||
background-color: #FFF;
|
||||
padding: 10px;
|
||||
padding-bottom: 5px;
|
||||
display: inline-block;
|
||||
margin-right: 25px;
|
||||
}
|
||||
|
||||
@media (min-width: 576px) {
|
||||
.qrcode-col {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
@media (max-width: 575.98px) {
|
||||
.qrcode-col {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.qrcode-col > div {
|
||||
margin-top: 20px;
|
||||
margin-right: 0px;
|
||||
}
|
||||
}
|
198
frontend/src/app/components/asset/asset.component.ts
Normal file
198
frontend/src/app/components/asset/asset.component.ts
Normal file
@ -0,0 +1,198 @@
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { ActivatedRoute, ParamMap } from '@angular/router';
|
||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
||||
import { switchMap, filter, catchError } from 'rxjs/operators';
|
||||
import { Asset, Transaction } from '../../interfaces/electrs.interface';
|
||||
import { WebsocketService } from 'src/app/services/websocket.service';
|
||||
import { StateService } from 'src/app/services/state.service';
|
||||
import { AudioService } from 'src/app/services/audio.service';
|
||||
import { ApiService } from 'src/app/services/api.service';
|
||||
import { of, merge, Subscription } from 'rxjs';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { environment } from 'src/environments/environment';
|
||||
|
||||
@Component({
|
||||
selector: 'app-asset',
|
||||
templateUrl: './asset.component.html',
|
||||
styleUrls: ['./asset.component.scss']
|
||||
})
|
||||
export class AssetComponent implements OnInit, OnDestroy {
|
||||
network = environment.network;
|
||||
|
||||
asset: Asset;
|
||||
assetString: string;
|
||||
isLoadingAsset = true;
|
||||
transactions: Transaction[];
|
||||
isLoadingTransactions = true;
|
||||
error: any;
|
||||
mainSubscription: Subscription;
|
||||
|
||||
totalConfirmedTxCount = 0;
|
||||
loadedConfirmedTxCount = 0;
|
||||
txCount = 0;
|
||||
receieved = 0;
|
||||
sent = 0;
|
||||
|
||||
private tempTransactions: Transaction[];
|
||||
private timeTxIndexes: number[];
|
||||
private lastTransactionTxId: string;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private electrsApiService: ElectrsApiService,
|
||||
private websocketService: WebsocketService,
|
||||
private stateService: StateService,
|
||||
private audioService: AudioService,
|
||||
private apiService: ApiService,
|
||||
private seoService: SeoService,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.websocketService.want(['blocks', 'stats', 'mempool-blocks']);
|
||||
|
||||
this.mainSubscription = this.route.paramMap
|
||||
.pipe(
|
||||
switchMap((params: ParamMap) => {
|
||||
this.error = undefined;
|
||||
this.isLoadingAsset = true;
|
||||
this.loadedConfirmedTxCount = 0;
|
||||
this.asset = null;
|
||||
this.isLoadingTransactions = true;
|
||||
this.transactions = null;
|
||||
document.body.scrollTo(0, 0);
|
||||
this.assetString = params.get('id') || '';
|
||||
this.seoService.setTitle('Asset: ' + this.assetString, true);
|
||||
|
||||
return merge(
|
||||
of(true),
|
||||
this.stateService.connectionState$
|
||||
.pipe(filter((state) => state === 2 && this.transactions && this.transactions.length > 0))
|
||||
)
|
||||
.pipe(
|
||||
switchMap(() => this.electrsApiService.getAsset$(this.assetString)
|
||||
.pipe(
|
||||
catchError((err) => {
|
||||
this.isLoadingAsset = false;
|
||||
this.error = err;
|
||||
console.log(err);
|
||||
return of(null);
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
})
|
||||
)
|
||||
.pipe(
|
||||
switchMap((asset: Asset) => {
|
||||
this.asset = asset;
|
||||
this.updateChainStats();
|
||||
this.websocketService.startTrackAsset(asset.asset_id);
|
||||
this.isLoadingAsset = false;
|
||||
this.isLoadingTransactions = true;
|
||||
return this.electrsApiService.getAssetTransactions$(asset.asset_id);
|
||||
}),
|
||||
switchMap((transactions) => {
|
||||
this.tempTransactions = transactions;
|
||||
if (transactions.length) {
|
||||
this.lastTransactionTxId = transactions[transactions.length - 1].txid;
|
||||
this.loadedConfirmedTxCount += transactions.filter((tx) => tx.status.confirmed).length;
|
||||
}
|
||||
|
||||
const fetchTxs: string[] = [];
|
||||
this.timeTxIndexes = [];
|
||||
transactions.forEach((tx, index) => {
|
||||
if (!tx.status.confirmed) {
|
||||
fetchTxs.push(tx.txid);
|
||||
this.timeTxIndexes.push(index);
|
||||
}
|
||||
});
|
||||
if (!fetchTxs.length) {
|
||||
return of([]);
|
||||
}
|
||||
return this.apiService.getTransactionTimes$(fetchTxs);
|
||||
})
|
||||
)
|
||||
.subscribe((times: number[]) => {
|
||||
times.forEach((time, index) => {
|
||||
this.tempTransactions[this.timeTxIndexes[index]].firstSeen = time;
|
||||
});
|
||||
this.tempTransactions.sort((a, b) => {
|
||||
return b.status.block_time - a.status.block_time || b.firstSeen - a.firstSeen;
|
||||
});
|
||||
|
||||
this.transactions = this.tempTransactions;
|
||||
this.isLoadingTransactions = false;
|
||||
},
|
||||
(error) => {
|
||||
console.log(error);
|
||||
this.error = error;
|
||||
this.isLoadingAsset = false;
|
||||
});
|
||||
|
||||
this.stateService.mempoolTransactions$
|
||||
.subscribe((transaction) => {
|
||||
if (this.transactions.some((t) => t.txid === transaction.txid)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.transactions.unshift(transaction);
|
||||
this.transactions = this.transactions.slice();
|
||||
this.txCount++;
|
||||
|
||||
// if (transaction.vout.some((vout) => vout.scriptpubkey_asset === this.asset.asset)) {
|
||||
// this.audioService.playSound('cha-ching');
|
||||
// } else {
|
||||
// this.audioService.playSound('chime');
|
||||
// }
|
||||
|
||||
// transaction.vin.forEach((vin) => {
|
||||
// if (vin.prevout.scriptpubkey_asset === this.asset.asset) {
|
||||
// this.sent += vin.prevout.value;
|
||||
// }
|
||||
// });
|
||||
// transaction.vout.forEach((vout) => {
|
||||
// if (vout.scriptpubkey_asset === this.asset.asset) {
|
||||
// this.receieved += vout.value;
|
||||
// }
|
||||
// });
|
||||
});
|
||||
|
||||
this.stateService.blockTransactions$
|
||||
.subscribe((transaction) => {
|
||||
const tx = this.transactions.find((t) => t.txid === transaction.txid);
|
||||
if (tx) {
|
||||
tx.status = transaction.status;
|
||||
this.transactions = this.transactions.slice();
|
||||
this.audioService.playSound('magic');
|
||||
}
|
||||
this.totalConfirmedTxCount++;
|
||||
this.loadedConfirmedTxCount++;
|
||||
});
|
||||
}
|
||||
|
||||
loadMore() {
|
||||
if (this.isLoadingTransactions || !this.totalConfirmedTxCount || this.loadedConfirmedTxCount >= this.totalConfirmedTxCount) {
|
||||
return;
|
||||
}
|
||||
this.isLoadingTransactions = true;
|
||||
this.electrsApiService.getAddressTransactionsFromHash$(this.asset.asset_id, this.lastTransactionTxId)
|
||||
.subscribe((transactions: Transaction[]) => {
|
||||
this.lastTransactionTxId = transactions[transactions.length - 1].txid;
|
||||
this.loadedConfirmedTxCount += transactions.length;
|
||||
this.transactions = this.transactions.concat(transactions);
|
||||
this.isLoadingTransactions = false;
|
||||
});
|
||||
}
|
||||
|
||||
updateChainStats() {
|
||||
// this.receieved = this.asset.chain_stats.funded_txo_sum + this.asset.mempool_stats.funded_txo_sum;
|
||||
// this.sent = this.asset.chain_stats.spent_txo_sum + this.asset.mempool_stats.spent_txo_sum;
|
||||
this.txCount = this.asset.chain_stats.tx_count + this.asset.mempool_stats.tx_count;
|
||||
// this.totalConfirmedTxCount = this.asset.chain_stats.tx_count;
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.mainSubscription.unsubscribe();
|
||||
this.websocketService.stopTrackingAsset();
|
||||
}
|
||||
}
|
@ -66,7 +66,7 @@
|
||||
<span class="d-none d-lg-block">{{ vout.scriptpubkey_address | shortenString : 42 }}</span>
|
||||
</a>
|
||||
<ng-template #scriptpubkey_type>
|
||||
OP_RETURN
|
||||
{{ vout.scriptpubkey_type | scriptpubkeyType }}
|
||||
</ng-template>
|
||||
</td>
|
||||
<td class="text-right nowrap">
|
||||
|
@ -98,3 +98,74 @@ export interface Outspend {
|
||||
vin: number;
|
||||
status: Status;
|
||||
}
|
||||
|
||||
export interface Asset {
|
||||
asset_id: string;
|
||||
issuance_txin: IssuanceTxin;
|
||||
issuance_prevout: IssuancePrevout;
|
||||
reissuance_token: string;
|
||||
contract_hash: string;
|
||||
status: Status;
|
||||
chain_stats: AssetChainStats;
|
||||
mempool_stats: AssetMempoolStats;
|
||||
contract: Contract;
|
||||
entity: Entity;
|
||||
precision: number;
|
||||
name: string;
|
||||
ticker: string;
|
||||
}
|
||||
|
||||
interface IssuanceTxin {
|
||||
txid: string;
|
||||
vin: number;
|
||||
}
|
||||
|
||||
interface IssuancePrevout {
|
||||
txid: string;
|
||||
vout: number;
|
||||
}
|
||||
|
||||
interface AssetChainStats {
|
||||
tx_count: number;
|
||||
issuance_count: number;
|
||||
issued_amount: number;
|
||||
burned_amount: number;
|
||||
has_blinded_issuances: boolean;
|
||||
reissuance_tokens: number;
|
||||
burned_reissuance_tokens: number;
|
||||
|
||||
peg_in_count: number;
|
||||
peg_in_amount: number;
|
||||
peg_out_count: number;
|
||||
peg_out_amount: number;
|
||||
burn_count: number;
|
||||
}
|
||||
|
||||
interface AssetMempoolStats {
|
||||
tx_count: number;
|
||||
issuance_count: number;
|
||||
issued_amount: number;
|
||||
burned_amount: number;
|
||||
has_blinded_issuances: boolean;
|
||||
reissuance_tokens: any;
|
||||
burned_reissuance_tokens: number;
|
||||
|
||||
peg_in_count: number;
|
||||
peg_in_amount: number;
|
||||
peg_out_count: number;
|
||||
peg_out_amount: number;
|
||||
burn_count: number;
|
||||
}
|
||||
|
||||
interface Contract {
|
||||
entity: Entity;
|
||||
issuer_pubkey: string;
|
||||
name: string;
|
||||
precision: number;
|
||||
ticker: string;
|
||||
version: number;
|
||||
}
|
||||
|
||||
interface Entity {
|
||||
domain: string;
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ export interface WebsocketResponse {
|
||||
tx?: Transaction;
|
||||
'track-tx'?: string;
|
||||
'track-address'?: string;
|
||||
'track-asset'?: string;
|
||||
'watch-mempool'?: boolean;
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,18 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
|
||||
@Pipe({
|
||||
name: 'scriptpubkeyType'
|
||||
})
|
||||
export class ScriptpubkeyTypePipe implements PipeTransform {
|
||||
|
||||
transform(value: string): string {
|
||||
switch (value) {
|
||||
case 'fee':
|
||||
return 'Transaction fee';
|
||||
case 'op_return':
|
||||
default:
|
||||
return 'Script';
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { Block, Transaction, Address, Outspend, Recent } from '../interfaces/electrs.interface';
|
||||
import { Block, Transaction, Address, Outspend, Recent, Asset } from '../interfaces/electrs.interface';
|
||||
|
||||
const API_BASE_URL = document.location.protocol + '//' + document.location.hostname + ':' + document.location.port + '/electrs';
|
||||
|
||||
@ -54,4 +54,16 @@ export class ElectrsApiService {
|
||||
return this.httpClient.get<Transaction[]>(API_BASE_URL + '/address/' + address + '/txs/chain/' + txid);
|
||||
}
|
||||
|
||||
getAsset$(assetId: string): Observable<Asset> {
|
||||
return this.httpClient.get<Asset>(API_BASE_URL + '/asset/' + assetId);
|
||||
}
|
||||
|
||||
getAssetTransactions$(assetId: string): Observable<Transaction[]> {
|
||||
return this.httpClient.get<Transaction[]>(API_BASE_URL + '/asset/' + assetId + '/txs');
|
||||
}
|
||||
|
||||
getAssetTransactionsFromHash$(assetId: string, txid: string): Observable<Transaction[]> {
|
||||
return this.httpClient.get<Transaction[]>(API_BASE_URL + '/asset/' + assetId + '/txs/chain/' + txid);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -157,6 +157,14 @@ export class WebsocketService {
|
||||
this.websocketSubject.next({ 'track-address': 'stop' });
|
||||
}
|
||||
|
||||
startTrackAsset(asset: string) {
|
||||
this.websocketSubject.next({ 'track-asset': asset });
|
||||
}
|
||||
|
||||
stopTrackingAsset() {
|
||||
this.websocketSubject.next({ 'track-asset': 'stop' });
|
||||
}
|
||||
|
||||
fetchStatistics(historicalDate: string) {
|
||||
this.websocketSubject.next({ historicalDate });
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user