WIP: Bisq DAO support. Transactions list and details.

This commit is contained in:
softsimon
2020-07-03 23:45:19 +07:00
parent 94ccd98d0a
commit d39b4a5c92
59 changed files with 926 additions and 38 deletions

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

@@ -0,0 +1,12 @@
<ng-template [ngIf]="loading" [ngIfElse]="done">
<span class="skeleton-loader"></span>
</ng-template>
<ng-template #done>
<ng-template [ngIf]="miner" [ngIfElse]="unknownMiner">
<a placement="bottom" [ngbTooltip]="title" [href]="url" target="_blank" class="badge badge-primary">{{ miner }}</a>
</ng-template>
<ng-template #unknownMiner>
<span class="badge badge-secondary">Unknown</span>
</ng-template>
</ng-template>

View File

@@ -0,0 +1,3 @@
.badge {
font-size: 14px;
}

View File

@@ -0,0 +1,69 @@
import { Component, Input, OnChanges } from '@angular/core';
import { AssetsService } from 'src/app/services/assets.service';
import { Transaction } from 'src/app/interfaces/electrs.interface';
@Component({
selector: 'app-miner',
templateUrl: './miner.component.html',
styleUrls: ['./miner.component.scss'],
})
export class MinerComponent implements OnChanges {
@Input() coinbaseTransaction: Transaction;
miner = '';
title = '';
url = '';
loading = true;
constructor(
private assetsService: AssetsService,
) { }
ngOnChanges() {
this.miner = '';
this.loading = true;
this.findMinerFromCoinbase();
}
findMinerFromCoinbase() {
if (this.coinbaseTransaction == null || this.coinbaseTransaction.vin == null || this.coinbaseTransaction.vin.length === 0) {
return null;
}
this.assetsService.getMiningPools$.subscribe((pools) => {
for (const vout of this.coinbaseTransaction.vout) {
if (!vout.scriptpubkey_address) {
continue;
}
if (pools.payout_addresses[vout.scriptpubkey_address]) {
this.miner = pools.payout_addresses[vout.scriptpubkey_address].name;
this.title = 'Identified by payout address: ' + vout.scriptpubkey_address;
this.url = pools.payout_addresses[vout.scriptpubkey_address].link;
break;
}
for (const tag in pools.coinbase_tags) {
if (pools.coinbase_tags.hasOwnProperty(tag)) {
const coinbaseAscii = this.hex2ascii(this.coinbaseTransaction.vin[0].scriptsig);
if (coinbaseAscii.indexOf(tag) > -1) {
this.miner = pools.coinbase_tags[tag].name;
this.title = 'Identified by coinbase tag: \'' + tag + '\'';
this.url = pools.coinbase_tags[tag].link;
break;
}
}
}
}
this.loading = false;
});
}
hex2ascii(hex: string) {
let str = '';
for (let i = 0; i < hex.length; i += 2) {
str += String.fromCharCode(parseInt(hex.substr(i, 2), 16));
}
return str;
}
}

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