mirror of
https://github.com/mempool/mempool.git
synced 2025-03-03 09:39:17 +01:00
WIP: Bisq DAO support. Transactions list and details.
This commit is contained in:
parent
2ebdb27dcb
commit
c21dad88bf
59 changed files with 926 additions and 38 deletions
1
backend/.gitignore
vendored
1
backend/.gitignore
vendored
|
@ -43,3 +43,4 @@ testem.log
|
|||
Thumbs.db
|
||||
|
||||
cache.json
|
||||
blocks.json
|
||||
|
|
|
@ -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
78
backend/src/api/bisq.ts
Normal 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();
|
|
@ -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) {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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)
|
||||
;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"TESTNET_ENABLED": false,
|
||||
"LIQUID_ENABLED": false,
|
||||
"BISQ_ENABLED": false,
|
||||
"ELCTRS_ITEMS_PER_PAGE": 25,
|
||||
"KEEP_BLOCKS_AMOUNT": 8
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -179,6 +179,11 @@ const routes: Routes = [
|
|||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'bisq',
|
||||
component: MasterPageComponent,
|
||||
loadChildren: () => import('./bisq/bisq.module').then(m => m.BisqModule)
|
||||
},
|
||||
{
|
||||
path: 'tv',
|
||||
component: TelevisionComponent,
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
],
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
|
|
|
@ -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>
|
|
@ -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);
|
||||
}
|
||||
}
|
19
frontend/src/app/bisq/bisq.module.ts
Normal file
19
frontend/src/app/bisq/bisq.module.ts
Normal 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 { }
|
58
frontend/src/app/bisq/bisq.routing.module.ts
Normal file
58
frontend/src/app/bisq/bisq.routing.module.ts
Normal 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 { }
|
|
@ -0,0 +1 @@
|
|||
<fa-icon [icon]="iconProp" [fixedWidth]="true" [ngStyle]="{ 'color': '#' + color }"></fa-icon>
|
81
frontend/src/app/components/bisq-icon/bisq-icon.component.ts
Normal file
81
frontend/src/app/components/bisq-icon/bisq-icon.component.ts
Normal 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';
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -26,6 +26,7 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy {
|
|||
|
||||
gradientColors = {
|
||||
'': ['#9339f4', '#105fb0'],
|
||||
bisq: ['#9339f4', '#105fb0'],
|
||||
liquid: ['#116761', '#183550'],
|
||||
testnet: ['#1d486f', '#183550'],
|
||||
};
|
||||
|
|
|
@ -17,6 +17,5 @@ export class BlockchainComponent implements OnInit {
|
|||
|
||||
ngOnInit() {
|
||||
this.stateService.blocks$.subscribe(() => this.isLoading = false);
|
||||
this.stateService.networkChanged$.subscribe(() => this.isLoading = true);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
74
frontend/src/app/interfaces/bisq.interfaces.ts
Normal file
74
frontend/src/app/interfaces/bisq.interfaces.ts
Normal 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;
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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 = '';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
60
frontend/src/app/shared/shared.module.ts
Normal file
60
frontend/src/app/shared/shared.module.ts
Normal 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);
|
||||
}
|
||||
}
|
BIN
frontend/src/resources/bisq-logo.png
Normal file
BIN
frontend/src/resources/bisq-logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.1 KiB |
|
@ -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"
|
||||
|
|
Loading…
Add table
Reference in a new issue