WIP: Bisq DAO support. Transactions list and details.

This commit is contained in:
softsimon 2020-07-03 23:45:19 +07:00
parent 2ebdb27dcb
commit c21dad88bf
No known key found for this signature in database
GPG key ID: 488D7DCFB5A430D7
59 changed files with 926 additions and 38 deletions

1
backend/.gitignore vendored
View file

@ -43,3 +43,4 @@ testem.log
Thumbs.db
cache.json
blocks.json

View file

@ -13,6 +13,7 @@
"INITIAL_BLOCK_AMOUNT": 8,
"TX_PER_SECOND_SPAN_SECONDS": 150,
"ELECTRS_API_URL": "https://www.blockstream.info/testnet/api",
"BISQ_ENABLED": false,
"SSL": false,
"SSL_CERT_FILE_PATH": "/etc/letsencrypt/live/mysite/fullchain.pem",
"SSL_KEY_FILE_PATH": "/etc/letsencrypt/live/mysite/privkey.pem"

78
backend/src/api/bisq.ts Normal file
View file

@ -0,0 +1,78 @@
import * as fs from 'fs';
import { BisqBlocks, BisqBlock, BisqTransaction } from '../interfaces';
class Bisq {
static FILE_NAME = './blocks.json';
private latestBlockHeight = 0;
private blocks: BisqBlock[] = [];
private transactions: BisqTransaction[] = [];
private transactionsIndex: { [txId: string]: BisqTransaction } = {};
private blocksIndex: { [hash: string]: BisqBlock } = {};
constructor() {}
startBisqService(): void {
this.loadBisqDumpFile();
}
async loadBisqDumpFile(): Promise<void> {
await this.loadBisqBlocksDump();
this.buildIndex();
}
getTransaction(txId: string): BisqTransaction | undefined {
return this.transactionsIndex[txId];
}
getTransactions(start: number, length: number): [BisqTransaction[], number] {
return [this.transactions.slice(start, length + start), this.transactions.length];
}
getBlock(hash: string): BisqBlock | undefined {
return this.blocksIndex[hash];
}
private buildIndex() {
this.blocks.forEach((block) => {
this.blocksIndex[block.hash] = block;
block.txs.forEach((tx) => {
this.transactions.push(tx);
this.transactionsIndex[tx.id] = tx;
});
});
this.blocks.reverse();
this.transactions.reverse();
console.log('Bisq data index rebuilt');
}
private async loadBisqBlocksDump() {
const start = new Date().getTime();
const cacheData = await this.loadData();
if (cacheData) {
console.log('Parsing Bisq data from dump file');
const data: BisqBlocks = JSON.parse(cacheData);
if (data.blocks) {
this.blocks = data.blocks;
this.latestBlockHeight = data.chainHeight;
const end = new Date().getTime();
const time = end - start;
console.log('Loaded bisq dump in ' + time + ' ms');
} else {
throw new Error(`Bisq dump didn't contain any blocks`);
}
}
}
private loadData(): Promise<string> {
return new Promise((resolve, reject) => {
fs.readFile(Bisq.FILE_NAME, 'utf8', (err, data) => {
if (err) {
reject(err);
}
resolve(data);
});
});
}
}
export default new Bisq();

View file

@ -73,10 +73,10 @@ class Blocks {
console.log(`${found} of ${txIds.length} found in mempool. ${notFound} not found.`);
block.reward = transactions[0].vout.reduce((acc, curr) => acc + curr.value, 0);
block.coinbaseTx = this.stripCoinbaseTransaction(transactions[0]);
transactions.sort((a, b) => b.feePerVsize - a.feePerVsize);
block.medianFee = transactions.length > 1 ? Common.median(transactions.map((tx) => tx.feePerVsize)) : 0;
block.feeRange = transactions.length > 1 ? Common.getFeesInRange(transactions, 8, 1) : [0, 0];
block.coinbaseTx = this.stripCoinbaseTransaction(transactions[0]);
this.blocks.push(block);
if (this.blocks.length > config.KEEP_BLOCK_AMOUNT) {

View file

@ -35,6 +35,9 @@ class Mempool {
public setMempool(mempoolData: { [txId: string]: TransactionExtended }) {
this.mempoolCache = mempoolData;
if (this.mempoolChangedCallback) {
this.mempoolChangedCallback(this.mempoolCache, [], []);
}
}
public async updateMemPoolInfo() {

View file

@ -14,6 +14,7 @@ import diskCache from './api/disk-cache';
import statistics from './api/statistics';
import websocketHandler from './api/websocket-handler';
import fiatConversion from './api/fiat-conversion';
import bisq from './api/bisq';
class Server {
wss: WebSocket.Server;
@ -50,6 +51,10 @@ class Server {
fiatConversion.startService();
diskCache.loadMempoolCache();
if (config.BISQ_ENABLED) {
bisq.startBisqService();
}
this.server.listen(config.HTTP_PORT, () => {
console.log(`Server started on port ${config.HTTP_PORT}`);
});
@ -84,6 +89,14 @@ class Server {
.get(config.API_ENDPOINT + 'statistics/1y', routes.get1YStatistics.bind(routes))
.get(config.API_ENDPOINT + 'backend-info', routes.getBackendInfo)
;
if (config.BISQ_ENABLED) {
this.app
.get(config.API_ENDPOINT + 'bisq/tx/:txId', routes.getBisqTransaction)
.get(config.API_ENDPOINT + 'bisq/block/:hash', routes.getBisqBlock)
.get(config.API_ENDPOINT + 'bisq/txs/:index/:length', routes.getBisqTransactions)
;
}
}
}

View file

@ -230,3 +230,77 @@ export interface VbytesPerSecond {
unixTime: number;
vSize: number;
}
export interface BisqBlocks {
chainHeight: number;
blocks: BisqBlock[];
}
export interface BisqBlock {
height: number;
time: number;
hash: string;
previousBlockHash: string;
txs: BisqTransaction[];
}
export interface BisqTransaction {
txVersion: string;
id: string;
blockHeight: number;
blockHash: string;
time: number;
inputs: BisqInput[];
outputs: BisqOutput[];
txType: string;
txTypeDisplayString: string;
burntFee: number;
invalidatedBsq: number;
unlockBlockHeight: number;
}
interface BisqInput {
spendingTxOutputIndex: number;
spendingTxId: string;
bsqAmount: number;
isVerified: boolean;
address: string;
time: number;
}
interface BisqOutput {
txVersion: string;
txId: string;
index: number;
bsqAmount: number;
btcAmount: number;
height: number;
isVerified: boolean;
burntFee: number;
invalidatedBsq: number;
address: string;
scriptPubKey: BisqScriptPubKey;
time: any;
txType: string;
txTypeDisplayString: string;
txOutputType: string;
txOutputTypeDisplayString: string;
lockTime: number;
isUnspent: boolean;
spentInfo: SpentInfo;
opReturn?: string;
}
interface BisqScriptPubKey {
addresses: string[];
asm: string;
hex: string;
reqSigs: number;
type: string;
}
interface SpentInfo {
height: number;
inputIndex: number;
txId: string;
}

View file

@ -4,6 +4,7 @@ import feeApi from './api/fee-api';
import backendInfo from './api/backend-info';
import mempoolBlocks from './api/mempool-blocks';
import mempool from './api/mempool';
import bisq from './api/bisq';
class Routes {
private cache = {};
@ -85,6 +86,36 @@ class Routes {
public getBackendInfo(req: Request, res: Response) {
res.send(backendInfo.getBackendInfo());
}
public getBisqTransaction(req: Request, res: Response) {
const result = bisq.getTransaction(req.params.txId);
if (result) {
res.send(result);
} else {
res.status(404).send('Bisq transaction not found');
}
}
public getBisqTransactions(req: Request, res: Response) {
const index = parseInt(req.params.index, 10) || 0;
const length = parseInt(req.params.length, 10) > 100 ? 100 : parseInt(req.params.length, 10) || 25;
const [transactions, count] = bisq.getTransactions(index, length);
if (transactions) {
res.header('X-Total-Count', count.toString());
res.send(transactions);
} else {
res.status(404).send('Bisq transaction not found');
}
}
public getBisqBlock(req: Request, res: Response) {
const result = bisq.getBlock(req['hash']);
if (result) {
res.send(result);
} else {
res.status(404).send('Bisq block not found');
}
}
}
export default new Routes();

View file

@ -1,6 +1,7 @@
{
"TESTNET_ENABLED": false,
"LIQUID_ENABLED": false,
"BISQ_ENABLED": false,
"ELCTRS_ITEMS_PER_PAGE": 25,
"KEEP_BLOCKS_AMOUNT": 8
}

View file

@ -40,6 +40,10 @@
"@angular/platform-browser": "~9.1.0",
"@angular/platform-browser-dynamic": "~9.1.0",
"@angular/router": "~9.1.0",
"@fortawesome/angular-fontawesome": "^0.6.1",
"@fortawesome/fontawesome-common-types": "^0.2.29",
"@fortawesome/fontawesome-svg-core": "^1.2.28",
"@fortawesome/free-solid-svg-icons": "^5.13.0",
"@ng-bootstrap/ng-bootstrap": "^6.1.0",
"@types/qrcode": "^1.3.4",
"bootstrap": "4.5.0",

View file

@ -179,6 +179,11 @@ const routes: Routes = [
},
]
},
{
path: 'bisq',
component: MasterPageComponent,
loadChildren: () => import('./bisq/bisq.module').then(m => m.BisqModule)
},
{
path: 'tv',
component: TelevisionComponent,

View file

@ -37,13 +37,15 @@ export const feeLevels = [1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 30, 40, 50, 60, 7
interface Env {
TESTNET_ENABLED: boolean;
LIQUID_ENABLED: boolean;
BISQ_ENABLED: boolean;
ELCTRS_ITEMS_PER_PAGE: number;
KEEP_BLOCKS_AMOUNT: number;
};
}
const defaultEnv: Env = {
'TESTNET_ENABLED': false,
'LIQUID_ENABLED': false,
'BISQ_ENABLED': false,
'ELCTRS_ITEMS_PER_PAGE': 25,
'KEEP_BLOCKS_AMOUNT': 8
};

View file

@ -11,15 +11,11 @@ import { AppComponent } from './components/app/app.component';
import { StartComponent } from './components/start/start.component';
import { ElectrsApiService } from './services/electrs-api.service';
import { BytesPipe } from './pipes/bytes-pipe/bytes.pipe';
import { VbytesPipe } from './pipes/bytes-pipe/vbytes.pipe';
import { WuBytesPipe } from './pipes/bytes-pipe/wubytes.pipe';
import { TransactionComponent } from './components/transaction/transaction.component';
import { TransactionsListComponent } from './components/transactions-list/transactions-list.component';
import { AmountComponent } from './components/amount/amount.component';
import { StateService } from './services/state.service';
import { BlockComponent } from './components/block/block.component';
import { ShortenStringPipe } from './pipes/shorten-string-pipe/shorten-string.pipe';
import { AddressComponent } from './components/address/address.component';
import { SearchFormComponent } from './components/search-form/search-form.component';
import { LatestBlocksComponent } from './components/latest-blocks/latest-blocks.component';
@ -27,7 +23,6 @@ import { WebsocketService } from './services/websocket.service';
import { TimeSinceComponent } from './components/time-since/time-since.component';
import { AddressLabelsComponent } from './components/address-labels/address-labels.component';
import { MempoolBlocksComponent } from './components/mempool-blocks/mempool-blocks.component';
import { CeilPipe } from './pipes/math-ceil/math-ceil.pipe';
import { QrcodeComponent } from './components/qrcode/qrcode.component';
import { ClipboardComponent } from './components/clipboard/clipboard.component';
import { MasterPageComponent } from './components/master-page/master-page.component';
@ -46,12 +41,12 @@ 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';
import { AssetsComponent } from './assets/assets.component';
import { RelativeUrlPipe } from './pipes/relative-url/relative-url.pipe';
import { MinerComponent } from './pipes/miner/miner.component';
import { Hex2asciiPipe } from './pipes/hex2ascii/hex2ascii.pipe';
import { StatusViewComponent } from './components/status-view/status-view.component';
import { MinerComponent } from './components/miner/miner.component';
import { SharedModule } from './shared/shared.module';
import { BisqTransfersComponent } from './components/bisq-transfers/bisq-transfers.component';
import { BisqTransactionDetailsComponent } from './components/bisq-transaction-details/bisq-transaction-details.component';
@NgModule({
declarations: [
@ -66,11 +61,6 @@ import { StatusViewComponent } from './components/status-view/status-view.compon
TransactionComponent,
BlockComponent,
TransactionsListComponent,
BytesPipe,
VbytesPipe,
WuBytesPipe,
CeilPipe,
ShortenStringPipe,
AddressComponent,
AmountComponent,
SearchFormComponent,
@ -88,12 +78,11 @@ import { StatusViewComponent } from './components/status-view/status-view.compon
FeeDistributionGraphComponent,
MempoolGraphComponent,
AssetComponent,
ScriptpubkeyTypePipe,
AssetsComponent,
RelativeUrlPipe,
MinerComponent,
Hex2asciiPipe,
StatusViewComponent,
BisqTransfersComponent,
BisqTransactionDetailsComponent,
],
imports: [
BrowserModule,
@ -105,12 +94,12 @@ import { StatusViewComponent } from './components/status-view/status-view.compon
NgbTooltipModule,
NgbPaginationModule,
InfiniteScrollModule,
SharedModule,
],
providers: [
ElectrsApiService,
StateService,
WebsocketService,
VbytesPipe,
AudioService,
SeoService,
],

View file

@ -67,7 +67,7 @@ export class AssetsComponent implements OnInit {
});
this.assets = this.assets.sort((a: any, b: any) => a.name.localeCompare(b.name));
this.assetsCache = this.assets;
this.searchForm.controls['searchText'].enable();
this.searchForm.get('searchText').enable();
this.filteredAssets = this.assets.slice(0, this.itemsPerPage);
this.isLoading = false;
},

View file

@ -0,0 +1,32 @@
<div class="container-xl">
<h2 style="float: left;">Latest BSQ Transactions</h2>
<br>
<div class="clearfix"></div>
<table class="table table-borderless table-striped">
<thead>
<th>Transaction</th>
<th>Type</th>
<th>Total Sent (BSQ)</th>
<th>Outputs</th>
<th>Block Height</th>
<th>Block Time</th>
</thead>
<tbody>
<tr *ngFor="let tx of transactions">
<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>{{ calculateTotalOutput(tx.outputs) / 100 | number: '1.2-2' }}</td>
<td>{{ tx.outputs.length }}</td>
<td><a [routerLink]="['/block/' | relativeUrl, tx.blockHash]">{{ tx.blockHeight }}</a></td>
<td>{{ tx.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>

View file

@ -0,0 +1,50 @@
import { Component, OnInit } from '@angular/core';
import { BisqTransaction, BisqOutput } from '../../interfaces/bisq.interfaces';
import { Subject } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import { ApiService } from 'src/app/services/api.service';
@Component({
selector: 'app-bisq-transactions',
templateUrl: './bisq-transactions.component.html',
styleUrls: ['./bisq-transactions.component.scss']
})
export class BisqTransactionsComponent implements OnInit {
transactions: BisqTransaction[];
totalCount: number;
page = 1;
itemsPerPage: number;
contentSpace = window.innerHeight - (200 + 200);
fiveItemsPxSize = 250;
pageSubject$ = new Subject<number>();
constructor(
private apiService: ApiService,
) { }
ngOnInit(): void {
this.itemsPerPage = Math.max(Math.round(this.contentSpace / this.fiveItemsPxSize) * 5, 10);
this.pageSubject$
.pipe(
switchMap((page) => this.apiService.listBisqTransactions$((page - 1) * 10, this.itemsPerPage))
)
.subscribe((response) => {
this.transactions = response.body;
this.totalCount = parseInt(response.headers.get('x-total-count'), 10);
}, (error) => {
console.log(error);
});
this.pageSubject$.next(1);
}
pageChange(page: number) {
this.pageSubject$.next(page);
}
calculateTotalOutput(outputs: BisqOutput[]): number {
return outputs.reduce((acc: number, output: BisqOutput) => acc + output.bsqAmount, 0);
}
}

View file

@ -0,0 +1,19 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { BisqRoutingModule } from './bisq.routing.module';
import { SharedModule } from '../shared/shared.module';
import { BisqTransactionsComponent } from './bisq-transactions/bisq-transactions.component';
import { NgbPaginationModule } from '@ng-bootstrap/ng-bootstrap';
@NgModule({
declarations: [
BisqTransactionsComponent,
],
imports: [
CommonModule,
BisqRoutingModule,
SharedModule,
NgbPaginationModule,
],
})
export class BisqModule { }

View file

@ -0,0 +1,58 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { StartComponent } from '../components/start/start.component';
import { TransactionComponent } from '../components/transaction/transaction.component';
import { BlockComponent } from '../components/block/block.component';
import { MempoolBlockComponent } from '../components/mempool-block/mempool-block.component';
import { AboutComponent } from '../components/about/about.component';
import { AddressComponent } from '../components/address/address.component';
import { BisqTransactionsComponent } from './bisq-transactions/bisq-transactions.component';
import { StatisticsComponent } from '../components/statistics/statistics.component';
const routes: Routes = [
{
path: '',
component: StartComponent,
children: [
{
path: '',
component: BisqTransactionsComponent
},
{
path: 'tx/:id',
component: TransactionComponent
},
{
path: 'block/:id',
component: BlockComponent
},
{
path: 'mempool-block/:id',
component: MempoolBlockComponent
},
],
},
{
path: 'graphs',
component: StatisticsComponent,
},
{
path: 'about',
component: AboutComponent,
},
{
path: 'address/:id',
children: [],
component: AddressComponent
},
{
path: '**',
redirectTo: ''
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class BisqRoutingModule { }

View file

@ -0,0 +1 @@
<fa-icon [icon]="iconProp" [fixedWidth]="true" [ngStyle]="{ 'color': '#' + color }"></fa-icon>

View file

@ -0,0 +1,81 @@
import { Component, ChangeDetectionStrategy, OnInit, Input } from '@angular/core';
import { IconPrefix, IconName } from '@fortawesome/fontawesome-common-types';
@Component({
selector: 'app-bisq-icon',
templateUrl: './bisq-icon.component.html',
styleUrls: ['./bisq-icon.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class BisqIconComponent implements OnInit {
@Input() txType: string;
iconProp: [IconPrefix, IconName] = ['fas', 'leaf'];
color: string;
constructor() { }
ngOnInit() {
switch (this.txType) {
case 'UNVERIFIED':
this.iconProp[1] = 'question';
this.color = 'ffac00';
break;
case 'INVALID':
this.iconProp[1] = 'exclamation-triangle';
this.color = 'ff4500';
break;
case 'GENESIS':
this.iconProp[1] = 'rocket';
this.color = '25B135';
break;
case 'TRANSFER_BSQ':
this.iconProp[1] = 'retweet';
this.color = 'a3a3a3';
break;
case 'PAY_TRADE_FEE':
this.iconProp[1] = 'leaf';
this.color = '689f43';
break;
case 'PROPOSAL':
this.iconProp[1] = 'file-alt';
this.color = '6c8b3b';
break;
case 'COMPENSATION_REQUEST':
this.iconProp[1] = 'money-bill';
this.color = '689f43';
break;
case 'REIMBURSEMENT_REQUEST':
this.iconProp[1] = 'money-bill';
this.color = '04a908';
break;
case 'BLIND_VOTE':
this.iconProp[1] = 'eye-slash';
this.color = '07579a';
break;
case 'VOTE_REVEAL':
this.iconProp[1] = 'eye';
this.color = '4AC5FF';
break;
case 'LOCKUP':
this.iconProp[1] = 'lock';
this.color = '0056c4';
break;
case 'UNLOCK':
this.iconProp[1] = 'lock-open';
this.color = '1d965f';
break;
case 'ASSET_LISTING_FEE':
this.iconProp[1] = 'file-alt';
this.color = '6c8b3b';
break;
case 'PROOF_OF_BURN':
this.iconProp[1] = 'file-alt';
this.color = '6c8b3b';
break;
default:
this.iconProp[1] = 'question';
this.color = 'ffac00';
}
}
}

View file

@ -0,0 +1,40 @@
<div class="box">
<div class="row">
<div class="col-sm">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td>Inputs</td>
<td>{{ totalInput / 100 | number: '1.2-2' }} BSQ</td>
</tr>
<tr>
<td>Outputs</td>
<td>{{ totalOutput / 100 | number: '1.2-2' }} BSQ</td>
</tr>
<tr>
<td>Burnt</td>
<td>{{ tx.burntFee / 100 | number: '1.2-2' }} BSQ</td>
</tr>
<tr>
<td>Issuance</td>
<td>{{ totalIssued / 100 | number: '1.2-2' }} BSQ</td>
</tr>
</tbody>
</table>
</div>
<div class="col-sm">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td>Type</td>
<td><app-bisq-icon class="mr-1" [txType]="tx.txType"></app-bisq-icon> {{ tx.txTypeDisplayString }}</td>
</tr>
<tr>
<td>Version</td>
<td>{{ tx.txVersion }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>

View file

@ -0,0 +1,26 @@
import { Component, ChangeDetectionStrategy, Input, OnChanges } from '@angular/core';
import { BisqTransaction } from 'src/app/interfaces/bisq.interfaces';
@Component({
selector: 'app-bisq-transaction-details',
templateUrl: './bisq-transaction-details.component.html',
styleUrls: ['./bisq-transaction-details.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class BisqTransactionDetailsComponent implements OnChanges {
@Input() tx: BisqTransaction;
totalInput: number;
totalOutput: number;
totalIssued: number;
constructor() { }
ngOnChanges() {
this.totalInput = this.tx.inputs.filter((input) => input.isVerified).reduce((acc, input) => acc + input.bsqAmount, 0);
this.totalOutput = this.tx.outputs.filter((output) => output.isVerified).reduce((acc, output) => acc + output.bsqAmount, 0);
this.totalIssued = this.tx.outputs
.filter((output) => output.isVerified && output.txOutputType === 'ISSUANCE_CANDIDATE_OUTPUT')
.reduce((acc, output) => acc + output.bsqAmount, 0);
}
}

View file

@ -0,0 +1,60 @@
<div class="header-bg box">
<div class="row">
<div class="col">
<table class="table table-borderless smaller-text table-xs" style="margin: 0;">
<tbody>
<ng-template ngFor let-input [ngForOf]="tx.inputs" [ngForTrackBy]="trackByIndexFn">
<tr *ngIf="input.isVerified">
<td class="arrow-td">
<ng-template [ngIf]="input.spendingTxId === null" [ngIfElse]="hasPreoutput">
<i class="arrow grey"></i>
</ng-template>
<ng-template #hasPreoutput>
<a [routerLink]="['/tx/' | relativeUrl, input.spendingTxId]">
<i class="arrow red"></i>
</a>
</ng-template>
</td>
<td>
<a [routerLink]="['/address/' | relativeUrl, input.address]" title="{{ 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>
</td>
<td class="text-right nowrap">
{{ input.bsqAmount / 100 | number: '1.2-2' }} BSQ
</td>
</tr>
</ng-template>
</tbody>
</table>
</div>
<div class="w-100 d-block d-md-none"></div>
<div class="col mobile-bottomcol">
<table class="table table-borderless smaller-text table-xs" style="margin: 0;">
<tbody>
<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 }}">
<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>
</td>
<td class="text-right nowrap">
{{ output.bsqAmount / 100 | number: '1.2-2' }} BSQ
</td>
<td class="pl-1 arrow-td">
<i *ngIf="!output.spentInfo; else spent" class="arrow green"></i>
<ng-template #spent>
<a [routerLink]="['/tx/' | relativeUrl, output.spentInfo.txId]"><i class="arrow red"></i></a>
</ng-template>
</td>
</tr>
</ng-template>
</tbody>
</table>
</div>
</div>
</div>

View file

@ -0,0 +1,84 @@
.arrow-td {
width: 22px;
}
.arrow {
display: inline-block!important;
position: relative;
width: 14px;
height: 22px;
box-sizing: content-box
}
.arrow:before {
position: absolute;
content: '';
margin: auto;
top: 0;
bottom: 0;
left: 0;
right: calc(-1*30px/3);
width: 0;
height: 0;
border-top: 6.66px solid transparent;
border-bottom: 6.66px solid transparent
}
.arrow:after {
position: absolute;
content: '';
margin: auto;
top: 0;
bottom: 0;
left: 0;
right: calc(30px/6);
width: calc(30px/3);
height: calc(20px/3);
background: rgba(0, 0, 0, 0);
}
.arrow.green:before {
border-left: 10px solid #28a745;
}
.arrow.green:after {
background-color:#28a745;
}
.arrow.red:before {
border-left: 10px solid #dc3545;
}
.arrow.red:after {
background-color:#dc3545;
}
.arrow.grey:before {
border-left: 10px solid #6c757d;
}
.arrow.grey:after {
background-color:#6c757d;
}
.scriptmessage {
max-width: 280px;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: middle;
}
.scriptmessage.longer {
max-width: 500px;
}
@media (max-width: 767.98px) {
.mobile-bottomcol {
margin-top: 15px;
}
.scriptmessage {
max-width: 90px !important;
}
.scriptmessage.longer {
max-width: 280px !important;
}
}

View file

@ -0,0 +1,19 @@
import { Component, OnInit, ChangeDetectionStrategy, Input } from '@angular/core';
import { BisqTransaction } from 'src/app/interfaces/bisq.interfaces';
@Component({
selector: 'app-bisq-transfers',
templateUrl: './bisq-transfers.component.html',
styleUrls: ['./bisq-transfers.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class BisqTransfersComponent {
@Input() tx: BisqTransaction;
constructor() { }
trackByIndexFn(index: number) {
return index;
}
}

View file

@ -26,6 +26,7 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy {
gradientColors = {
'': ['#9339f4', '#105fb0'],
bisq: ['#9339f4', '#105fb0'],
liquid: ['#116761', '#183550'],
testnet: ['#1d486f', '#183550'],
};

View file

@ -17,6 +17,5 @@ export class BlockchainComponent implements OnInit {
ngOnInit() {
this.stateService.blocks$.subscribe(() => this.isLoading = false);
this.stateService.networkChanged$.subscribe(() => this.isLoading = true);
}
}

View file

@ -1,18 +1,19 @@
<header>
<nav class="navbar navbar-expand-md navbar-dark bg-dark">
<a class="navbar-brand" routerLink="/" style="position: relative;">
<a class="navbar-brand" [routerLink]="['/' | relativeUrl]" style="position: relative;">
<img src="./resources/mempool-logo.png" height="35" width="140" class="logo" [ngStyle]="{'opacity': connectionState === 2 ? 1 : 0.5 }">
<div class="badge badge-warning connection-badge" *ngIf="connectionState === 0">Offline</div>
<div class="badge badge-warning connection-badge" style="left: 30px;" *ngIf="connectionState === 1">Reconnecting...</div>
</a>
<div class="btn-group" style="margin-right: 16px;" *ngIf="env.TESTNET_ENABLED || env.LIQUID_ENABLED">
<div class="btn-group" style="margin-right: 16px;" *ngIf="env.TESTNET_ENABLED || env.LIQUID_ENABLED || env.BISQ_ENABLED">
<button type="button" (click)="networkDropdownHidden = !networkDropdownHidden" class="btn btn-secondary dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="sr-only">Toggle Dropdown</span>
</button>
<div class="dropdown-menu" [class.d-block]="!networkDropdownHidden">
<a class="dropdown-item mainnet" [class.active]="network === ''" routerLink="/"><img src="./resources/bitcoin-logo.png" style="width: 35.5px;"> Mainnet</a>
<a class="dropdown-item mainnet" routerLink="/"><img src="./resources/bitcoin-logo.png" style="width: 35.5px;"> Mainnet</a>
<a *ngIf="env.LIQUID_ENABLED" class="dropdown-item liquid" [class.active]="network === 'liquid'" routerLink="/liquid"><img src="./resources/liquid-logo.png" style="width: 35.5px;"> Liquid</a>
<a *ngIf="env.BISQ_ENABLED" class="dropdown-item mainnet" [class.active]="network === 'bisq'" routerLink="/bisq"><img src="./resources/bisq-logo.png" style="width: 35.5px;"> Bisq</a>
<a *ngIf="env.TESTNET_ENABLED" class="dropdown-item testnet" [class.active]="network === 'testnet'" routerLink="/testnet"><img src="./resources/testnet-logo.png" style="width: 35.5px;"> Testnet</a>
</div>
</div>

View file

@ -37,14 +37,6 @@ export class MasterPageComponent implements OnInit {
this.stateService.networkChanged$
.subscribe((network) => {
this.network = network;
if (network === 'testnet') {
this.tvViewRoute = '/testnet-tv';
} else if (network === 'liquid') {
this.tvViewRoute = '/liquid-tv';
} else {
this.tvViewRoute = '/tv';
}
});
}

View file

@ -1,6 +1,6 @@
import { Component, OnInit, Input, Inject, LOCALE_ID, ChangeDetectionStrategy, OnChanges } from '@angular/core';
import { formatDate } from '@angular/common';
import { VbytesPipe } from 'src/app/pipes/bytes-pipe/vbytes.pipe';
import { VbytesPipe } from 'src/app/shared/pipes/bytes-pipe/vbytes.pipe';
import * as Chartist from 'chartist';
import { OptimizedMempoolStats } from 'src/app/interfaces/node-api.interface';
import { StateService } from 'src/app/services/state.service';

View file

@ -148,6 +148,21 @@
<br>
<ng-template [ngIf]="bisqTx">
<h2>BSQ Information</h2>
<app-bisq-transaction-details [tx]="bisqTx"></app-bisq-transaction-details>
<br>
<h2>BSQ transfers</h2>
<app-bisq-transfers [tx]="bisqTx"></app-bisq-transfers>
<br>
</ng-template>
<h2>Inputs & Outputs</h2>
<app-transactions-list [transactions]="[tx]" [transactionPage]="true"></app-transactions-list>

View file

@ -10,6 +10,7 @@ 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 { calcSegwitFeeGains } from 'src/app/bitcoin.utils';
import { BisqTransaction } from 'src/app/interfaces/bisq.interfaces';
@Component({
selector: 'app-transaction',
@ -37,6 +38,7 @@ export class TransactionComponent implements OnInit, OnDestroy {
};
isRbfTransaction: boolean;
rbfTransaction: undefined | Transaction;
bisqTx: BisqTransaction;
constructor(
private route: ActivatedRoute,
@ -90,6 +92,10 @@ export class TransactionComponent implements OnInit, OnDestroy {
this.segwitGains = calcSegwitFeeGains(tx);
this.isRbfTransaction = tx.vin.some((v) => v.sequence < 0xfffffffe);
if (this.network === 'bisq') {
this.loadBisqTransaction();
}
if (!tx.status.confirmed) {
this.websocketService.startTrackTransaction(tx.txid);
@ -133,6 +139,17 @@ export class TransactionComponent implements OnInit, OnDestroy {
.subscribe((rbfTransaction) => this.rbfTransaction = rbfTransaction);
}
loadBisqTransaction() {
if (history.state.bsqTx) {
this.bisqTx = history.state.bsqTx;
} else {
this.apiService.getBisqTransaction$(this.txId)
.subscribe((tx) => {
this.bisqTx = tx;
});
}
}
handleLoadElectrsTransactionError(error: any): Observable<any> {
if (error.status === 404 && /^[a-fA-F0-9]{64}$/.test(this.txId)) {
this.websocketService.startMultiTrackTransaction(this.txId);
@ -204,6 +221,7 @@ export class TransactionComponent implements OnInit, OnDestroy {
this.waitingForTransaction = false;
this.isLoadingTx = true;
this.rbfTransaction = undefined;
this.bisqTx = undefined;
this.transactionTime = -1;
document.body.scrollTo(0, 0);
this.leaveTransaction();

View file

@ -17,7 +17,7 @@
<div class="col">
<table class="table table-borderless smaller-text table-xs" style="margin: 0;">
<tbody>
<tr *ngFor="let vin of getFilteredTxVin(tx)">
<tr *ngFor="let vin of getFilteredTxVin(tx); trackBy: trackByIndexFn">
<td class="arrow-td">
<ng-template [ngIf]="vin.prevout === null && !vin.is_pegin" [ngIfElse]="hasPrevout">
<i class="arrow grey"></i>
@ -73,7 +73,7 @@
<div class="col mobile-bottomcol">
<table class="table table-borderless smaller-text table-xs" style="margin: 0;">
<tbody>
<tr *ngFor="let vout of getFilteredTxVout(tx); let vindex = index;">
<tr *ngFor="let vout of getFilteredTxVout(tx); let vindex = index; trackBy: trackByIndexFn">
<td>
<a *ngIf="vout.scriptpubkey_address; else scriptpubkey_type" [routerLink]="['/address/' | relativeUrl, vout.scriptpubkey_address]" title="{{ vout.scriptpubkey_address }}">
<span class="d-block d-lg-none">{{ vout.scriptpubkey_address | shortenString : 16 }}</span>

View file

@ -109,4 +109,8 @@ export class TransactionsListComponent implements OnInit, OnChanges {
getFilteredTxVout(tx: Transaction) {
return tx.vout.slice(0, tx['@voutLength']);
}
trackByIndexFn(index: number) {
return index;
}
}

View file

@ -0,0 +1,74 @@
export interface BisqBlocks {
chainHeight: number;
blocks: BisqBlock[];
}
export interface BisqBlock {
height: number;
time: number;
hash: string;
previousBlockHash: string;
txs: BisqTransaction[];
}
export interface BisqTransaction {
txVersion: string;
id: string;
blockHeight: number;
blockHash: string;
time: number;
inputs: BisqInput[];
outputs: BisqOutput[];
txType: string;
txTypeDisplayString: string;
burntFee: number;
invalidatedBsq: number;
unlockBlockHeight: number;
}
interface BisqInput {
spendingTxOutputIndex: number;
spendingTxId: string;
bsqAmount: number;
isVerified: boolean;
address: string;
time: number;
}
export interface BisqOutput {
txVersion: string;
txId: string;
index: number;
bsqAmount: number;
btcAmount: number;
height: number;
isVerified: boolean;
burntFee: number;
invalidatedBsq: number;
address: string;
scriptPubKey: BisqScriptPubKey;
spentInfo?: SpentInfo;
time: any;
txType: string;
txTypeDisplayString: string;
txOutputType: string;
txOutputTypeDisplayString: string;
lockTime: number;
isUnspent: boolean;
opReturn?: string;
}
interface BisqScriptPubKey {
addresses: string[];
asm: string;
hex: string;
reqSigs: number;
type: string;
}
interface SpentInfo {
height: number;
inputIndex: number;
txId: string;
}

View file

@ -1,8 +1,9 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http';
import { OptimizedMempoolStats } from '../interfaces/node-api.interface';
import { Observable } from 'rxjs';
import { StateService } from './state.service';
import { BisqTransaction, BisqBlock } from '../interfaces/bisq.interfaces';
const API_BASE_URL = '/api{network}/v1';
@ -18,6 +19,9 @@ export class ApiService {
) {
this.apiBaseUrl = API_BASE_URL.replace('{network}', '');
this.stateService.networkChanged$.subscribe((network) => {
if (network === 'bisq') {
network = '';
}
this.apiBaseUrl = API_BASE_URL.replace('{network}', network ? '/' + network : '');
});
}
@ -57,4 +61,16 @@ export class ApiService {
});
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);
}
}

View file

@ -18,6 +18,9 @@ export class ElectrsApiService {
) {
this.apiBaseUrl = API_BASE_URL;
this.stateService.networkChanged$.subscribe((network) => {
if (network === 'bisq') {
network = '';
}
this.apiBaseUrl = API_BASE_URL + '/' + network;
});
}

View file

@ -64,6 +64,12 @@ export class StateService {
this.networkChanged$.next('testnet');
}
return;
case 'bisq':
if (this.network !== 'bisq') {
this.network = 'bisq';
this.networkChanged$.next('bisq');
}
return;
default:
if (this.network !== '') {
this.network = '';

View file

@ -29,11 +29,14 @@ export class WebsocketService {
constructor(
private stateService: StateService,
) {
this.network = this.stateService.network;
this.network = this.stateService.network === 'bisq' ? '' : this.stateService.network;
this.websocketSubject = webSocket<WebsocketResponse | any>(WEB_SOCKET_URL + '/' + this.network);
this.startSubscription();
this.stateService.networkChanged$.subscribe((network) => {
if (network === 'bisq') {
network = '';
}
if (network === this.network) {
return;
}

View file

@ -0,0 +1,60 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { VbytesPipe } from './pipes/bytes-pipe/vbytes.pipe';
import { ShortenStringPipe } from './pipes/shorten-string-pipe/shorten-string.pipe';
import { CeilPipe } from './pipes/math-ceil/math-ceil.pipe';
import { Hex2asciiPipe } from './pipes/hex2ascii/hex2ascii.pipe';
import { RelativeUrlPipe } from './pipes/relative-url/relative-url.pipe';
import { ScriptpubkeyTypePipe } from './pipes/scriptpubkey-type-pipe/scriptpubkey-type.pipe';
import { BytesPipe } from './pipes/bytes-pipe/bytes.pipe';
import { WuBytesPipe } from './pipes/bytes-pipe/wubytes.pipe';
import { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontawesome';
import { BisqIconComponent } from '../components/bisq-icon/bisq-icon.component';
import { faLeaf, faQuestion, faExclamationTriangle, faRocket, faRetweet, faFileAlt, faMoneyBill, faEye, faEyeSlash, faLock, faLockOpen } from '@fortawesome/free-solid-svg-icons';
@NgModule({
declarations: [
ScriptpubkeyTypePipe,
RelativeUrlPipe,
Hex2asciiPipe,
BytesPipe,
VbytesPipe,
WuBytesPipe,
CeilPipe,
ShortenStringPipe,
BisqIconComponent,
],
imports: [
CommonModule,
FontAwesomeModule,
],
providers: [
VbytesPipe,
],
exports: [
ScriptpubkeyTypePipe,
RelativeUrlPipe,
Hex2asciiPipe,
BytesPipe,
VbytesPipe,
WuBytesPipe,
CeilPipe,
ShortenStringPipe,
BisqIconComponent,
]
})
export class SharedModule {
constructor(library: FaIconLibrary) {
library.addIcons(faQuestion);
library.addIcons(faExclamationTriangle);
library.addIcons(faRocket);
library.addIcons(faRetweet);
library.addIcons(faLeaf);
library.addIcons(faFileAlt);
library.addIcons(faMoneyBill);
library.addIcons(faEye);
library.addIcons(faEyeSlash);
library.addIcons(faLock);
library.addIcons(faLockOpen);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

View file

@ -1189,6 +1189,30 @@
lodash "^4.17.13"
to-fast-properties "^2.0.0"
"@fortawesome/angular-fontawesome@^0.6.1":
version "0.6.1"
resolved "https://registry.yarnpkg.com/@fortawesome/angular-fontawesome/-/angular-fontawesome-0.6.1.tgz#1ebe5db16bfdd4be44bdde61f78c760eb4e219fa"
integrity sha512-ARQjtRuT+ZskzJDJKPwuiGO3+7nS0iyNLU/uHVJHfG4LwGJxwVIGldwg1SU957sra0Z0OtWEajHMhiS4vB9LwQ==
"@fortawesome/fontawesome-common-types@^0.2.29":
version "0.2.29"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.29.tgz#e1a456b643237462d390304cab6975ff3fd68397"
integrity sha512-cY+QfDTbZ7XVxzx7jxbC98Oxr/zc7R2QpTLqTxqlfyXDrAJjzi/xUIqAUsygELB62JIrbsWxtSRhayKFkGI7MA==
"@fortawesome/fontawesome-svg-core@^1.2.28":
version "1.2.29"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.29.tgz#34ef32824664534f9e4ef37982ebf286b899a189"
integrity sha512-xmPmP2t8qrdo8RyKihTkGb09RnZoc+7HFBCnr0/6ZhStdGDSLeEd7ajV181+2W29NWIFfylO13rU+s3fpy3cnA==
dependencies:
"@fortawesome/fontawesome-common-types" "^0.2.29"
"@fortawesome/free-solid-svg-icons@^5.13.0":
version "5.13.1"
resolved "https://registry.yarnpkg.com/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.13.1.tgz#010a846b718a0f110b3cd137d072639b4e8bd41a"
integrity sha512-LQH/0L1p4+rqtoSHa9qFYR84hpuRZKqaQ41cfBQx8b68p21zoWSekTAeA54I/2x9VlCHDLFlG74Nmdg4iTPQOg==
dependencies:
"@fortawesome/fontawesome-common-types" "^0.2.29"
"@istanbuljs/schema@^0.1.2":
version "0.1.2"
resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.2.tgz#26520bf09abe4a5644cd5414e37125a8954241dd"