Address index and api.

Address view.
This commit is contained in:
softsimon 2020-07-13 21:46:25 +07:00
parent db2e293ce5
commit 432fb9cd66
No known key found for this signature in database
GPG Key ID: 488D7DCFB5A430D7
22 changed files with 295 additions and 54 deletions

View File

@ -3,11 +3,11 @@ import * as fs from 'fs';
import { BisqBlocks, BisqBlock, BisqTransaction } from '../interfaces';
class Bisq {
private latestBlockHeight = 0;
private blocks: BisqBlock[] = [];
private transactions: BisqTransaction[] = [];
private transactionsIndex: { [txId: string]: BisqTransaction } = {};
private blocksIndex: { [hash: string]: BisqBlock } = {};
private transactionIndex: { [txId: string]: BisqTransaction } = {};
private blockIndex: { [hash: string]: BisqBlock } = {};
private addressIndex: { [address: string]: BisqTransaction[] } = {};
constructor() {}
@ -29,7 +29,7 @@ class Bisq {
}
getTransaction(txId: string): BisqTransaction | undefined {
return this.transactionsIndex[txId];
return this.transactionIndex[txId];
}
getTransactions(start: number, length: number): [BisqTransaction[], number] {
@ -37,9 +37,11 @@ class Bisq {
}
getBlock(hash: string): BisqBlock | undefined {
console.log(hash);
console.log(this.blocksIndex[hash]);
return this.blocksIndex[hash];
return this.blockIndex[hash];
}
getAddress(hash: string): BisqTransaction[] {
return this.addressIndex[hash];
}
getBlocks(start: number, length: number): [BisqBlock[], number] {
@ -59,16 +61,42 @@ class Bisq {
private buildIndex() {
const start = new Date().getTime();
this.transactions = [];
this.transactionsIndex = {};
this.transactionIndex = {};
this.addressIndex = {};
this.blocks.forEach((block) => {
if (!this.blocksIndex[block.hash]) {
this.blocksIndex[block.hash] = block;
/* Build block index */
if (!this.blockIndex[block.hash]) {
this.blockIndex[block.hash] = block;
}
/* Build transactions index */
block.txs.forEach((tx) => {
this.transactions.push(tx);
this.transactionsIndex[tx.id] = tx;
this.transactionIndex[tx.id] = tx;
});
});
/* Build address index */
this.transactions.forEach((tx) => {
tx.inputs.forEach((input) => {
if (!this.addressIndex[input.address]) {
this.addressIndex[input.address] = [];
}
if (this.addressIndex[input.address].indexOf(tx) === -1) {
this.addressIndex[input.address].push(tx);
}
});
tx.outputs.forEach((output) => {
if (!this.addressIndex[output.address]) {
this.addressIndex[output.address] = [];
}
if (this.addressIndex[output.address].indexOf(tx) === -1) {
this.addressIndex[output.address].push(tx);
}
});
});
const time = new Date().getTime() - start;
console.log('Bisq data index rebuilt in ' + time + ' ms');
}
@ -81,7 +109,6 @@ class Bisq {
if (data.blocks && data.blocks.length !== this.blocks.length) {
this.blocks = data.blocks;
this.blocks.reverse();
this.latestBlockHeight = data.chainHeight;
const time = new Date().getTime() - start;
console.log('Bisq dump loaded in ' + time + ' ms');
} else {

View File

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

View File

@ -120,6 +120,15 @@ class Routes {
res.header('X-Total-Count', count.toString());
res.send(transactions);
}
public getBisqAddress(req: Request, res: Response) {
const result = bisq.getAddress(req.params.address.substr(1));
if (result) {
res.send(result);
} else {
res.status(404).send('Bisq address not found');
}
}
}
export default new Routes();

View File

@ -22,7 +22,6 @@ import { LatestBlocksComponent } from './components/latest-blocks/latest-blocks.
import { WebsocketService } from './services/websocket.service';
import { AddressLabelsComponent } from './components/address-labels/address-labels.component';
import { MempoolBlocksComponent } from './components/mempool-blocks/mempool-blocks.component';
import { QrcodeComponent } from './components/qrcode/qrcode.component';
import { MasterPageComponent } from './components/master-page/master-page.component';
import { AboutComponent } from './components/about/about.component';
import { TelevisionComponent } from './components/television/television.component';
@ -64,7 +63,6 @@ import { SharedModule } from './shared/shared.module';
TimespanComponent,
AddressLabelsComponent,
MempoolBlocksComponent,
QrcodeComponent,
ChartistComponent,
FooterComponent,
FiatComponent,

View File

@ -0,0 +1,106 @@
<div class="container-xl">
<h1 style="float: left;">Bisq Address</h1>
<a [routerLink]="['/address/' | relativeUrl, addressString]" style="line-height: 56px; margin-left: 10px;">
<span class="d-inline d-lg-none">{{ addressString | shortenString : 24 }}</span>
<span class="d-none d-lg-inline">{{ addressString }}</span>
</a>
<app-clipboard [text]="addressString"></app-clipboard>
<br>
<div class="clearfix"></div>
<ng-template [ngIf]="!isLoadingAddress && !error">
<div class="box">
<div class="row">
<div class="col">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td>Total received</td>
<td>{{ totalReceived / 100 }} BSQ</td>
</tr>
<tr>
<td>Total sent</td>
<td>{{ totalSent / 100 }} BSQ</td>
</tr>
<tr>
<td>Final balance</td>
<td>{{ (totalReceived - totalSent) / 100 }} BSQ</td>
</tr>
</tbody>
</table>
</div>
<div class="w-100 d-block d-md-none"></div>
<div class="col qrcode-col">
<div class="qr-wrapper">
<app-qrcode [data]="addressString"></app-qrcode>
</div>
</div>
</div>
</div>
<br>
<h2>{{ transactions.length | number }} transactions</h2>
<ng-template ngFor let-tx [ngForOf]="transactions">
<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]="isLoadingAddress && !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="w-100 d-block d-md-none"></div>
<div class="col">
</div>
</div>
</div>
</ng-template>
<ng-template [ngIf]="error">
<div class="text-center">
Error loading address data.
<br>
<i>{{ error.error }}</i>
</div>
</ng-template>
</div>
<br>

View 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;
}
}

View File

@ -0,0 +1,82 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { SeoService } from 'src/app/services/seo.service';
import { switchMap, filter, catchError } from 'rxjs/operators';
import { ParamMap, ActivatedRoute } from '@angular/router';
import { Subscription, of } from 'rxjs';
import { BisqTransaction } from '../bisq.interfaces';
import { BisqApiService } from '../bisq-api.service';
@Component({
selector: 'app-bisq-address',
templateUrl: './bisq-address.component.html',
styleUrls: ['./bisq-address.component.scss']
})
export class BisqAddressComponent implements OnInit, OnDestroy {
transactions: BisqTransaction[];
addressString: string;
isLoadingAddress = true;
error: any;
mainSubscription: Subscription;
totalReceived = 0;
totalSent = 0;
constructor(
private route: ActivatedRoute,
private seoService: SeoService,
private bisqApiService: BisqApiService,
) { }
ngOnInit() {
this.mainSubscription = this.route.paramMap
.pipe(
switchMap((params: ParamMap) => {
this.error = undefined;
this.isLoadingAddress = true;
this.transactions = null;
document.body.scrollTo(0, 0);
this.addressString = params.get('id') || '';
this.seoService.setTitle('Address: ' + this.addressString, true);
return this.bisqApiService.getAddress$(this.addressString)
.pipe(
catchError((err) => {
this.isLoadingAddress = false;
this.error = err;
console.log(err);
return of(null);
})
);
}),
filter((transactions) => transactions !== null)
)
.subscribe((transactions: BisqTransaction[]) => {
this.transactions = transactions;
this.updateChainStats();
this.isLoadingAddress = false;
},
(error) => {
console.log(error);
this.error = error;
this.isLoadingAddress = false;
});
}
updateChainStats() {
const shortenedAddress = this.addressString.substr(1);
this.totalSent = this.transactions.reduce((acc, tx) =>
acc + tx.inputs
.filter((input) => input.address === shortenedAddress)
.reduce((a, input) => a + input.bsqAmount, 0), 0);
this.totalReceived = this.transactions.reduce((acc, tx) =>
acc + tx.outputs
.filter((output) => output.address === shortenedAddress)
.reduce((a, output) => a + output.bsqAmount, 0), 0);
}
ngOnDestroy() {
this.mainSubscription.unsubscribe();
}
}

View File

@ -30,4 +30,8 @@ export class BisqApiService {
listBlocks$(start: number, length: number): Observable<HttpResponse<BisqBlock[]>> {
return this.httpClient.get<BisqBlock[]>(API_BASE_URL + `/bisq/blocks/${start}/${length}`, { observe: 'response' });
}
getAddress$(address: string): Observable<BisqTransaction[]> {
return this.httpClient.get<BisqTransaction[]>(API_BASE_URL + '/bisq/address/' + address);
}
}

View File

@ -1,7 +1,7 @@
<div class="container-xl">
<div class="title-block">
<h1>Block <ng-template [ngIf]="blockHeight"><a [routerLink]="['/block/' | relativeUrl, blockHash]">{{ blockHeight }}</a></ng-template></h1>
<h1>Bisq Block <ng-template [ngIf]="blockHeight"><a [routerLink]="['/block/' | relativeUrl, blockHash]">{{ blockHeight }}</a></ng-template></h1>
</div>
<div class="clearfix"></div>

View File

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

View File

@ -4,6 +4,7 @@ import { BisqApiService } from '../bisq-api.service';
import { ActivatedRoute, ParamMap } from '@angular/router';
import { Subscribable, Subscription, of } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import { SeoService } from 'src/app/services/seo.service';
@Component({
selector: 'app-bisq-block',
@ -21,6 +22,7 @@ export class BisqBlockComponent implements OnInit, OnDestroy {
constructor(
private bisqApiService: BisqApiService,
private route: ActivatedRoute,
private seoService: SeoService,
) { }
ngOnInit(): void {
@ -28,6 +30,7 @@ export class BisqBlockComponent implements OnInit, OnDestroy {
.pipe(
switchMap((params: ParamMap) => {
this.blockHash = params.get('id') || '';
document.body.scrollTo(0, 0);
this.isLoading = true;
if (history.state.data && history.state.data.blockHeight) {
this.blockHeight = history.state.data.blockHeight;
@ -42,6 +45,7 @@ export class BisqBlockComponent implements OnInit, OnDestroy {
.subscribe((block: BisqBlock) => {
this.isLoading = false;
this.blockHeight = block.height;
this.seoService.setTitle('Block: #' + block.height + ': ' + block.hash, true);
this.block = block;
});
}

View File

@ -1,5 +1,5 @@
<div class="container-xl">
<h2 style="float: left;">BSQ Blocks</h2>
<h2 style="float: left;">Bisq Blocks</h2>
<br>
<div class="clearfix"></div>

View File

@ -3,6 +3,7 @@ import { BisqApiService } from '../bisq-api.service';
import { switchMap } from 'rxjs/operators';
import { Subject } from 'rxjs';
import { BisqBlock, BisqOutput, BisqTransaction } from '../bisq.interfaces';
import { SeoService } from 'src/app/services/seo.service';
@Component({
selector: 'app-bisq-blocks',
@ -21,9 +22,11 @@ export class BisqBlocksComponent implements OnInit {
constructor(
private bisqApiService: BisqApiService,
private seoService: SeoService,
) { }
ngOnInit(): void {
this.seoService.setTitle('Blocks', true);
this.itemsPerPage = Math.max(Math.round(this.contentSpace / this.fiveItemsPxSize) * 5, 10);
this.pageSubject$

View File

@ -1,6 +1,6 @@
<div class="container-xl">
<h1 class="float-left mr-3 mb-md-3">Transaction</h1>
<h1 class="float-left mr-3 mb-md-3">Bisq Transaction</h1>
<ng-template [ngIf]="!isLoading" [ngIfElse]="isLoadingTmpl">

View File

@ -6,6 +6,7 @@ import { of, Observable, Subscription } from 'rxjs';
import { StateService } from 'src/app/services/state.service';
import { Block } from 'src/app/interfaces/electrs.interface';
import { BisqApiService } from '../bisq-api.service';
import { SeoService } from 'src/app/services/seo.service';
@Component({
selector: 'app-bisq-transaction',
@ -23,13 +24,16 @@ export class BisqTransactionComponent implements OnInit, OnDestroy {
private route: ActivatedRoute,
private bisqApiService: BisqApiService,
private stateService: StateService,
private seoService: SeoService,
) { }
ngOnInit(): void {
this.subscription = this.route.paramMap.pipe(
switchMap((params: ParamMap) => {
this.isLoading = true;
document.body.scrollTo(0, 0);
this.txId = params.get('id') || '';
this.seoService.setTitle('Transaction: ' + this.txId, true);
if (history.state.data) {
return of(history.state.data);
}

View File

@ -1,5 +1,5 @@
<div class="container-xl">
<h2 style="float: left;">BSQ Transactions</h2>
<h2 style="float: left;">Bisq Transactions</h2>
<br>
<div class="clearfix"></div>

View File

@ -3,6 +3,7 @@ import { BisqTransaction, BisqOutput } from '../bisq.interfaces';
import { Subject } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import { BisqApiService } from '../bisq-api.service';
import { SeoService } from 'src/app/services/seo.service';
@Component({
selector: 'app-bisq-transactions',
@ -21,9 +22,12 @@ export class BisqTransactionsComponent implements OnInit {
constructor(
private bisqApiService: BisqApiService,
private seoService: SeoService,
) { }
ngOnInit(): void {
this.seoService.setTitle('Transactions', true);
this.itemsPerPage = Math.max(Math.round(this.contentSpace / this.fiveItemsPxSize) * 5, 10);
this.pageSubject$

View File

@ -16,7 +16,7 @@
</ng-template>
</td>
<td>
<a [routerLink]="['/address/' | relativeUrl, input.address]" title="{{ input.address }}">
<a [routerLink]="['/address/' | relativeUrl, 'B' + input.address]" title="B{{ input.address }}">
<span class="d-block d-lg-none">B{{ input.address | shortenString : 16 }}</span>
<span class="d-none d-lg-block">B{{ input.address | shortenString : 35 }}</span>
</a>
@ -36,7 +36,7 @@
<ng-template ngFor let-output [ngForOf]="tx.outputs" [ngForTrackBy]="trackByIndexFn">
<tr *ngIf="output.isVerified && output.opReturn === undefined">
<td>
<a [routerLink]="['/address/' | relativeUrl, output.address]" title="{{ output.address }}">
<a [routerLink]="['/address/' | relativeUrl, 'B' + output.address]" title="B{{ output.address }}">
<span class="d-block d-lg-none">B{{ output.address | shortenString : 16 }}</span>
<span class="d-none d-lg-block">B{{ output.address | shortenString : 35 }}</span>
</a>

View File

@ -15,6 +15,7 @@ import { faLeaf, faQuestion, faExclamationTriangle, faRocket, faRetweet, faFileA
import { BisqBlocksComponent } from './bisq-blocks/bisq-blocks.component';
import { BisqExplorerComponent } from './bisq-explorer/bisq-explorer.component';
import { BisqApiService } from './bisq-api.service';
import { BisqAddressComponent } from './bisq-address/bisq-address.component';
@NgModule({
declarations: [
@ -27,6 +28,7 @@ import { BisqApiService } from './bisq-api.service';
BisqTransfersComponent,
BisqBlocksComponent,
BisqExplorerComponent,
BisqAddressComponent,
],
imports: [
CommonModule,

View File

@ -7,6 +7,7 @@ import { BisqTransactionComponent } from './bisq-transaction/bisq-transaction.co
import { BisqBlockComponent } from './bisq-block/bisq-block.component';
import { BisqBlocksComponent } from './bisq-blocks/bisq-blocks.component';
import { BisqExplorerComponent } from './bisq-explorer/bisq-explorer.component';
import { BisqAddressComponent } from './bisq-address/bisq-address.component';
const routes: Routes = [
{
@ -32,7 +33,7 @@ const routes: Routes = [
},
{
path: 'address/:id',
component: AddressComponent
component: BisqAddressComponent,
},
{
path: 'about',

View File

@ -18,14 +18,9 @@ export class SeoService {
setTitle(newTitle: string, prependNetwork = false) {
let networkName = '';
if (prependNetwork) {
if (this.network === 'liquid') {
networkName = 'Liquid ';
} else if (this.network === 'testnet') {
networkName = 'Testnet ';
}
if (prependNetwork && this.network !== '') {
networkName = this.network.substr(0, 1).toUpperCase() + this.network.substr(1) + ' ';
}
this.titleService.setTitle(networkName + newTitle + ' - ' + this.defaultTitle);
}

View File

@ -10,6 +10,7 @@ import { BytesPipe } from './pipes/bytes-pipe/bytes.pipe';
import { WuBytesPipe } from './pipes/bytes-pipe/wubytes.pipe';
import { TimeSinceComponent } from '../components/time-since/time-since.component';
import { ClipboardComponent } from '../components/clipboard/clipboard.component';
import { QrcodeComponent } from '../components/qrcode/qrcode.component';
@NgModule({
declarations: [
@ -23,6 +24,7 @@ import { ClipboardComponent } from '../components/clipboard/clipboard.component'
ShortenStringPipe,
ClipboardComponent,
TimeSinceComponent,
QrcodeComponent,
],
imports: [
CommonModule,
@ -40,7 +42,8 @@ import { ClipboardComponent } from '../components/clipboard/clipboard.component'
CeilPipe,
ShortenStringPipe,
TimeSinceComponent,
ClipboardComponent
ClipboardComponent,
QrcodeComponent,
]
})
export class SharedModule {}