Bisq module separation.

Transaction view.
Block view.
Blocks list.
This commit is contained in:
softsimon 2020-07-13 15:16:12 +07:00
parent 60e1b9a41e
commit db2e293ce5
No known key found for this signature in database
GPG Key ID: 488D7DCFB5A430D7
25 changed files with 438 additions and 187 deletions

View File

@ -21,23 +21,13 @@ class Bisq {
clearTimeout(fsWait); clearTimeout(fsWait);
} }
fsWait = setTimeout(() => { fsWait = setTimeout(() => {
console.log(`${filename} file changed. Reloading dump file.`); console.log(`${filename} file change detected.`);
this.loadBisqDumpFile(); this.loadBisqDumpFile();
}, 1000); }, 1000);
} }
}); });
} }
async loadBisqDumpFile(): Promise<void> {
try {
const data = await this.loadData();
await this.loadBisqBlocksDump(data);
this.buildIndex();
} catch (e) {
console.log('loadBisqDumpFile() error.', e.message);
}
}
getTransaction(txId: string): BisqTransaction | undefined { getTransaction(txId: string): BisqTransaction | undefined {
return this.transactionsIndex[txId]; return this.transactionsIndex[txId];
} }
@ -46,43 +36,54 @@ class Bisq {
return [this.transactions.slice(start, length + start), this.transactions.length]; return [this.transactions.slice(start, length + start), this.transactions.length];
} }
getBlockTransactions(blockHash: string, start: number, length: number): [BisqTransaction[], number] {
const block = this.blocksIndex[blockHash];
if (!block) {
return [[], -1];
}
return [block.txs.slice(start, length + start), block.txs.length];
}
getBlock(hash: string): BisqBlock | undefined { getBlock(hash: string): BisqBlock | undefined {
console.log(hash);
console.log(this.blocksIndex[hash]);
return this.blocksIndex[hash]; return this.blocksIndex[hash];
} }
getBlocks(start: number, length: number): [BisqBlock[], number] {
return [this.blocks.slice(start, length + start), this.blocks.length];
}
private async loadBisqDumpFile(): Promise<void> {
try {
const data = await this.loadData();
await this.loadBisqBlocksDump(data);
this.buildIndex();
} catch (e) {
console.log('loadBisqDumpFile() error.', e.message);
}
}
private buildIndex() { private buildIndex() {
const start = new Date().getTime();
this.transactions = [];
this.transactionsIndex = {};
this.blocks.forEach((block) => { this.blocks.forEach((block) => {
if (this.blocksIndex[block.hash]) { if (!this.blocksIndex[block.hash]) {
return; this.blocksIndex[block.hash] = block;
} }
this.blocksIndex[block.hash] = block;
block.txs.forEach((tx) => { block.txs.forEach((tx) => {
this.transactions.unshift(tx); this.transactions.push(tx);
this.transactionsIndex[tx.id] = tx; this.transactionsIndex[tx.id] = tx;
}); });
}); });
console.log('Bisq data index rebuilt'); const time = new Date().getTime() - start;
console.log('Bisq data index rebuilt in ' + time + ' ms');
} }
private async loadBisqBlocksDump(cacheData: string): Promise<void> { private async loadBisqBlocksDump(cacheData: string): Promise<void> {
const start = new Date().getTime(); const start = new Date().getTime();
if (cacheData && cacheData.length !== 0) { if (cacheData && cacheData.length !== 0) {
console.log('Parsing Bisq data from dump file'); console.log('Loading Bisq data from dump...');
const data: BisqBlocks = JSON.parse(cacheData); const data: BisqBlocks = JSON.parse(cacheData);
if (data.blocks && data.blocks.length !== this.blocks.length) { if (data.blocks && data.blocks.length !== this.blocks.length) {
this.blocks = data.blocks; this.blocks = data.blocks;
this.blocks.reverse();
this.latestBlockHeight = data.chainHeight; this.latestBlockHeight = data.chainHeight;
const end = new Date().getTime(); const time = new Date().getTime() - start;
const time = end - start; console.log('Bisq dump loaded in ' + time + ' ms');
console.log('Loaded bisq dump in ' + time + ' ms');
} else { } else {
throw new Error(`Bisq dump didn't contain any blocks`); throw new Error(`Bisq dump didn't contain any blocks`);
} }

View File

@ -94,7 +94,7 @@ class Server {
this.app this.app
.get(config.API_ENDPOINT + 'bisq/tx/:txId', routes.getBisqTransaction) .get(config.API_ENDPOINT + 'bisq/tx/:txId', routes.getBisqTransaction)
.get(config.API_ENDPOINT + 'bisq/block/:hash', routes.getBisqBlock) .get(config.API_ENDPOINT + 'bisq/block/:hash', routes.getBisqBlock)
.get(config.API_ENDPOINT + 'bisq/block/:hash/txs/:index/:length', routes.getBisqBlockTransactions) .get(config.API_ENDPOINT + 'bisq/blocks/:index/:length', routes.getBisqBlocks)
.get(config.API_ENDPOINT + 'bisq/txs/:index/:length', routes.getBisqTransactions) .get(config.API_ENDPOINT + 'bisq/txs/:index/:length', routes.getBisqTransactions)
; ;
} }

View File

@ -105,7 +105,7 @@ class Routes {
} }
public getBisqBlock(req: Request, res: Response) { public getBisqBlock(req: Request, res: Response) {
const result = bisq.getBlock(req['hash']); const result = bisq.getBlock(req.params.hash);
if (result) { if (result) {
res.send(result); res.send(result);
} else { } else {
@ -113,16 +113,10 @@ class Routes {
} }
} }
public getBisqBlockTransactions(req: Request, res: Response) { public getBisqBlocks(req: Request, res: Response) {
const blockHash = req.params.hash || '';
const index = parseInt(req.params.index, 10) || 0; const index = parseInt(req.params.index, 10) || 0;
const length = parseInt(req.params.length, 10) > 100 ? 100 : parseInt(req.params.length, 10) || 25; const length = parseInt(req.params.length, 10) > 100 ? 100 : parseInt(req.params.length, 10) || 25;
const [transactions, count] = bisq.getBlockTransactions(blockHash, index, length); const [transactions, count] = bisq.getBlocks(index, length);
if (count === -1) {
res.header('X-Total-Count', '0');
res.send([]);
return;
}
res.header('X-Total-Count', count.toString()); res.header('X-Total-Count', count.toString());
res.send(transactions); res.send(transactions);
} }

View File

@ -23,7 +23,6 @@ import { WebsocketService } from './services/websocket.service';
import { AddressLabelsComponent } from './components/address-labels/address-labels.component'; import { AddressLabelsComponent } from './components/address-labels/address-labels.component';
import { MempoolBlocksComponent } from './components/mempool-blocks/mempool-blocks.component'; import { MempoolBlocksComponent } from './components/mempool-blocks/mempool-blocks.component';
import { QrcodeComponent } from './components/qrcode/qrcode.component'; import { QrcodeComponent } from './components/qrcode/qrcode.component';
import { ClipboardComponent } from './components/clipboard/clipboard.component';
import { MasterPageComponent } from './components/master-page/master-page.component'; import { MasterPageComponent } from './components/master-page/master-page.component';
import { AboutComponent } from './components/about/about.component'; import { AboutComponent } from './components/about/about.component';
import { TelevisionComponent } from './components/television/television.component'; import { TelevisionComponent } from './components/television/television.component';
@ -66,7 +65,6 @@ import { SharedModule } from './shared/shared.module';
AddressLabelsComponent, AddressLabelsComponent,
MempoolBlocksComponent, MempoolBlocksComponent,
QrcodeComponent, QrcodeComponent,
ClipboardComponent,
ChartistComponent, ChartistComponent,
FooterComponent, FooterComponent,
FiatComponent, FiatComponent,

View File

@ -0,0 +1,33 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpResponse } from '@angular/common/http';
import { Observable } from 'rxjs';
import { BisqTransaction, BisqBlock } from './bisq.interfaces';
const API_BASE_URL = '/api/v1';
@Injectable({
providedIn: 'root'
})
export class BisqApiService {
apiBaseUrl: string;
constructor(
private httpClient: HttpClient,
) { }
getTransaction$(txId: string): Observable<BisqTransaction> {
return this.httpClient.get<BisqTransaction>(API_BASE_URL + '/bisq/tx/' + txId);
}
listTransactions$(start: number, length: number): Observable<HttpResponse<BisqTransaction[]>> {
return this.httpClient.get<BisqTransaction[]>(API_BASE_URL + `/bisq/txs/${start}/${length}`, { observe: 'response' });
}
getBlock$(hash: string): Observable<BisqBlock> {
return this.httpClient.get<BisqBlock>(API_BASE_URL + '/bisq/block/' + hash);
}
listBlocks$(start: number, length: number): Observable<HttpResponse<BisqBlock[]>> {
return this.httpClient.get<BisqBlock[]>(API_BASE_URL + `/bisq/blocks/${start}/${length}`, { observe: 'response' });
}
}

View File

@ -6,23 +6,95 @@
<div class="clearfix"></div> <div class="clearfix"></div>
<ng-template ngFor let-tx [ngForOf]="bisqTransactions"> <ng-template [ngIf]="!isLoading && !error">
<div class="header-bg box" style="padding: 10px; margin-bottom: 10px;"> <div class="box">
<a [routerLink]="['/tx/' | relativeUrl, tx.id]" [state]="{ data: tx }"> <div class="row">
<span style="float: left;" class="d-block d-md-none">{{ tx.id | shortenString : 16 }}</span> <div class="col-sm">
<span style="float: left;" class="d-none d-md-block">{{ tx.id }}</span> <table class="table table-borderless table-striped">
</a> <tbody>
<div class="float-right"> <tr>
{{ tx.time | date:'yyyy-MM-dd HH:mm' }} <td class="td-width">Hash</td>
<td><a [routerLink]="['/block/' | relativeUrl, block.hash]" title="{{ block.hash }}">{{ block.hash | shortenString : 13 }}</a> <app-clipboard class="d-none d-sm-inline-block" [text]="block.hash"></app-clipboard></td>
</tr>
<tr>
<td>Timestamp</td>
<td>
{{ block.time | date:'yyyy-MM-dd HH:mm' }}
<div class="lg-inline">
<i>(<app-time-since [time]="block.time / 1000" [fastRender]="true"></app-time-since> ago)</i>
</div>
</td>
</tr>
</table>
</div>
<div class="col-sm">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td class="td-width">Previous hash</td>
<td><a [routerLink]="['/block/' | relativeUrl, block.previousBlockHash]" title="{{ block.hash }}">{{ block.previousBlockHash | shortenString : 13 }}</a> <app-clipboard class="d-none d-sm-inline-block" [text]="block.previousBlockHash"></app-clipboard></td>
</tr>
</table>
</div>
</div> </div>
<div class="clearfix"></div>
</div> </div>
<app-bisq-transfers [tx]="tx"></app-bisq-transfers> <div class="clearfix"></div>
<br> <br>
<h2>{{ block.txs.length | number }} transactions</h2>
<ng-template ngFor let-tx [ngForOf]="block.txs">
<div class="header-bg box" style="padding: 10px; margin-bottom: 10px;">
<a [routerLink]="['/tx/' | relativeUrl, tx.id]" [state]="{ data: tx }">
<span style="float: left;" class="d-block d-md-none">{{ tx.id | shortenString : 16 }}</span>
<span style="float: left;" class="d-none d-md-block">{{ tx.id }}</span>
</a>
<div class="float-right">
{{ tx.time | date:'yyyy-MM-dd HH:mm' }}
</div>
<div class="clearfix"></div>
</div>
<app-bisq-transfers [tx]="tx"></app-bisq-transfers>
<br>
</ng-template>
</ng-template>
<ng-template [ngIf]="isLoading">
<div class="box">
<div class="row">
<div class="col-sm">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td class="td-width">Hash</td>
<td><span class="skeleton-loader"></span></td>
</tr>
<tr>
<td>Timestamp</td>
<td><span class="skeleton-loader"></span></td>
</tr>
</table>
</div>
<div class="col-sm">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td class="td-width">Previous hash</td>
<td><span class="skeleton-loader"></span></td>
</tr>
</table>
</div>
</div>
</div>
</ng-template> </ng-template>
</div> </div>

View File

@ -0,0 +1,10 @@
.td-width {
width: 175px;
}
@media (max-width: 767.98px) {
.td-width {
width: 140px;
}
}

View File

@ -1,32 +1,52 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit, OnDestroy } from '@angular/core';
import { BisqTransaction } from 'src/app/interfaces/bisq.interfaces'; import { BisqTransaction, BisqBlock } from 'src/app/bisq/bisq.interfaces';
import { ApiService } from 'src/app/services/api.service'; import { BisqApiService } from '../bisq-api.service';
import { ActivatedRoute, ParamMap } from '@angular/router';
import { Subscribable, Subscription, of } from 'rxjs';
import { switchMap } from 'rxjs/operators';
@Component({ @Component({
selector: 'app-bisq-block', selector: 'app-bisq-block',
templateUrl: './bisq-block.component.html', templateUrl: './bisq-block.component.html',
styleUrls: ['./bisq-block.component.scss'] styleUrls: ['./bisq-block.component.scss']
}) })
export class BisqBlockComponent implements OnInit { export class BisqBlockComponent implements OnInit, OnDestroy {
bisqTransactions: BisqTransaction[]; block: BisqBlock;
bisqTransactionsCount: number; subscription: Subscription;
blockHash = ''; blockHash = '';
blockHeight = 0; blockHeight = 0;
isLoading = true;
error: any;
constructor( constructor(
private apiService: ApiService, private bisqApiService: BisqApiService,
private route: ActivatedRoute,
) { } ) { }
ngOnInit(): void { ngOnInit(): void {
this.apiService.listBisqBlockTransactions$(this.blockHash, 0, 10) this.subscription = this.route.paramMap
.subscribe((response) => { .pipe(
this.bisqTransactionsCount = parseInt(response.headers.get('x-total-count'), 10); switchMap((params: ParamMap) => {
this.bisqTransactions = response.body; this.blockHash = params.get('id') || '';
}); this.isLoading = true;
if (history.state.data && history.state.data.blockHeight) {
this.blockHeight = history.state.data.blockHeight;
}
if (history.state.data && history.state.data.block) {
this.blockHeight = history.state.data.block.height;
return of(history.state.data.block);
}
return this.bisqApiService.getBlock$(this.blockHash);
})
)
.subscribe((block: BisqBlock) => {
this.isLoading = false;
this.blockHeight = block.height;
this.block = block;
});
} }
ngOnDestroy() {
this.subscription.unsubscribe();
}
} }

View File

@ -1,5 +1,30 @@
<div class="container-xl"> <div class="container-xl">
<h2 style="float: left;">Blocks</h2> <h2 style="float: left;">BSQ Blocks</h2>
<br> <br>
<div class="clearfix"></div>
<table class="table table-borderless table-striped">
<thead>
<th>Hash</th>
<th>Total Sent (BSQ)</th>
<th>Transactions</th>
<th>Height</th>
<th>Time</th>
</thead>
<tbody>
<tr *ngFor="let block of blocks; trackBy: trackByFn">
<td><a [routerLink]="['/block/' | relativeUrl, block.hash]" title="{{ block.hash }}" [state]="{ data: { block: block } }">{{ block.hash | shortenString : 13 }}</a></td>
<td>{{ calculateTotalOutput(block) / 100 | number: '1.2-2' }}</td>
<td>{{ block.txs.length }}</td>
<td><a [routerLink]="['/block/' | relativeUrl, block.hash]" [state]="{ data: { block: block } }">{{ block.height }}</a></td>
<td>{{ block.time | date:'yyyy-MM-dd HH:mm' }}</td>
</tr>
</tbody>
</table>
<br>
<ngb-pagination [collectionSize]="totalCount" [rotate]="true" [pageSize]="itemsPerPage" [(page)]="page" (pageChange)="pageChange(page)" [maxSize]="5" [boundaryLinks]="true"></ngb-pagination>
</div> </div>

View File

@ -1,4 +1,8 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { BisqApiService } from '../bisq-api.service';
import { switchMap } from 'rxjs/operators';
import { Subject } from 'rxjs';
import { BisqBlock, BisqOutput, BisqTransaction } from '../bisq.interfaces';
@Component({ @Component({
selector: 'app-bisq-blocks', selector: 'app-bisq-blocks',
@ -6,10 +10,47 @@ import { Component, OnInit } from '@angular/core';
styleUrls: ['./bisq-blocks.component.scss'] styleUrls: ['./bisq-blocks.component.scss']
}) })
export class BisqBlocksComponent implements OnInit { export class BisqBlocksComponent implements OnInit {
blocks: BisqBlock[];
totalCount: number;
page = 1;
itemsPerPage: number;
contentSpace = window.innerHeight - (165 + 75);
fiveItemsPxSize = 250;
constructor() { } pageSubject$ = new Subject<number>();
constructor(
private bisqApiService: BisqApiService,
) { }
ngOnInit(): void { ngOnInit(): void {
this.itemsPerPage = Math.max(Math.round(this.contentSpace / this.fiveItemsPxSize) * 5, 10);
this.pageSubject$
.pipe(
switchMap((page) => this.bisqApiService.listBlocks$((page - 1) * 10, this.itemsPerPage))
)
.subscribe((response) => {
this.blocks = response.body;
this.totalCount = parseInt(response.headers.get('x-total-count'), 10);
}, (error) => {
console.log(error);
});
this.pageSubject$.next(1);
} }
calculateTotalOutput(block: BisqBlock): number {
return block.txs.reduce((a: number, tx: BisqTransaction) =>
a + tx.outputs.reduce((acc: number, output: BisqOutput) => acc + output.bsqAmount, 0), 0
);
}
trackByFn(index: number) {
return index;
}
pageChange(page: number) {
this.pageSubject$.next(page);
}
} }

View File

@ -11,10 +11,6 @@
<td>Outputs</td> <td>Outputs</td>
<td>{{ totalOutput / 100 | number: '1.2-2' }} BSQ</td> <td>{{ totalOutput / 100 | number: '1.2-2' }} BSQ</td>
</tr> </tr>
<tr>
<td>Burnt</td>
<td>{{ tx.burntFee / 100 | number: '1.2-2' }} BSQ</td>
</tr>
<tr> <tr>
<td>Issuance</td> <td>Issuance</td>
<td>{{ totalIssued / 100 | number: '1.2-2' }} BSQ</td> <td>{{ totalIssued / 100 | number: '1.2-2' }} BSQ</td>

View File

@ -1,5 +1,5 @@
import { Component, ChangeDetectionStrategy, Input, OnChanges } from '@angular/core'; import { Component, ChangeDetectionStrategy, Input, OnChanges } from '@angular/core';
import { BisqTransaction } from 'src/app/interfaces/bisq.interfaces'; import { BisqTransaction } from 'src/app/bisq/bisq.interfaces';
@Component({ @Component({
selector: 'app-bisq-transaction-details', selector: 'app-bisq-transaction-details',

View File

@ -2,64 +2,133 @@
<h1 class="float-left mr-3 mb-md-3">Transaction</h1> <h1 class="float-left mr-3 mb-md-3">Transaction</h1>
<button type="button" class="btn btn-sm btn-success float-right mr-2 mt-1 mt-md-3">2 confirmations</button> <ng-template [ngIf]="!isLoading" [ngIfElse]="isLoadingTmpl">
<div> <button *ngIf="(latestBlock$ | async) as latestBlock" type="button" class="btn btn-sm btn-success float-right mr-2 mt-1 mt-md-3">{{ latestBlock.height - bisqTx.blockHeight + 1 }} confirmation<ng-container *ngIf="latestBlock.height - bisqTx.blockHeight + 1 > 1">s</ng-container></button>
<a [routerLink]="['/bisq-tx' | relativeUrl, bisqTx.blockHash]" style="line-height: 56px;">
<span class="d-inline d-lg-none">{{ bisqTx.blockHash | shortenString : 24 }}</span>
<span class="d-none d-lg-inline">{{ bisqTx.blockHash }}</span>
</a>
</div>
<div class="clearfix"></div>
<div class="box"> <div>
<div class="row"> <a [routerLink]="['/bisq-tx' | relativeUrl, bisqTx.blockHash]" style="line-height: 56px;">
<div class="col-sm"> <span class="d-inline d-lg-none">{{ bisqTx.blockHash | shortenString : 24 }}</span>
<span class="d-none d-lg-inline">{{ bisqTx.blockHash }}</span>
</a>
<app-clipboard [text]="txId"></app-clipboard>
</div>
<div class="clearfix"></div>
<div class="box">
<div class="row">
<div class="col-sm">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td class="td-width">Included in block</td>
<td>
<a [routerLink]="['/block/' | relativeUrl, bisqTx.blockHash]" [state]="{ data: { blockHeight: bisqTx.blockHash } }">{{ bisqTx.blockHeight }}</a>
<i> (<app-time-since [time]="bisqTx.time / 1000" [fastRender]="true"></app-time-since> ago)</i>
</td>
</tr>
</tbody>
</table>
</div>
<div class="col-sm">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td class="td-width">Fee burnt</td>
<td>
{{ bisqTx.burntFee / 100 | number: '1.2-2' }} BSQ
</tr>
</tbody>
</table>
</div>
</div>
</div>
<br>
<h2>Details</h2>
<app-bisq-transaction-details [tx]="bisqTx"></app-bisq-transaction-details>
<br>
<h2>Inputs & Outputs</h2>
<app-bisq-transfers [tx]="bisqTx"></app-bisq-transfers>
<br>
</ng-template>
<ng-template #isLoadingTmpl>
<div class="clearfix"></div>
<div class="box">
<div class="row">
<div class="col-sm">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td class="td-width"><span class="skeleton-loader"></span></td>
<td><span class="skeleton-loader"></span></td>
</tr>
</tbody>
</table>
</div>
<div class="col-sm">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td class="td-width"><span class="skeleton-loader"></span></td>
<td><span class="skeleton-loader"></span></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<br>
<h2>Details</h2>
<div class="box">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td><span class="skeleton-loader"></span></td>
<td><span class="skeleton-loader"></span></td>
</tr>
<tr>
<td><span class="skeleton-loader"></span></td>
<td><span class="skeleton-loader"></span></td>
</tr>
<tr>
<td><span class="skeleton-loader"></span></td>
</tr>
</tbody>
</table>
</div>
<br>
<h2>Inputs & Outputs</h2>
<div class="box">
<div class="row">
<table class="table table-borderless table-striped"> <table class="table table-borderless table-striped">
<tbody> <tbody>
<tr> <tr>
<td class="td-width">Included in block</td> <td><span class="skeleton-loader"></span></td>
<td> <td><span class="skeleton-loader"></span></td>
<a [routerLink]="['/block/' | relativeUrl, bisqTx.blockHash]" [state]="{ data: { blockHeight: bisqTx.blockHash } }">{{ bisqTx.blockHeight }}</a>
<i> (<app-time-since [time]="bisqTx.time" [fastRender]="true"></app-time-since> ago)</i>
</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="col-sm">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td class="td-width">Fee</td>
<td>15,436 sat ($1.40)</td>
</tr>
<tr>
<td>Fee per vByte</td>
<td> 68.2 sat/vB
</td>
</tr>
</tbody>
</table>
</div>
</div> </div>
</div>
</ng-template>
<br>
<h2>Details</h2>
<app-bisq-transaction-details [tx]="bisqTx"></app-bisq-transaction-details>
<br>
<h2>Inputs & Outputs</h2>
<app-bisq-transfers [tx]="bisqTx"></app-bisq-transfers>
<br>
</div> </div>

View File

@ -0,0 +1,9 @@
.td-width {
width: 175px;
}
@media (max-width: 767.98px) {
.td-width {
width: 150px;
}
}

View File

@ -1,25 +0,0 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { BisqTransactionComponent } from './bisq-transaction.component';
describe('BisqTransactionComponent', () => {
let component: BisqTransactionComponent;
let fixture: ComponentFixture<BisqTransactionComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ BisqTransactionComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(BisqTransactionComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -1,36 +1,50 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit, OnDestroy } from '@angular/core';
import { ActivatedRoute, ParamMap } from '@angular/router'; import { ActivatedRoute, ParamMap } from '@angular/router';
import { BisqTransaction } from 'src/app/interfaces/bisq.interfaces'; import { BisqTransaction } from 'src/app/bisq/bisq.interfaces';
import { switchMap } from 'rxjs/operators'; import { switchMap, map } from 'rxjs/operators';
import { ApiService } from 'src/app/services/api.service'; import { of, Observable, Subscription } from 'rxjs';
import { of } from 'rxjs'; import { StateService } from 'src/app/services/state.service';
import { Block } from 'src/app/interfaces/electrs.interface';
import { BisqApiService } from '../bisq-api.service';
@Component({ @Component({
selector: 'app-bisq-transaction', selector: 'app-bisq-transaction',
templateUrl: './bisq-transaction.component.html', templateUrl: './bisq-transaction.component.html',
styleUrls: ['./bisq-transaction.component.scss'] styleUrls: ['./bisq-transaction.component.scss']
}) })
export class BisqTransactionComponent implements OnInit { export class BisqTransactionComponent implements OnInit, OnDestroy {
bisqTx: BisqTransaction; bisqTx: BisqTransaction;
latestBlock$: Observable<Block>;
txId: string; txId: string;
isLoading = true;
subscription: Subscription;
constructor( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
private apiService: ApiService, private bisqApiService: BisqApiService,
private stateService: StateService,
) { } ) { }
ngOnInit(): void { ngOnInit(): void {
this.route.paramMap.pipe( this.subscription = this.route.paramMap.pipe(
switchMap((params: ParamMap) => { switchMap((params: ParamMap) => {
this.isLoading = true;
this.txId = params.get('id') || ''; this.txId = params.get('id') || '';
if (history.state.bsqTx) { if (history.state.data) {
return of(history.state.bsqTx); return of(history.state.data);
} }
return this.apiService.getBisqTransaction$(this.txId); return this.bisqApiService.getTransaction$(this.txId);
}) })
) )
.subscribe((tx) => { .subscribe((tx) => {
this.isLoading = false;
this.bisqTx = tx; this.bisqTx = tx;
}); });
this.latestBlock$ = this.stateService.blocks$.pipe(map((([block]) => block)));
}
ngOnDestroy() {
this.subscription.unsubscribe();
} }
} }

View File

@ -1,5 +1,5 @@
<div class="container-xl"> <div class="container-xl">
<h2 style="float: left;">Latest BSQ Transactions</h2> <h2 style="float: left;">BSQ Transactions</h2>
<br> <br>
<div class="clearfix"></div> <div class="clearfix"></div>
@ -14,7 +14,7 @@
<th>Block Time</th> <th>Block Time</th>
</thead> </thead>
<tbody> <tbody>
<tr *ngFor="let tx of transactions"> <tr *ngFor="let tx of transactions; trackBy: trackByFn">
<td><a [routerLink]="['/tx/' | relativeUrl, tx.id]" [state]="{ bsqTx: tx }">{{ tx.id | shortenString : 16 }}</a></td> <td><a [routerLink]="['/tx/' | relativeUrl, tx.id]" [state]="{ bsqTx: tx }">{{ tx.id | shortenString : 16 }}</a></td>
<td><app-bisq-icon class="mr-1" [txType]="tx.txType"></app-bisq-icon> {{ tx.txTypeDisplayString }}</td> <td><app-bisq-icon class="mr-1" [txType]="tx.txType"></app-bisq-icon> {{ tx.txTypeDisplayString }}</td>
<td>{{ calculateTotalOutput(tx.outputs) / 100 | number: '1.2-2' }}</td> <td>{{ calculateTotalOutput(tx.outputs) / 100 | number: '1.2-2' }}</td>

View File

@ -1,8 +1,8 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { BisqTransaction, BisqOutput } from '../../interfaces/bisq.interfaces'; import { BisqTransaction, BisqOutput } from '../bisq.interfaces';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { switchMap } from 'rxjs/operators'; import { switchMap } from 'rxjs/operators';
import { ApiService } from 'src/app/services/api.service'; import { BisqApiService } from '../bisq-api.service';
@Component({ @Component({
selector: 'app-bisq-transactions', selector: 'app-bisq-transactions',
@ -14,13 +14,13 @@ export class BisqTransactionsComponent implements OnInit {
totalCount: number; totalCount: number;
page = 1; page = 1;
itemsPerPage: number; itemsPerPage: number;
contentSpace = window.innerHeight - (200 + 200); contentSpace = window.innerHeight - (165 + 75);
fiveItemsPxSize = 250; fiveItemsPxSize = 250;
pageSubject$ = new Subject<number>(); pageSubject$ = new Subject<number>();
constructor( constructor(
private apiService: ApiService, private bisqApiService: BisqApiService,
) { } ) { }
ngOnInit(): void { ngOnInit(): void {
@ -28,7 +28,7 @@ export class BisqTransactionsComponent implements OnInit {
this.pageSubject$ this.pageSubject$
.pipe( .pipe(
switchMap((page) => this.apiService.listBisqTransactions$((page - 1) * 10, this.itemsPerPage)) switchMap((page) => this.bisqApiService.listTransactions$((page - 1) * 10, this.itemsPerPage))
) )
.subscribe((response) => { .subscribe((response) => {
this.transactions = response.body; this.transactions = response.body;
@ -47,4 +47,8 @@ export class BisqTransactionsComponent implements OnInit {
calculateTotalOutput(outputs: BisqOutput[]): number { calculateTotalOutput(outputs: BisqOutput[]): number {
return outputs.reduce((acc: number, output: BisqOutput) => acc + output.bsqAmount, 0); return outputs.reduce((acc: number, output: BisqOutput) => acc + output.bsqAmount, 0);
} }
trackByFn(index: number) {
return index;
}
} }

View File

@ -1,5 +1,5 @@
import { Component, OnInit, ChangeDetectionStrategy, Input } from '@angular/core'; import { Component, OnInit, ChangeDetectionStrategy, Input } from '@angular/core';
import { BisqTransaction } from 'src/app/interfaces/bisq.interfaces'; import { BisqTransaction } from 'src/app/bisq/bisq.interfaces';
@Component({ @Component({
selector: 'app-bisq-transfers', selector: 'app-bisq-transfers',

View File

@ -14,6 +14,7 @@ import { faLeaf, faQuestion, faExclamationTriangle, faRocket, faRetweet, faFileA
faEye, faEyeSlash, faLock, faLockOpen } from '@fortawesome/free-solid-svg-icons'; faEye, faEyeSlash, faLock, faLockOpen } from '@fortawesome/free-solid-svg-icons';
import { BisqBlocksComponent } from './bisq-blocks/bisq-blocks.component'; import { BisqBlocksComponent } from './bisq-blocks/bisq-blocks.component';
import { BisqExplorerComponent } from './bisq-explorer/bisq-explorer.component'; import { BisqExplorerComponent } from './bisq-explorer/bisq-explorer.component';
import { BisqApiService } from './bisq-api.service';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -34,6 +35,9 @@ import { BisqExplorerComponent } from './bisq-explorer/bisq-explorer.component';
NgbPaginationModule, NgbPaginationModule,
FontAwesomeModule, FontAwesomeModule,
], ],
providers: [
BisqApiService,
]
}) })
export class BisqModule { export class BisqModule {
constructor(library: FaIconLibrary) { constructor(library: FaIconLibrary) {

View File

@ -4,7 +4,7 @@ import { ActivatedRoute, ParamMap, Router } from '@angular/router';
import { ElectrsApiService } from '../../services/electrs-api.service'; import { ElectrsApiService } from '../../services/electrs-api.service';
import { switchMap, tap, debounceTime, catchError } from 'rxjs/operators'; import { switchMap, tap, debounceTime, catchError } from 'rxjs/operators';
import { Block, Transaction, Vout } from '../../interfaces/electrs.interface'; import { Block, Transaction, Vout } from '../../interfaces/electrs.interface';
import { of } from 'rxjs'; import { of, Subscription } from 'rxjs';
import { StateService } from '../../services/state.service'; import { StateService } from '../../services/state.service';
import { SeoService } from 'src/app/services/seo.service'; import { SeoService } from 'src/app/services/seo.service';
import { env } from 'src/app/app.constants'; import { env } from 'src/app/app.constants';
@ -25,6 +25,7 @@ export class BlockComponent implements OnInit, OnDestroy {
isLoadingTransactions = true; isLoadingTransactions = true;
error: any; error: any;
blockSubsidy: number; blockSubsidy: number;
subscription: Subscription;
fees: number; fees: number;
paginationMaxSize: number; paginationMaxSize: number;
page = 1; page = 1;
@ -43,7 +44,7 @@ export class BlockComponent implements OnInit, OnDestroy {
this.paginationMaxSize = window.matchMedia('(max-width: 700px)').matches ? 3 : 5; this.paginationMaxSize = window.matchMedia('(max-width: 700px)').matches ? 3 : 5;
this.network = this.stateService.network; this.network = this.stateService.network;
this.route.paramMap this.subscription = this.route.paramMap
.pipe( .pipe(
switchMap((params: ParamMap) => { switchMap((params: ParamMap) => {
const blockHash: string = params.get('id') || ''; const blockHash: string = params.get('id') || '';
@ -129,6 +130,7 @@ export class BlockComponent implements OnInit, OnDestroy {
ngOnDestroy() { ngOnDestroy() {
this.stateService.markBlock$.next({}); this.stateService.markBlock$.next({});
this.subscription.unsubscribe();
} }
setBlockSubsidy() { setBlockSubsidy() {

View File

@ -10,7 +10,7 @@ import { AudioService } from 'src/app/services/audio.service';
import { ApiService } from 'src/app/services/api.service'; import { ApiService } from 'src/app/services/api.service';
import { SeoService } from 'src/app/services/seo.service'; import { SeoService } from 'src/app/services/seo.service';
import { calcSegwitFeeGains } from 'src/app/bitcoin.utils'; import { calcSegwitFeeGains } from 'src/app/bitcoin.utils';
import { BisqTransaction } from 'src/app/interfaces/bisq.interfaces'; import { BisqTransaction } from 'src/app/bisq/bisq.interfaces';
@Component({ @Component({
selector: 'app-transaction', selector: 'app-transaction',

View File

@ -3,7 +3,6 @@ import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http';
import { OptimizedMempoolStats } from '../interfaces/node-api.interface'; import { OptimizedMempoolStats } from '../interfaces/node-api.interface';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { StateService } from './state.service'; import { StateService } from './state.service';
import { BisqTransaction, BisqBlock } from '../interfaces/bisq.interfaces';
const API_BASE_URL = '{network}/api/v1'; const API_BASE_URL = '{network}/api/v1';
@ -61,22 +60,4 @@ export class ApiService {
}); });
return this.httpClient.get<number[]>(this.apiBaseUrl + '/transaction-times', { params }); return this.httpClient.get<number[]>(this.apiBaseUrl + '/transaction-times', { params });
} }
getBisqTransaction$(txId: string): Observable<BisqTransaction> {
return this.httpClient.get<BisqTransaction>(this.apiBaseUrl + '/bisq/tx/' + txId);
}
listBisqTransactions$(start: number, length: number): Observable<HttpResponse<BisqTransaction[]>> {
return this.httpClient.get<BisqTransaction[]>(this.apiBaseUrl + `/bisq/txs/${start}/${length}`, { observe: 'response' });
}
getBisqBlock$(hash: string): Observable<BisqBlock> {
return this.httpClient.get<BisqBlock>(this.apiBaseUrl + '/bisq/block/' + hash);
}
listBisqBlockTransactions$(blockHash: string, start: number, length: number): Observable<HttpResponse<BisqTransaction[]>> {
return this.httpClient.get<BisqTransaction[]>(
this.apiBaseUrl + `/bisq/block/${blockHash}/txs/${start}/${length}`, { observe: 'response' }
);
}
} }

View File

@ -9,6 +9,7 @@ import { ScriptpubkeyTypePipe } from './pipes/scriptpubkey-type-pipe/scriptpubke
import { BytesPipe } from './pipes/bytes-pipe/bytes.pipe'; import { BytesPipe } from './pipes/bytes-pipe/bytes.pipe';
import { WuBytesPipe } from './pipes/bytes-pipe/wubytes.pipe'; import { WuBytesPipe } from './pipes/bytes-pipe/wubytes.pipe';
import { TimeSinceComponent } from '../components/time-since/time-since.component'; import { TimeSinceComponent } from '../components/time-since/time-since.component';
import { ClipboardComponent } from '../components/clipboard/clipboard.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -20,6 +21,7 @@ import { TimeSinceComponent } from '../components/time-since/time-since.componen
WuBytesPipe, WuBytesPipe,
CeilPipe, CeilPipe,
ShortenStringPipe, ShortenStringPipe,
ClipboardComponent,
TimeSinceComponent, TimeSinceComponent,
], ],
imports: [ imports: [
@ -38,6 +40,7 @@ import { TimeSinceComponent } from '../components/time-since/time-since.componen
CeilPipe, CeilPipe,
ShortenStringPipe, ShortenStringPipe,
TimeSinceComponent, TimeSinceComponent,
ClipboardComponent
] ]
}) })
export class SharedModule {} export class SharedModule {}