From c21dad88bfca2e769a30d49c1ef49cafb9f903b8 Mon Sep 17 00:00:00 2001 From: softsimon Date: Fri, 3 Jul 2020 23:45:19 +0700 Subject: [PATCH 01/32] WIP: Bisq DAO support. Transactions list and details. --- backend/.gitignore | 1 + backend/mempool-config.sample.json | 1 + backend/src/api/bisq.ts | 78 ++++++++++++++++ backend/src/api/blocks.ts | 2 +- backend/src/api/mempool.ts | 3 + backend/src/index.ts | 13 +++ backend/src/interfaces.ts | 74 +++++++++++++++ backend/src/routes.ts | 31 +++++++ frontend/mempool-frontend-config.sample.json | 1 + frontend/package.json | 4 + frontend/src/app/app-routing.module.ts | 5 ++ frontend/src/app/app.constants.ts | 4 +- frontend/src/app/app.module.ts | 25 ++---- frontend/src/app/assets/assets.component.ts | 2 +- .../bisq-transactions.component.html | 32 +++++++ .../bisq-transactions.component.scss | 0 .../bisq-transactions.component.ts | 50 +++++++++++ frontend/src/app/bisq/bisq.module.ts | 19 ++++ frontend/src/app/bisq/bisq.routing.module.ts | 58 ++++++++++++ .../bisq-icon/bisq-icon.component.html | 1 + .../bisq-icon/bisq-icon.component.scss | 0 .../bisq-icon/bisq-icon.component.ts | 81 +++++++++++++++++ .../bisq-transaction-details.component.html | 40 +++++++++ .../bisq-transaction-details.component.scss | 0 .../bisq-transaction-details.component.ts | 26 ++++++ .../bisq-transfers.component.html | 60 +++++++++++++ .../bisq-transfers.component.scss | 84 ++++++++++++++++++ .../bisq-transfers.component.ts | 19 ++++ .../blockchain-blocks.component.ts | 1 + .../blockchain/blockchain.component.ts | 1 - .../master-page/master-page.component.html | 7 +- .../master-page/master-page.component.ts | 8 -- .../mempool-graph/mempool-graph.component.ts | 2 +- .../miner/miner.component.html | 0 .../miner/miner.component.scss | 0 .../miner/miner.component.ts | 0 .../transaction/transaction.component.html | 15 ++++ .../transaction/transaction.component.ts | 18 ++++ .../transactions-list.component.html | 4 +- .../transactions-list.component.ts | 4 + .../src/app/interfaces/bisq.interfaces.ts | 74 +++++++++++++++ frontend/src/app/services/api.service.ts | 18 +++- .../src/app/services/electrs-api.service.ts | 3 + frontend/src/app/services/state.service.ts | 6 ++ .../src/app/services/websocket.service.ts | 5 +- .../pipes/bytes-pipe/bytes.pipe.ts | 0 .../{ => shared}/pipes/bytes-pipe/utils.ts | 0 .../pipes/bytes-pipe/vbytes.pipe.ts | 0 .../pipes/bytes-pipe/wubytes.pipe.ts | 0 .../pipes/hex2ascii/hex2ascii.pipe.spec.ts | 0 .../pipes/hex2ascii/hex2ascii.pipe.ts | 0 .../pipes/math-ceil/math-ceil.pipe.ts | 0 .../relative-url/relative-url.pipe.spec.ts | 0 .../pipes/relative-url/relative-url.pipe.ts | 0 .../scriptpubkey-type.pipe.ts | 0 .../shorten-string.pipe.ts | 0 frontend/src/app/shared/shared.module.ts | 60 +++++++++++++ frontend/src/resources/bisq-logo.png | Bin 0 -> 7228 bytes frontend/yarn.lock | 24 +++++ 59 files changed, 926 insertions(+), 38 deletions(-) create mode 100644 backend/src/api/bisq.ts create mode 100644 frontend/src/app/bisq/bisq-transactions/bisq-transactions.component.html create mode 100644 frontend/src/app/bisq/bisq-transactions/bisq-transactions.component.scss create mode 100644 frontend/src/app/bisq/bisq-transactions/bisq-transactions.component.ts create mode 100644 frontend/src/app/bisq/bisq.module.ts create mode 100644 frontend/src/app/bisq/bisq.routing.module.ts create mode 100644 frontend/src/app/components/bisq-icon/bisq-icon.component.html create mode 100644 frontend/src/app/components/bisq-icon/bisq-icon.component.scss create mode 100644 frontend/src/app/components/bisq-icon/bisq-icon.component.ts create mode 100644 frontend/src/app/components/bisq-transaction-details/bisq-transaction-details.component.html create mode 100644 frontend/src/app/components/bisq-transaction-details/bisq-transaction-details.component.scss create mode 100644 frontend/src/app/components/bisq-transaction-details/bisq-transaction-details.component.ts create mode 100644 frontend/src/app/components/bisq-transfers/bisq-transfers.component.html create mode 100644 frontend/src/app/components/bisq-transfers/bisq-transfers.component.scss create mode 100644 frontend/src/app/components/bisq-transfers/bisq-transfers.component.ts rename frontend/src/app/{pipes => components}/miner/miner.component.html (100%) rename frontend/src/app/{pipes => components}/miner/miner.component.scss (100%) rename frontend/src/app/{pipes => components}/miner/miner.component.ts (100%) create mode 100644 frontend/src/app/interfaces/bisq.interfaces.ts rename frontend/src/app/{ => shared}/pipes/bytes-pipe/bytes.pipe.ts (100%) rename frontend/src/app/{ => shared}/pipes/bytes-pipe/utils.ts (100%) rename frontend/src/app/{ => shared}/pipes/bytes-pipe/vbytes.pipe.ts (100%) rename frontend/src/app/{ => shared}/pipes/bytes-pipe/wubytes.pipe.ts (100%) rename frontend/src/app/{ => shared}/pipes/hex2ascii/hex2ascii.pipe.spec.ts (100%) rename frontend/src/app/{ => shared}/pipes/hex2ascii/hex2ascii.pipe.ts (100%) rename frontend/src/app/{ => shared}/pipes/math-ceil/math-ceil.pipe.ts (100%) rename frontend/src/app/{ => shared}/pipes/relative-url/relative-url.pipe.spec.ts (100%) rename frontend/src/app/{ => shared}/pipes/relative-url/relative-url.pipe.ts (100%) rename frontend/src/app/{ => shared}/pipes/scriptpubkey-type-pipe/scriptpubkey-type.pipe.ts (100%) rename frontend/src/app/{ => shared}/pipes/shorten-string-pipe/shorten-string.pipe.ts (100%) create mode 100644 frontend/src/app/shared/shared.module.ts create mode 100644 frontend/src/resources/bisq-logo.png diff --git a/backend/.gitignore b/backend/.gitignore index ba8416a23..e0bd6b423 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -43,3 +43,4 @@ testem.log Thumbs.db cache.json +blocks.json diff --git a/backend/mempool-config.sample.json b/backend/mempool-config.sample.json index a8c5d13ef..11407a026 100644 --- a/backend/mempool-config.sample.json +++ b/backend/mempool-config.sample.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" diff --git a/backend/src/api/bisq.ts b/backend/src/api/bisq.ts new file mode 100644 index 000000000..31756bb86 --- /dev/null +++ b/backend/src/api/bisq.ts @@ -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 { + 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 { + return new Promise((resolve, reject) => { + fs.readFile(Bisq.FILE_NAME, 'utf8', (err, data) => { + if (err) { + reject(err); + } + resolve(data); + }); + }); + } +} + +export default new Bisq(); diff --git a/backend/src/api/blocks.ts b/backend/src/api/blocks.ts index 20eb6b607..8f67c0a1a 100644 --- a/backend/src/api/blocks.ts +++ b/backend/src/api/blocks.ts @@ -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) { diff --git a/backend/src/api/mempool.ts b/backend/src/api/mempool.ts index a425af982..05d310462 100644 --- a/backend/src/api/mempool.ts +++ b/backend/src/api/mempool.ts @@ -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() { diff --git a/backend/src/index.ts b/backend/src/index.ts index ab72d260b..eaa0e8b3e 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -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) + ; + } } } diff --git a/backend/src/interfaces.ts b/backend/src/interfaces.ts index 49ab65f65..5d19411b7 100644 --- a/backend/src/interfaces.ts +++ b/backend/src/interfaces.ts @@ -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; +} diff --git a/backend/src/routes.ts b/backend/src/routes.ts index 3240489fe..8655af188 100644 --- a/backend/src/routes.ts +++ b/backend/src/routes.ts @@ -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(); diff --git a/frontend/mempool-frontend-config.sample.json b/frontend/mempool-frontend-config.sample.json index 13ba1cd5d..579250e88 100644 --- a/frontend/mempool-frontend-config.sample.json +++ b/frontend/mempool-frontend-config.sample.json @@ -1,6 +1,7 @@ { "TESTNET_ENABLED": false, "LIQUID_ENABLED": false, + "BISQ_ENABLED": false, "ELCTRS_ITEMS_PER_PAGE": 25, "KEEP_BLOCKS_AMOUNT": 8 } \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index a7a4dc76c..6968fbf67 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index 443b6caa9..b401bbd4f 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -179,6 +179,11 @@ const routes: Routes = [ }, ] }, + { + path: 'bisq', + component: MasterPageComponent, + loadChildren: () => import('./bisq/bisq.module').then(m => m.BisqModule) + }, { path: 'tv', component: TelevisionComponent, diff --git a/frontend/src/app/app.constants.ts b/frontend/src/app/app.constants.ts index 76808bcb9..ab880cf9c 100644 --- a/frontend/src/app/app.constants.ts +++ b/frontend/src/app/app.constants.ts @@ -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 }; diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 72fd39741..ef8a9eafe 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -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, ], diff --git a/frontend/src/app/assets/assets.component.ts b/frontend/src/app/assets/assets.component.ts index 14c3146f5..3e003945d 100644 --- a/frontend/src/app/assets/assets.component.ts +++ b/frontend/src/app/assets/assets.component.ts @@ -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; }, diff --git a/frontend/src/app/bisq/bisq-transactions/bisq-transactions.component.html b/frontend/src/app/bisq/bisq-transactions/bisq-transactions.component.html new file mode 100644 index 000000000..8e713910a --- /dev/null +++ b/frontend/src/app/bisq/bisq-transactions/bisq-transactions.component.html @@ -0,0 +1,32 @@ +
+

Latest BSQ Transactions

+
+ +
+ + + + + + + + + + + + + + + + + + + + +
TransactionTypeTotal Sent (BSQ)OutputsBlock HeightBlock Time
{{ tx.id | shortenString : 16 }} {{ tx.txTypeDisplayString }}{{ calculateTotalOutput(tx.outputs) / 100 | number: '1.2-2' }}{{ tx.outputs.length }}{{ tx.blockHeight }}{{ tx.time | date:'yyyy-MM-dd HH:mm' }}
+ +
+ + + +
\ No newline at end of file diff --git a/frontend/src/app/bisq/bisq-transactions/bisq-transactions.component.scss b/frontend/src/app/bisq/bisq-transactions/bisq-transactions.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/app/bisq/bisq-transactions/bisq-transactions.component.ts b/frontend/src/app/bisq/bisq-transactions/bisq-transactions.component.ts new file mode 100644 index 000000000..80df5083b --- /dev/null +++ b/frontend/src/app/bisq/bisq-transactions/bisq-transactions.component.ts @@ -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(); + + 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); + } +} diff --git a/frontend/src/app/bisq/bisq.module.ts b/frontend/src/app/bisq/bisq.module.ts new file mode 100644 index 000000000..1b8f1295a --- /dev/null +++ b/frontend/src/app/bisq/bisq.module.ts @@ -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 { } diff --git a/frontend/src/app/bisq/bisq.routing.module.ts b/frontend/src/app/bisq/bisq.routing.module.ts new file mode 100644 index 000000000..e1d45004a --- /dev/null +++ b/frontend/src/app/bisq/bisq.routing.module.ts @@ -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 { } diff --git a/frontend/src/app/components/bisq-icon/bisq-icon.component.html b/frontend/src/app/components/bisq-icon/bisq-icon.component.html new file mode 100644 index 000000000..5ea603892 --- /dev/null +++ b/frontend/src/app/components/bisq-icon/bisq-icon.component.html @@ -0,0 +1 @@ + diff --git a/frontend/src/app/components/bisq-icon/bisq-icon.component.scss b/frontend/src/app/components/bisq-icon/bisq-icon.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/app/components/bisq-icon/bisq-icon.component.ts b/frontend/src/app/components/bisq-icon/bisq-icon.component.ts new file mode 100644 index 000000000..f9128dd15 --- /dev/null +++ b/frontend/src/app/components/bisq-icon/bisq-icon.component.ts @@ -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'; + } + } +} diff --git a/frontend/src/app/components/bisq-transaction-details/bisq-transaction-details.component.html b/frontend/src/app/components/bisq-transaction-details/bisq-transaction-details.component.html new file mode 100644 index 000000000..59f412171 --- /dev/null +++ b/frontend/src/app/components/bisq-transaction-details/bisq-transaction-details.component.html @@ -0,0 +1,40 @@ +
+
+
+ + + + + + + + + + + + + + + + + + + +
Inputs{{ totalInput / 100 | number: '1.2-2' }} BSQ
Outputs{{ totalOutput / 100 | number: '1.2-2' }} BSQ
Burnt{{ tx.burntFee / 100 | number: '1.2-2' }} BSQ
Issuance{{ totalIssued / 100 | number: '1.2-2' }} BSQ
+
+
+ + + + + + + + + + + +
Type {{ tx.txTypeDisplayString }}
Version{{ tx.txVersion }}
+
+
+
\ No newline at end of file diff --git a/frontend/src/app/components/bisq-transaction-details/bisq-transaction-details.component.scss b/frontend/src/app/components/bisq-transaction-details/bisq-transaction-details.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/app/components/bisq-transaction-details/bisq-transaction-details.component.ts b/frontend/src/app/components/bisq-transaction-details/bisq-transaction-details.component.ts new file mode 100644 index 000000000..927f3af20 --- /dev/null +++ b/frontend/src/app/components/bisq-transaction-details/bisq-transaction-details.component.ts @@ -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); + } +} diff --git a/frontend/src/app/components/bisq-transfers/bisq-transfers.component.html b/frontend/src/app/components/bisq-transfers/bisq-transfers.component.html new file mode 100644 index 000000000..828b03e18 --- /dev/null +++ b/frontend/src/app/components/bisq-transfers/bisq-transfers.component.html @@ -0,0 +1,60 @@ +
+
+
+ + + + + + + + + + +
+ + + + + + + + + + + B{{ input.address | shortenString : 16 }} + B{{ input.address | shortenString : 35 }} + + + {{ input.bsqAmount / 100 | number: '1.2-2' }} BSQ +
+
+
+
+ + + + + + + + + + +
+ + B{{ output.address | shortenString : 16 }} + B{{ output.address | shortenString : 35 }} + + + {{ output.bsqAmount / 100 | number: '1.2-2' }} BSQ + + + + + +
+
+
+ +
\ No newline at end of file diff --git a/frontend/src/app/components/bisq-transfers/bisq-transfers.component.scss b/frontend/src/app/components/bisq-transfers/bisq-transfers.component.scss new file mode 100644 index 000000000..3f78768ca --- /dev/null +++ b/frontend/src/app/components/bisq-transfers/bisq-transfers.component.scss @@ -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; + } +} \ No newline at end of file diff --git a/frontend/src/app/components/bisq-transfers/bisq-transfers.component.ts b/frontend/src/app/components/bisq-transfers/bisq-transfers.component.ts new file mode 100644 index 000000000..8b96a709b --- /dev/null +++ b/frontend/src/app/components/bisq-transfers/bisq-transfers.component.ts @@ -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; + } + +} diff --git a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.ts b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.ts index 2eca3e2c5..7f25d5ff7 100644 --- a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.ts +++ b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.ts @@ -26,6 +26,7 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy { gradientColors = { '': ['#9339f4', '#105fb0'], + bisq: ['#9339f4', '#105fb0'], liquid: ['#116761', '#183550'], testnet: ['#1d486f', '#183550'], }; diff --git a/frontend/src/app/components/blockchain/blockchain.component.ts b/frontend/src/app/components/blockchain/blockchain.component.ts index 48e0f2bb1..fd6209e24 100644 --- a/frontend/src/app/components/blockchain/blockchain.component.ts +++ b/frontend/src/app/components/blockchain/blockchain.component.ts @@ -17,6 +17,5 @@ export class BlockchainComponent implements OnInit { ngOnInit() { this.stateService.blocks$.subscribe(() => this.isLoading = false); - this.stateService.networkChanged$.subscribe(() => this.isLoading = true); } } diff --git a/frontend/src/app/components/master-page/master-page.component.html b/frontend/src/app/components/master-page/master-page.component.html index ca4003fad..1373da184 100644 --- a/frontend/src/app/components/master-page/master-page.component.html +++ b/frontend/src/app/components/master-page/master-page.component.html @@ -1,18 +1,19 @@