diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts
index db0206921..c57fc9750 100644
--- a/frontend/src/app/app-routing.module.ts
+++ b/frontend/src/app/app-routing.module.ts
@@ -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
+ },
],
},
{
diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts
index 1aa94dc82..1af136245 100644
--- a/frontend/src/app/app.module.ts
+++ b/frontend/src/app/app.module.ts
@@ -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,
diff --git a/frontend/src/app/components/asset/asset.component.html b/frontend/src/app/components/asset/asset.component.html
new file mode 100644
index 000000000..d91ff40bd
--- /dev/null
+++ b/frontend/src/app/components/asset/asset.component.html
@@ -0,0 +1,124 @@
+
+
Asset
+
+ {{ assetString | shortenString : 24 }}
+ {{ assetString }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Circulating amount |
+ {{ (asset.chain_stats.issued_amount - asset.chain_stats.burned_amount) / 100000000 | number: '1.0-' + asset.precision }} |
+
+
+ Issued amount |
+ {{ asset.chain_stats.issued_amount / 100000000 | number: '1.0-' + asset.precision }} |
+
+
+ Burned amount |
+ {{ asset.chain_stats.burned_amount / 100000000 | number: '1.0-' + asset.precision }} |
+
+
+
+
+
+
+
+
+
+
+ {{ (transactions?.length | number) || '?' }} of {{ txCount | number }} transactions
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Error loading asset data.
+
+ {{ error.error }}
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/src/app/components/asset/asset.component.scss b/frontend/src/app/components/asset/asset.component.scss
new file mode 100644
index 000000000..c5961e428
--- /dev/null
+++ b/frontend/src/app/components/asset/asset.component.scss
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/app/components/asset/asset.component.ts b/frontend/src/app/components/asset/asset.component.ts
new file mode 100644
index 000000000..e444382a8
--- /dev/null
+++ b/frontend/src/app/components/asset/asset.component.ts
@@ -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();
+ }
+}
diff --git a/frontend/src/app/components/transactions-list/transactions-list.component.html b/frontend/src/app/components/transactions-list/transactions-list.component.html
index 487c716e4..d03d065c9 100644
--- a/frontend/src/app/components/transactions-list/transactions-list.component.html
+++ b/frontend/src/app/components/transactions-list/transactions-list.component.html
@@ -66,7 +66,7 @@
{{ vout.scriptpubkey_address | shortenString : 42 }}
- OP_RETURN
+ {{ vout.scriptpubkey_type | scriptpubkeyType }}
diff --git a/frontend/src/app/interfaces/electrs.interface.ts b/frontend/src/app/interfaces/electrs.interface.ts
index 5f52d75cb..47c3a3af1 100644
--- a/frontend/src/app/interfaces/electrs.interface.ts
+++ b/frontend/src/app/interfaces/electrs.interface.ts
@@ -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;
+}
diff --git a/frontend/src/app/interfaces/websocket.interface.ts b/frontend/src/app/interfaces/websocket.interface.ts
index 15b557746..eabff1583 100644
--- a/frontend/src/app/interfaces/websocket.interface.ts
+++ b/frontend/src/app/interfaces/websocket.interface.ts
@@ -13,6 +13,7 @@ export interface WebsocketResponse {
tx?: Transaction;
'track-tx'?: string;
'track-address'?: string;
+ 'track-asset'?: string;
'watch-mempool'?: boolean;
}
diff --git a/frontend/src/app/pipes/scriptpubkey-type-pipe/scriptpubkey-type.pipe.ts b/frontend/src/app/pipes/scriptpubkey-type-pipe/scriptpubkey-type.pipe.ts
new file mode 100644
index 000000000..7c9239517
--- /dev/null
+++ b/frontend/src/app/pipes/scriptpubkey-type-pipe/scriptpubkey-type.pipe.ts
@@ -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';
+ }
+ }
+
+}
diff --git a/frontend/src/app/services/electrs-api.service.ts b/frontend/src/app/services/electrs-api.service.ts
index ae1d1a856..9896e1f15 100644
--- a/frontend/src/app/services/electrs-api.service.ts
+++ b/frontend/src/app/services/electrs-api.service.ts
@@ -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(API_BASE_URL + '/address/' + address + '/txs/chain/' + txid);
}
+ getAsset$(assetId: string): Observable {
+ return this.httpClient.get(API_BASE_URL + '/asset/' + assetId);
+ }
+
+ getAssetTransactions$(assetId: string): Observable {
+ return this.httpClient.get(API_BASE_URL + '/asset/' + assetId + '/txs');
+ }
+
+ getAssetTransactionsFromHash$(assetId: string, txid: string): Observable {
+ return this.httpClient.get(API_BASE_URL + '/asset/' + assetId + '/txs/chain/' + txid);
+ }
+
}
diff --git a/frontend/src/app/services/websocket.service.ts b/frontend/src/app/services/websocket.service.ts
index 34d14c1e0..8ba46b3bb 100644
--- a/frontend/src/app/services/websocket.service.ts
+++ b/frontend/src/app/services/websocket.service.ts
@@ -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 });
}
|