diff --git a/backend/mempool-config.sample.json b/backend/mempool-config.sample.json index a8c5d13ef..6372dede5 100644 --- a/backend/mempool-config.sample.json +++ b/backend/mempool-config.sample.json @@ -13,6 +13,8 @@ "INITIAL_BLOCK_AMOUNT": 8, "TX_PER_SECOND_SPAN_SECONDS": 150, "ELECTRS_API_URL": "https://www.blockstream.info/testnet/api", + "BISQ_ENABLED": false, + "BSQ_BLOCKS_DATA_PATH": "/bisq/data", "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..e5f9e11c4 --- /dev/null +++ b/backend/src/api/bisq.ts @@ -0,0 +1,228 @@ +const config = require('../../mempool-config.json'); +import * as fs from 'fs'; +import * as request from 'request'; +import { BisqBlocks, BisqBlock, BisqTransaction, BisqStats, BisqTrade } from '../interfaces'; +import { Common } from './common'; + +class Bisq { + private latestBlockHeight = 0; + private blocks: BisqBlock[] = []; + private transactions: BisqTransaction[] = []; + private transactionIndex: { [txId: string]: BisqTransaction } = {}; + private blockIndex: { [hash: string]: BisqBlock } = {}; + private addressIndex: { [address: string]: BisqTransaction[] } = {}; + private stats: BisqStats = { + minted: 0, + burnt: 0, + addresses: 0, + unspent_txos: 0, + spent_txos: 0, + }; + private price: number = 0; + private priceUpdateCallbackFunction: ((price: number) => void) | undefined; + private subdirectoryWatcher: fs.FSWatcher | undefined; + + constructor() {} + + startBisqService(): void { + this.loadBisqDumpFile(); + setInterval(this.updatePrice.bind(this), 1000 * 60 * 60); + this.updatePrice(); + this.startTopLevelDirectoryWatcher(); + this.restartSubDirectoryWatcher(); + } + + getTransaction(txId: string): BisqTransaction | undefined { + return this.transactionIndex[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.blockIndex[hash]; + } + + getAddress(hash: string): BisqTransaction[] { + return this.addressIndex[hash]; + } + + getBlocks(start: number, length: number): [BisqBlock[], number] { + return [this.blocks.slice(start, length + start), this.blocks.length]; + } + + getStats(): BisqStats { + return this.stats; + } + + setPriceCallbackFunction(fn: (price: number) => void) { + this.priceUpdateCallbackFunction = fn; + } + + getLatestBlockHeight(): number { + return this.latestBlockHeight; + } + + private startTopLevelDirectoryWatcher() { + let fsWait: NodeJS.Timeout | null = null; + fs.watch(config.BSQ_BLOCKS_DATA_PATH, () => { + if (fsWait) { + clearTimeout(fsWait); + } + fsWait = setTimeout(() => { + console.log(`Change detected in the top level Bisq data folder. Resetting inner watcher.`); + this.restartSubDirectoryWatcher(); + }, 15000); + }); + } + + private restartSubDirectoryWatcher() { + if (this.subdirectoryWatcher) { + this.subdirectoryWatcher.close(); + } + + let fsWait: NodeJS.Timeout | null = null; + this.subdirectoryWatcher = fs.watch(config.BSQ_BLOCKS_DATA_PATH + '/all', () => { + if (fsWait) { + clearTimeout(fsWait); + } + fsWait = setTimeout(() => { + console.log(`Change detected in the Bisq data folder.`); + this.loadBisqDumpFile(); + }, 2000); + }); + } + + private updatePrice() { + request('https://markets.bisq.network/api/trades/?market=bsq_btc', { json: true }, (err, res, trades: BisqTrade[]) => { + if (err) { return console.log(err); } + + const prices: number[] = []; + trades.forEach((trade) => { + prices.push(parseFloat(trade.price) * 100000000); + }); + prices.sort((a, b) => a - b); + this.price = Common.median(prices); + if (this.priceUpdateCallbackFunction) { + this.priceUpdateCallbackFunction(this.price); + } + }); + } + + private async loadBisqDumpFile(): Promise { + try { + const data = await this.loadData(); + await this.loadBisqBlocksDump(data); + this.buildIndex(); + this.calculateStats(); + } catch (e) { + console.log('loadBisqDumpFile() error.', e.message); + } + } + + private buildIndex() { + const start = new Date().getTime(); + this.transactions = []; + this.transactionIndex = {}; + this.addressIndex = {}; + + this.blocks.forEach((block) => { + /* Build block index */ + if (!this.blockIndex[block.hash]) { + this.blockIndex[block.hash] = block; + } + + /* Build transactions index */ + block.txs.forEach((tx) => { + this.transactions.push(tx); + this.transactionIndex[tx.id] = tx; + }); + }); + + /* Build address index */ + this.transactions.forEach((tx) => { + tx.inputs.forEach((input) => { + if (!this.addressIndex[input.address]) { + this.addressIndex[input.address] = []; + } + if (this.addressIndex[input.address].indexOf(tx) === -1) { + this.addressIndex[input.address].push(tx); + } + }); + tx.outputs.forEach((output) => { + if (!this.addressIndex[output.address]) { + this.addressIndex[output.address] = []; + } + if (this.addressIndex[output.address].indexOf(tx) === -1) { + this.addressIndex[output.address].push(tx); + } + }); + }); + + const time = new Date().getTime() - start; + console.log('Bisq data index rebuilt in ' + time + ' ms'); + } + + private calculateStats() { + let minted = 0; + let burned = 0; + let unspent = 0; + let spent = 0; + + this.transactions.forEach((tx) => { + tx.outputs.forEach((output) => { + if (output.opReturn) { + return; + } + if (output.txOutputType === 'GENESIS_OUTPUT' || output.txOutputType === 'ISSUANCE_CANDIDATE_OUTPUT' && output.isVerified) { + minted += output.bsqAmount; + } + if (output.isUnspent) { + unspent++; + } else { + spent++; + } + }); + burned += tx['burntFee']; + }); + + this.stats = { + addresses: Object.keys(this.addressIndex).length, + minted: minted, + burnt: burned, + spent_txos: spent, + unspent_txos: unspent, + }; + } + + private async loadBisqBlocksDump(cacheData: string): Promise { + const start = new Date().getTime(); + if (cacheData && cacheData.length !== 0) { + console.log('Loading Bisq data from dump...'); + const data: BisqBlocks = JSON.parse(cacheData); + if (data.blocks && data.blocks.length !== this.blocks.length) { + this.blocks = data.blocks.filter((block) => block.txs.length > 0); + this.blocks.reverse(); + this.latestBlockHeight = data.chainHeight; + const time = new Date().getTime() - start; + console.log('Bisq dump loaded in ' + time + ' ms'); + } else { + throw new Error(`Bisq dump didn't contain any blocks`); + } + } + } + + private loadData(): Promise { + return new Promise((resolve, reject) => { + fs.readFile(config.BSQ_BLOCKS_DATA_PATH + '/all/blocks.json', '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/api/websocket-handler.ts b/backend/src/api/websocket-handler.ts index fef583d4e..1174a261d 100644 --- a/backend/src/api/websocket-handler.ts +++ b/backend/src/api/websocket-handler.ts @@ -12,6 +12,7 @@ import { Common } from './common'; class WebsocketHandler { private wss: WebSocket.Server | undefined; private nativeAssetId = '6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d'; + private extraInitProperties = {}; constructor() { } @@ -19,6 +20,10 @@ class WebsocketHandler { this.wss = wss; } + setExtraInitProperties(property: string, value: any) { + this.extraInitProperties[property] = value; + } + setupConnectionHandling() { if (!this.wss) { throw new Error('WebSocket.Server is not set'); @@ -84,6 +89,7 @@ class WebsocketHandler { 'mempool-blocks': mempoolBlocks.getMempoolBlocks(), 'git-commit': backendInfo.gitCommitHash, 'hostname': backendInfo.hostname, + ...this.extraInitProperties })); } diff --git a/backend/src/index.ts b/backend/src/index.ts index ab72d260b..09371d54a 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,11 @@ class Server { fiatConversion.startService(); diskCache.loadMempoolCache(); + if (config.BISQ_ENABLED) { + bisq.startBisqService(); + bisq.setPriceCallbackFunction((price) => websocketHandler.setExtraInitProperties('bsq-price', price)); + } + this.server.listen(config.HTTP_PORT, () => { console.log(`Server started on port ${config.HTTP_PORT}`); }); @@ -84,6 +90,18 @@ 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/stats', routes.getBisqStats) + .get(config.API_ENDPOINT + 'bisq/tx/:txId', routes.getBisqTransaction) + .get(config.API_ENDPOINT + 'bisq/block/:hash', routes.getBisqBlock) + .get(config.API_ENDPOINT + 'bisq/blocks/tip/height', routes.getBisqTip) + .get(config.API_ENDPOINT + 'bisq/blocks/:index/:length', routes.getBisqBlocks) + .get(config.API_ENDPOINT + 'bisq/address/:address', routes.getBisqAddress) + .get(config.API_ENDPOINT + 'bisq/txs/:index/:length', routes.getBisqTransactions) + ; + } } } diff --git a/backend/src/interfaces.ts b/backend/src/interfaces.ts index 49ab65f65..9da3cde44 100644 --- a/backend/src/interfaces.ts +++ b/backend/src/interfaces.ts @@ -230,3 +230,95 @@ 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; +} + +export interface BisqStats { + minted: number; + burnt: number; + addresses: number; + unspent_txos: number; + spent_txos: 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; +} + +export interface BisqTrade { + direction: string; + price: string; + amount: string; + volume: string; + payment_method: string; + trade_id: string; + trade_date: number; +} diff --git a/backend/src/routes.ts b/backend/src/routes.ts index 3240489fe..ddb625f04 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 = {}; @@ -25,42 +26,42 @@ class Routes { public async get2HStatistics(req: Request, res: Response) { const result = await statistics.$list2H(); - res.send(result); + res.json(result); } public get24HStatistics(req: Request, res: Response) { - res.send(this.cache['24h']); + res.json(this.cache['24h']); } public get1WHStatistics(req: Request, res: Response) { - res.send(this.cache['1w']); + res.json(this.cache['1w']); } public get1MStatistics(req: Request, res: Response) { - res.send(this.cache['1m']); + res.json(this.cache['1m']); } public get3MStatistics(req: Request, res: Response) { - res.send(this.cache['3m']); + res.json(this.cache['3m']); } public get6MStatistics(req: Request, res: Response) { - res.send(this.cache['6m']); + res.json(this.cache['6m']); } public get1YStatistics(req: Request, res: Response) { - res.send(this.cache['1y']); + res.json(this.cache['1y']); } public async getRecommendedFees(req: Request, res: Response) { const result = feeApi.getRecommendedFee(); - res.send(result); + res.json(result); } public getMempoolBlocks(req: Request, res: Response) { try { const result = mempoolBlocks.getMempoolBlocks(); - res.send(result); + res.json(result); } catch (e) { res.status(500).send(e.message); } @@ -79,11 +80,65 @@ class Routes { } const times = mempool.getFirstSeenForTransactions(txIds); - res.send(times); + res.json(times); } public getBackendInfo(req: Request, res: Response) { - res.send(backendInfo.getBackendInfo()); + res.json(backendInfo.getBackendInfo()); + } + + public getBisqStats(req: Request, res: Response) { + const result = bisq.getStats(); + res.json(result); + } + + public getBisqTip(req: Request, res: Response) { + const result = bisq.getLatestBlockHeight(); + res.type('text/plain'); + res.send(result.toString()); + } + + public getBisqTransaction(req: Request, res: Response) { + const result = bisq.getTransaction(req.params.txId); + if (result) { + res.json(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); + res.header('X-Total-Count', count.toString()); + res.json(transactions); + } + + public getBisqBlock(req: Request, res: Response) { + const result = bisq.getBlock(req.params.hash); + if (result) { + res.json(result); + } else { + res.status(404).send('Bisq block not found'); + } + } + + public getBisqBlocks(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.getBlocks(index, length); + res.header('X-Total-Count', count.toString()); + res.json(transactions); + } + + public getBisqAddress(req: Request, res: Response) { + const result = bisq.getAddress(req.params.address.substr(1)); + if (result) { + res.json(result); + } else { + res.status(404).send('Bisq address not found'); + } } } 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/proxy.conf.json b/frontend/proxy.conf.json index 7de229905..2e161a18d 100644 --- a/frontend/proxy.conf.json +++ b/frontend/proxy.conf.json @@ -1,18 +1,25 @@ { - "/api": { + "/api/v1": { "target": "http://localhost:8999/", "secure": false }, - "/ws": { + "/api/v1/ws": { "target": "http://localhost:8999/", "secure": false, "ws": true }, - "/electrs": { - "target": "https://www.blockstream.info/testnet/api/", + "/bisq/api": { + "target": "http://localhost:8999/", "secure": false, "pathRewrite": { - "^/electrs": "" + "^/bisq/api": "/api/v1/bisq" + } + }, + "/api": { + "target": "http://localhost:50001/", + "secure": false, + "pathRewrite": { + "^/api": "" } } } \ No newline at end of file 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..5b3680f3d 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -3,7 +3,7 @@ import { NgModule } from '@angular/core'; import { HttpClientModule } from '@angular/common/http'; import { ReactiveFormsModule } from '@angular/forms'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { NgbButtonsModule, NgbTooltipModule, NgbPaginationModule } from '@ng-bootstrap/ng-bootstrap'; +import { NgbButtonsModule, NgbPaginationModule, NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'; import { InfiniteScrollModule } from 'ngx-infinite-scroll'; import { AppRoutingModule } from './app-routing.module'; @@ -11,25 +11,17 @@ 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'; 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'; import { AboutComponent } from './components/about/about.component'; import { TelevisionComponent } from './components/television/television.component'; @@ -39,19 +31,16 @@ import { BlockchainBlocksComponent } from './components/blockchain-blocks/blockc import { BlockchainComponent } from './components/blockchain/blockchain.component'; import { FooterComponent } from './components/footer/footer.component'; import { AudioService } from './services/audio.service'; -import { FiatComponent } from './fiat/fiat.component'; import { MempoolBlockComponent } from './components/mempool-block/mempool-block.component'; import { FeeDistributionGraphComponent } from './components/fee-distribution-graph/fee-distribution-graph.component'; 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'; @NgModule({ declarations: [ @@ -66,33 +55,21 @@ import { StatusViewComponent } from './components/status-view/status-view.compon TransactionComponent, BlockComponent, TransactionsListComponent, - BytesPipe, - VbytesPipe, - WuBytesPipe, - CeilPipe, - ShortenStringPipe, AddressComponent, AmountComponent, SearchFormComponent, LatestBlocksComponent, - TimeSinceComponent, TimespanComponent, AddressLabelsComponent, MempoolBlocksComponent, - QrcodeComponent, - ClipboardComponent, ChartistComponent, FooterComponent, - FiatComponent, MempoolBlockComponent, FeeDistributionGraphComponent, MempoolGraphComponent, AssetComponent, - ScriptpubkeyTypePipe, AssetsComponent, - RelativeUrlPipe, MinerComponent, - Hex2asciiPipe, StatusViewComponent, ], imports: [ @@ -102,15 +79,15 @@ import { StatusViewComponent } from './components/status-view/status-view.compon ReactiveFormsModule, BrowserAnimationsModule, NgbButtonsModule, - NgbTooltipModule, NgbPaginationModule, + NgbDropdownModule, 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-address/bisq-address.component.html b/frontend/src/app/bisq/bisq-address/bisq-address.component.html new file mode 100644 index 000000000..2413b90a0 --- /dev/null +++ b/frontend/src/app/bisq/bisq-address/bisq-address.component.html @@ -0,0 +1,106 @@ +
+

Address

+ + {{ addressString | shortenString : 24 }} + {{ addressString }} + + +
+ +
+ + +
+ +
+
+ + + + + + + + + + + + + + + +
Total received{{ totalReceived / 100 | number: '1.2-2' }} BSQ
Total sent{{ totalSent / 100 | number: '1.2-2' }} BSQ
Final balance{{ (totalReceived - totalSent) / 100 | number: '1.2-2' }} BSQ ()
+
+
+
+
+ +
+
+
+ +
+ +
+ +

{{ transactions.length | number }} transactions

+ + + +
+ + {{ tx.id | shortenString : 16 }} + {{ tx.id }} + +
+ {{ tx.time | date:'yyyy-MM-dd HH:mm' }} +
+
+
+ + + +
+
+ +
+ + + +
+
+
+ + + + + + + + + + + + +
+
+
+
+ +
+
+
+ +
+ + +
+ Error loading address data. +
+ {{ error.error }} +
+
+ +
+ +
\ No newline at end of file diff --git a/frontend/src/app/bisq/bisq-address/bisq-address.component.scss b/frontend/src/app/bisq/bisq-address/bisq-address.component.scss new file mode 100644 index 000000000..c5961e428 --- /dev/null +++ b/frontend/src/app/bisq/bisq-address/bisq-address.component.scss @@ -0,0 +1,23 @@ +.qr-wrapper { + background-color: #FFF; + padding: 10px; + padding-bottom: 5px; + display: inline-block; + margin-right: 25px; +} + +@media (min-width: 576px) { + .qrcode-col { + text-align: right; + } +} +@media (max-width: 575.98px) { + .qrcode-col { + text-align: center; + } + + .qrcode-col > div { + margin-top: 20px; + margin-right: 0px; + } +} \ No newline at end of file diff --git a/frontend/src/app/bisq/bisq-address/bisq-address.component.ts b/frontend/src/app/bisq/bisq-address/bisq-address.component.ts new file mode 100644 index 000000000..a82a31e58 --- /dev/null +++ b/frontend/src/app/bisq/bisq-address/bisq-address.component.ts @@ -0,0 +1,82 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { SeoService } from 'src/app/services/seo.service'; +import { switchMap, filter, catchError } from 'rxjs/operators'; +import { ParamMap, ActivatedRoute } from '@angular/router'; +import { Subscription, of } from 'rxjs'; +import { BisqTransaction } from '../bisq.interfaces'; +import { BisqApiService } from '../bisq-api.service'; + +@Component({ + selector: 'app-bisq-address', + templateUrl: './bisq-address.component.html', + styleUrls: ['./bisq-address.component.scss'] +}) +export class BisqAddressComponent implements OnInit, OnDestroy { + transactions: BisqTransaction[]; + addressString: string; + isLoadingAddress = true; + error: any; + mainSubscription: Subscription; + + totalReceived = 0; + totalSent = 0; + + constructor( + private route: ActivatedRoute, + private seoService: SeoService, + private bisqApiService: BisqApiService, + ) { } + + ngOnInit() { + this.mainSubscription = this.route.paramMap + .pipe( + switchMap((params: ParamMap) => { + this.error = undefined; + this.isLoadingAddress = true; + this.transactions = null; + document.body.scrollTo(0, 0); + this.addressString = params.get('id') || ''; + this.seoService.setTitle('Address: ' + this.addressString, true); + + return this.bisqApiService.getAddress$(this.addressString) + .pipe( + catchError((err) => { + this.isLoadingAddress = false; + this.error = err; + console.log(err); + return of(null); + }) + ); + }), + filter((transactions) => transactions !== null) + ) + .subscribe((transactions: BisqTransaction[]) => { + this.transactions = transactions; + this.updateChainStats(); + this.isLoadingAddress = false; + }, + (error) => { + console.log(error); + this.error = error; + this.isLoadingAddress = false; + }); + } + + updateChainStats() { + const shortenedAddress = this.addressString.substr(1); + + this.totalSent = this.transactions.reduce((acc, tx) => + acc + tx.inputs + .filter((input) => input.address === shortenedAddress) + .reduce((a, input) => a + input.bsqAmount, 0), 0); + + this.totalReceived = this.transactions.reduce((acc, tx) => + acc + tx.outputs + .filter((output) => output.address === shortenedAddress) + .reduce((a, output) => a + output.bsqAmount, 0), 0); + } + + ngOnDestroy() { + this.mainSubscription.unsubscribe(); + } +} diff --git a/frontend/src/app/bisq/bisq-api.service.ts b/frontend/src/app/bisq/bisq-api.service.ts new file mode 100644 index 000000000..f5690d5ea --- /dev/null +++ b/frontend/src/app/bisq/bisq-api.service.ts @@ -0,0 +1,41 @@ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpResponse } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { BisqTransaction, BisqBlock, BisqStats } from './bisq.interfaces'; + +const API_BASE_URL = '/bisq/api'; + +@Injectable({ + providedIn: 'root' +}) +export class BisqApiService { + apiBaseUrl: string; + + constructor( + private httpClient: HttpClient, + ) { } + + getStats$(): Observable { + return this.httpClient.get(API_BASE_URL + '/stats'); + } + + getTransaction$(txId: string): Observable { + return this.httpClient.get(API_BASE_URL + '/tx/' + txId); + } + + listTransactions$(start: number, length: number): Observable> { + return this.httpClient.get(API_BASE_URL + `/txs/${start}/${length}`, { observe: 'response' }); + } + + getBlock$(hash: string): Observable { + return this.httpClient.get(API_BASE_URL + '/block/' + hash); + } + + listBlocks$(start: number, length: number): Observable> { + return this.httpClient.get(API_BASE_URL + `/blocks/${start}/${length}`, { observe: 'response' }); + } + + getAddress$(address: string): Observable { + return this.httpClient.get(API_BASE_URL + '/address/' + address); + } +} diff --git a/frontend/src/app/bisq/bisq-block/bisq-block.component.html b/frontend/src/app/bisq/bisq-block/bisq-block.component.html new file mode 100644 index 000000000..4e1c7b95c --- /dev/null +++ b/frontend/src/app/bisq/bisq-block/bisq-block.component.html @@ -0,0 +1,108 @@ +
+ +
+

Block {{ blockHeight }}

+
+ +
+ + + +
+
+
+ + + + + + + + + + +
Hash{{ block.hash | shortenString : 13 }}
Timestamp + {{ block.time | date:'yyyy-MM-dd HH:mm' }} +
+ ( ago) +
+
+
+ +
+
+ +
+ +
+ +

{{ block.txs.length | number }} transactions

+ + + +
+ + {{ tx.id | shortenString : 16 }} + {{ tx.id }} + +
+ {{ tx.time | date:'yyyy-MM-dd HH:mm' }} +
+
+
+ + + +
+
+ +
+ + +
+
+
+ + + + + + + + + + +
Hash
Timestamp
+
+
+ + + + + + +
Previous hash
+
+
+
+
+ + +
+ +
+ Error loading block +
+ {{ error.status }}: {{ error.statusText }} +
+
+ +
\ No newline at end of file diff --git a/frontend/src/app/bisq/bisq-block/bisq-block.component.scss b/frontend/src/app/bisq/bisq-block/bisq-block.component.scss new file mode 100644 index 000000000..0fc8e761e --- /dev/null +++ b/frontend/src/app/bisq/bisq-block/bisq-block.component.scss @@ -0,0 +1,10 @@ + +.td-width { + width: 175px; +} + +@media (max-width: 767.98px) { + .td-width { + width: 140px; + } +} diff --git a/frontend/src/app/bisq/bisq-block/bisq-block.component.ts b/frontend/src/app/bisq/bisq-block/bisq-block.component.ts new file mode 100644 index 000000000..9ce3be8fb --- /dev/null +++ b/frontend/src/app/bisq/bisq-block/bisq-block.component.ts @@ -0,0 +1,98 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { BisqBlock } from 'src/app/bisq/bisq.interfaces'; +import { Location } from '@angular/common'; +import { BisqApiService } from '../bisq-api.service'; +import { ActivatedRoute, ParamMap, Router } from '@angular/router'; +import { Subscription, of } from 'rxjs'; +import { switchMap, catchError } from 'rxjs/operators'; +import { SeoService } from 'src/app/services/seo.service'; +import { ElectrsApiService } from 'src/app/services/electrs-api.service'; +import { HttpErrorResponse } from '@angular/common/http'; + +@Component({ + selector: 'app-bisq-block', + templateUrl: './bisq-block.component.html', + styleUrls: ['./bisq-block.component.scss'] +}) +export class BisqBlockComponent implements OnInit, OnDestroy { + block: BisqBlock; + subscription: Subscription; + blockHash = ''; + blockHeight = 0; + isLoading = true; + error: HttpErrorResponse | null; + + constructor( + private bisqApiService: BisqApiService, + private route: ActivatedRoute, + private seoService: SeoService, + private electrsApiService: ElectrsApiService, + private router: Router, + private location: Location, + ) { } + + ngOnInit(): void { + this.subscription = this.route.paramMap + .pipe( + switchMap((params: ParamMap) => { + const blockHash = params.get('id') || ''; + document.body.scrollTo(0, 0); + this.isLoading = true; + this.error = null; + if (history.state.data && history.state.data.blockHeight) { + this.blockHeight = history.state.data.blockHeight; + } + if (history.state.data && history.state.data.block) { + this.blockHeight = history.state.data.block.height; + return of(history.state.data.block); + } + + let isBlockHeight = false; + if (/^[0-9]+$/.test(blockHash)) { + isBlockHeight = true; + } else { + this.blockHash = blockHash; + } + + if (isBlockHeight) { + return this.electrsApiService.getBlockHashFromHeight$(parseInt(blockHash, 10)) + .pipe( + switchMap((hash) => { + if (!hash) { + return; + } + this.blockHash = hash; + this.location.replaceState( + this.router.createUrlTree(['/bisq/block/', hash]).toString() + ); + return this.bisqApiService.getBlock$(this.blockHash) + .pipe(catchError(this.caughtHttpError.bind(this))); + }), + catchError(this.caughtHttpError.bind(this)) + ); + } + + return this.bisqApiService.getBlock$(this.blockHash) + .pipe(catchError(this.caughtHttpError.bind(this))); + }) + ) + .subscribe((block: BisqBlock) => { + if (!block) { + return; + } + this.isLoading = false; + this.blockHeight = block.height; + this.seoService.setTitle('Block: #' + block.height + ': ' + block.hash, true); + this.block = block; + }); + } + + ngOnDestroy() { + this.subscription.unsubscribe(); + } + + caughtHttpError(err: HttpErrorResponse){ + this.error = err; + return of(null); + } +} diff --git a/frontend/src/app/bisq/bisq-blocks/bisq-blocks.component.html b/frontend/src/app/bisq/bisq-blocks/bisq-blocks.component.html new file mode 100644 index 000000000..48deff2da --- /dev/null +++ b/frontend/src/app/bisq/bisq-blocks/bisq-blocks.component.html @@ -0,0 +1,36 @@ +
+

Blocks

+
+ +
+ +
+ + + + + + + + + + + + + + + +
HeightConfirmedTotal SentTransactions
{{ block.height }} ago{{ calculateTotalOutput(block) / 100 | number: '1.2-2' }} BSQ{{ block.txs.length }}
+
+ +
+ + + +
+ + + + + + diff --git a/frontend/src/app/bisq/bisq-blocks/bisq-blocks.component.scss b/frontend/src/app/bisq/bisq-blocks/bisq-blocks.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/app/bisq/bisq-blocks/bisq-blocks.component.ts b/frontend/src/app/bisq/bisq-blocks/bisq-blocks.component.ts new file mode 100644 index 000000000..2c1631de7 --- /dev/null +++ b/frontend/src/app/bisq/bisq-blocks/bisq-blocks.component.ts @@ -0,0 +1,71 @@ +import { Component, OnInit } from '@angular/core'; +import { BisqApiService } from '../bisq-api.service'; +import { switchMap, tap } from 'rxjs/operators'; +import { Subject } from 'rxjs'; +import { BisqBlock, BisqOutput, BisqTransaction } from '../bisq.interfaces'; +import { SeoService } from 'src/app/services/seo.service'; + +@Component({ + selector: 'app-bisq-blocks', + templateUrl: './bisq-blocks.component.html', + styleUrls: ['./bisq-blocks.component.scss'] +}) +export class BisqBlocksComponent implements OnInit { + blocks: BisqBlock[]; + totalCount: number; + page = 1; + itemsPerPage: number; + contentSpace = window.innerHeight - (165 + 75); + fiveItemsPxSize = 250; + loadingItems: number[]; + isLoading = true; + // @ts-ignore + paginationSize: 'sm' | 'lg' = 'md'; + paginationMaxSize = 10; + + pageSubject$ = new Subject(); + + constructor( + private bisqApiService: BisqApiService, + private seoService: SeoService, + ) { } + + ngOnInit(): void { + this.seoService.setTitle('Blocks', true); + this.itemsPerPage = Math.max(Math.round(this.contentSpace / this.fiveItemsPxSize) * 5, 10); + this.loadingItems = Array(this.itemsPerPage); + if (document.body.clientWidth < 768) { + this.paginationSize = 'sm'; + this.paginationMaxSize = 3; + } + + this.pageSubject$ + .pipe( + tap(() => this.isLoading = true), + switchMap((page) => this.bisqApiService.listBlocks$((page - 1) * this.itemsPerPage, this.itemsPerPage)) + ) + .subscribe((response) => { + this.isLoading = false; + this.blocks = response.body; + this.totalCount = parseInt(response.headers.get('x-total-count'), 10); + }, (error) => { + console.log(error); + }); + + this.pageSubject$.next(1); + } + + calculateTotalOutput(block: BisqBlock): number { + return block.txs.reduce((a: number, tx: BisqTransaction) => + a + tx.outputs.reduce((acc: number, output: BisqOutput) => acc + output.bsqAmount, 0), 0 + ); + } + + trackByFn(index: number) { + return index; + } + + pageChange(page: number) { + this.pageSubject$.next(page); + } +} diff --git a/frontend/src/app/bisq/bisq-explorer/bisq-explorer.component.html b/frontend/src/app/bisq/bisq-explorer/bisq-explorer.component.html new file mode 100644 index 000000000..0680b43f9 --- /dev/null +++ b/frontend/src/app/bisq/bisq-explorer/bisq-explorer.component.html @@ -0,0 +1 @@ + diff --git a/frontend/src/app/bisq/bisq-explorer/bisq-explorer.component.scss b/frontend/src/app/bisq/bisq-explorer/bisq-explorer.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/app/bisq/bisq-explorer/bisq-explorer.component.ts b/frontend/src/app/bisq/bisq-explorer/bisq-explorer.component.ts new file mode 100644 index 000000000..bb9a37809 --- /dev/null +++ b/frontend/src/app/bisq/bisq-explorer/bisq-explorer.component.ts @@ -0,0 +1,18 @@ +import { Component, OnInit } from '@angular/core'; +import { WebsocketService } from 'src/app/services/websocket.service'; + +@Component({ + selector: 'app-bisq-explorer', + templateUrl: './bisq-explorer.component.html', + styleUrls: ['./bisq-explorer.component.scss'] +}) +export class BisqExplorerComponent implements OnInit { + + constructor( + private websocketService: WebsocketService, + ) { } + + ngOnInit(): void { + this.websocketService.want(['blocks']); + } +} diff --git a/frontend/src/app/bisq/bisq-icon/bisq-icon.component.html b/frontend/src/app/bisq/bisq-icon/bisq-icon.component.html new file mode 100644 index 000000000..5ea603892 --- /dev/null +++ b/frontend/src/app/bisq/bisq-icon/bisq-icon.component.html @@ -0,0 +1 @@ + diff --git a/frontend/src/app/bisq/bisq-icon/bisq-icon.component.scss b/frontend/src/app/bisq/bisq-icon/bisq-icon.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/app/bisq/bisq-icon/bisq-icon.component.ts b/frontend/src/app/bisq/bisq-icon/bisq-icon.component.ts new file mode 100644 index 000000000..455b6b2ef --- /dev/null +++ b/frontend/src/app/bisq/bisq-icon/bisq-icon.component.ts @@ -0,0 +1,81 @@ +import { Component, ChangeDetectionStrategy, Input, OnChanges } 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 OnChanges { + @Input() txType: string; + + iconProp: [IconPrefix, IconName] = ['fas', 'leaf']; + color: string; + + constructor() { } + + ngOnChanges() { + 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/bisq/bisq-stats/bisq-stats.component.html b/frontend/src/app/bisq/bisq-stats/bisq-stats.component.html new file mode 100644 index 000000000..b34cc17ce --- /dev/null +++ b/frontend/src/app/bisq/bisq-stats/bisq-stats.component.html @@ -0,0 +1,90 @@ +
+

BSQ Statistics

+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PropertyValue
Existing amount{{ (stats.minted - stats.burnt) / 100 | number: '1.2-2' }} BSQ
Minted amount{{ stats.minted | number: '1.2-2' }} BSQ
Burnt amount{{ stats.burnt | number: '1.2-2' }} BSQ
Addresses{{ stats.addresses | number }}
Unspent TXOs{{ stats.unspent_txos | number }}
Spent TXOs{{ stats.spent_txos | number }}
Price
Market cap
+ +
+
+
+
+ + + + + Existing amount + + + + Minted amount + + + + Burnt amount + + + + Addresses + + + + Unspent TXOs + + + + Spent TXOs + + + + Price + + + + Market cap + + + + \ No newline at end of file diff --git a/frontend/src/app/bisq/bisq-stats/bisq-stats.component.scss b/frontend/src/app/bisq/bisq-stats/bisq-stats.component.scss new file mode 100644 index 000000000..e1f02094f --- /dev/null +++ b/frontend/src/app/bisq/bisq-stats/bisq-stats.component.scss @@ -0,0 +1,9 @@ +.td-width { + width: 250px; +} + +@media (max-width: 767.98px) { + .td-width { + width: 175px; + } +} diff --git a/frontend/src/app/bisq/bisq-stats/bisq-stats.component.ts b/frontend/src/app/bisq/bisq-stats/bisq-stats.component.ts new file mode 100644 index 000000000..787cad58c --- /dev/null +++ b/frontend/src/app/bisq/bisq-stats/bisq-stats.component.ts @@ -0,0 +1,38 @@ +import { Component, OnInit } from '@angular/core'; +import { BisqApiService } from '../bisq-api.service'; +import { BisqStats } from '../bisq.interfaces'; +import { SeoService } from 'src/app/services/seo.service'; +import { StateService } from 'src/app/services/state.service'; + +@Component({ + selector: 'app-bisq-stats', + templateUrl: './bisq-stats.component.html', + styleUrls: ['./bisq-stats.component.scss'] +}) +export class BisqStatsComponent implements OnInit { + isLoading = true; + stats: BisqStats; + price: number; + + constructor( + private bisqApiService: BisqApiService, + private seoService: SeoService, + private stateService: StateService, + ) { } + + ngOnInit() { + this.seoService.setTitle('BSQ Statistics', false); + + this.stateService.bsqPrice$ + .subscribe((bsqPrice) => { + this.price = bsqPrice; + }); + + this.bisqApiService.getStats$() + .subscribe((stats) => { + this.isLoading = false; + this.stats = stats; + }); + } + +} diff --git a/frontend/src/app/bisq/bisq-transaction-details/bisq-transaction-details.component.html b/frontend/src/app/bisq/bisq-transaction-details/bisq-transaction-details.component.html new file mode 100644 index 000000000..fd605fb8d --- /dev/null +++ b/frontend/src/app/bisq/bisq-transaction-details/bisq-transaction-details.component.html @@ -0,0 +1,36 @@ +
+
+
+ + + + + + + + + + + + + + + +
Inputs{{ totalInput / 100 | number: '1.2-2' }} BSQ
Outputs{{ totalOutput / 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/bisq/bisq-transaction-details/bisq-transaction-details.component.scss b/frontend/src/app/bisq/bisq-transaction-details/bisq-transaction-details.component.scss new file mode 100644 index 000000000..ee64d3473 --- /dev/null +++ b/frontend/src/app/bisq/bisq-transaction-details/bisq-transaction-details.component.scss @@ -0,0 +1,11 @@ +@media (max-width: 767.98px) { + .td-width { + width: 150px; + } + .mobile-even tr:nth-of-type(even) { + background-color: #181b2d; + } + .mobile-even tr:nth-of-type(odd) { + background-color: inherit; + } +} \ No newline at end of file diff --git a/frontend/src/app/bisq/bisq-transaction-details/bisq-transaction-details.component.ts b/frontend/src/app/bisq/bisq-transaction-details/bisq-transaction-details.component.ts new file mode 100644 index 000000000..d10d0507e --- /dev/null +++ b/frontend/src/app/bisq/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/bisq/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/bisq/bisq-transaction/bisq-transaction.component.html b/frontend/src/app/bisq/bisq-transaction/bisq-transaction.component.html new file mode 100644 index 000000000..b2a949321 --- /dev/null +++ b/frontend/src/app/bisq/bisq-transaction/bisq-transaction.component.html @@ -0,0 +1,164 @@ +
+ +

Transaction

+ + + + + + +
+ +
+
+
+ + + + + + + + + + + +
Included in block + {{ bisqTx.blockHeight }} + ( ago) +
Features + + + + +
+
+
+ + + + + + + + + + + + + +
Burnt + {{ bisqTx.burntFee / 100 | number: '1.2-2' }} BSQ () +
Fee per vByte + {{ tx.fee / (tx.weight / 4) | number : '1.1-1' }} sat/vB +   + +
+
+ +
+
+ +
+ +

Details

+ + + + +
+ +

Inputs & Outputs

+ + + +
+ +
+ + + +
+ +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+
+
+ +
+ +

Details

+
+ + + + + + + + + + + + + + +
+
+ +
+ +

Inputs & Outputs

+ +
+
+ + + + + + + +
+
+
+ +
+ + +
+ +
+ Error loading transaction +

+ {{ error.status }}: {{ error.statusText }} +
+
+ +
\ No newline at end of file diff --git a/frontend/src/app/bisq/bisq-transaction/bisq-transaction.component.scss b/frontend/src/app/bisq/bisq-transaction/bisq-transaction.component.scss new file mode 100644 index 000000000..35000b90e --- /dev/null +++ b/frontend/src/app/bisq/bisq-transaction/bisq-transaction.component.scss @@ -0,0 +1,9 @@ +.td-width { + width: 175px; +} + +@media (max-width: 767.98px) { + .td-width { + width: 150px; + } +} \ No newline at end of file diff --git a/frontend/src/app/bisq/bisq-transaction/bisq-transaction.component.ts b/frontend/src/app/bisq/bisq-transaction/bisq-transaction.component.ts new file mode 100644 index 000000000..6da3c369f --- /dev/null +++ b/frontend/src/app/bisq/bisq-transaction/bisq-transaction.component.ts @@ -0,0 +1,118 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { ActivatedRoute, ParamMap, Router } from '@angular/router'; +import { BisqTransaction } from 'src/app/bisq/bisq.interfaces'; +import { switchMap, map, catchError } from 'rxjs/operators'; +import { of, Observable, Subscription } from 'rxjs'; +import { StateService } from 'src/app/services/state.service'; +import { Block, Transaction } from 'src/app/interfaces/electrs.interface'; +import { BisqApiService } from '../bisq-api.service'; +import { SeoService } from 'src/app/services/seo.service'; +import { ElectrsApiService } from 'src/app/services/electrs-api.service'; +import { HttpErrorResponse } from '@angular/common/http'; + +@Component({ + selector: 'app-bisq-transaction', + templateUrl: './bisq-transaction.component.html', + styleUrls: ['./bisq-transaction.component.scss'] +}) +export class BisqTransactionComponent implements OnInit, OnDestroy { + bisqTx: BisqTransaction; + tx: Transaction; + latestBlock$: Observable; + txId: string; + price: number; + isLoading = true; + isLoadingTx = true; + error = null; + subscription: Subscription; + + constructor( + private route: ActivatedRoute, + private bisqApiService: BisqApiService, + private electrsApiService: ElectrsApiService, + private stateService: StateService, + private seoService: SeoService, + private router: Router, + ) { } + + ngOnInit(): void { + this.subscription = this.route.paramMap.pipe( + switchMap((params: ParamMap) => { + this.isLoading = true; + this.isLoadingTx = true; + this.error = null; + document.body.scrollTo(0, 0); + this.txId = params.get('id') || ''; + this.seoService.setTitle('Transaction: ' + this.txId, true); + if (history.state.data) { + return of(history.state.data); + } + return this.bisqApiService.getTransaction$(this.txId) + .pipe( + catchError((bisqTxError: HttpErrorResponse) => { + if (bisqTxError.status === 404) { + return this.electrsApiService.getTransaction$(this.txId) + .pipe( + map((tx) => { + if (tx.status.confirmed) { + this.error = { + status: 200, + statusText: 'Transaction is confirmed but not available in the Bisq database, please try reloading this page.' + }; + return null; + } + return tx; + }), + catchError((txError: HttpErrorResponse) => { + console.log(txError); + this.error = txError; + return of(null); + }) + ); + } + this.error = bisqTxError; + return of(null); + }) + ); + }), + switchMap((tx) => { + if (!tx) { + return of(null); + } + + if (tx.version) { + this.router.navigate(['/tx/', this.txId], { state: { data: tx, bsqTx: true }}); + return of(null); + } + + this.bisqTx = tx; + this.isLoading = false; + + return this.electrsApiService.getTransaction$(this.txId); + }), + ) + .subscribe((tx) => { + this.isLoadingTx = false; + + if (!tx) { + return; + } + + this.tx = tx; + }, + (error) => { + this.error = error; + }); + + this.latestBlock$ = this.stateService.blocks$.pipe(map((([block]) => block))); + + this.stateService.bsqPrice$ + .subscribe((bsqPrice) => { + this.price = bsqPrice; + }); + } + + ngOnDestroy() { + this.subscription.unsubscribe(); + } +} 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..7cfa7a407 --- /dev/null +++ b/frontend/src/app/bisq/bisq-transactions/bisq-transactions.component.html @@ -0,0 +1,47 @@ +
+

Transactions

+
+ +
+ + + + + + + + + + + + + + + + + + +
TransactionTypeAmountConfirmedHeight
{{ tx.id | slice : 0 : 8 }} + + {{ tx.txTypeDisplayString }} + + + + {{ tx.burntFee / 100 | number: '1.2-2' }} BSQ + + + {{ calculateTotalOutput(tx.outputs) / 100 | number: '1.2-2' }} BSQ + + ago{{ tx.blockHeight }}
+ +
+ + + +
+ + + + + + \ 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..96977ce51 --- /dev/null +++ b/frontend/src/app/bisq/bisq-transactions/bisq-transactions.component.ts @@ -0,0 +1,71 @@ +import { Component, OnInit } from '@angular/core'; +import { BisqTransaction, BisqOutput } from '../bisq.interfaces'; +import { Subject } from 'rxjs'; +import { switchMap, tap } from 'rxjs/operators'; +import { BisqApiService } from '../bisq-api.service'; +import { SeoService } from 'src/app/services/seo.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 - (165 + 75); + fiveItemsPxSize = 250; + isLoading = true; + loadingItems: number[]; + pageSubject$ = new Subject(); + + // @ts-ignore + paginationSize: 'sm' | 'lg' = 'md'; + paginationMaxSize = 10; + + constructor( + private bisqApiService: BisqApiService, + private seoService: SeoService, + ) { } + + ngOnInit(): void { + this.seoService.setTitle('Transactions', true); + + this.itemsPerPage = Math.max(Math.round(this.contentSpace / this.fiveItemsPxSize) * 5, 10); + this.loadingItems = Array(this.itemsPerPage); + + if (document.body.clientWidth < 768) { + this.paginationSize = 'sm'; + this.paginationMaxSize = 3; + } + + this.pageSubject$ + .pipe( + tap(() => this.isLoading = true), + switchMap((page) => this.bisqApiService.listTransactions$((page - 1) * this.itemsPerPage, this.itemsPerPage)) + ) + .subscribe((response) => { + this.isLoading = false; + 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); + } + + trackByFn(index: number) { + return index; + } +} diff --git a/frontend/src/app/bisq/bisq-transfers/bisq-transfers.component.html b/frontend/src/app/bisq/bisq-transfers/bisq-transfers.component.html new file mode 100644 index 000000000..d2da552f2 --- /dev/null +++ b/frontend/src/app/bisq/bisq-transfers/bisq-transfers.component.html @@ -0,0 +1,77 @@ +
+ + +
+
+ Burnt: {{ tx.burntFee / 100 | number: '1.2-2' }} BSQ () +
+ +
+ + +   + + +
+
+
+ +
\ No newline at end of file diff --git a/frontend/src/app/bisq/bisq-transfers/bisq-transfers.component.scss b/frontend/src/app/bisq/bisq-transfers/bisq-transfers.component.scss new file mode 100644 index 000000000..3f78768ca --- /dev/null +++ b/frontend/src/app/bisq/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/bisq/bisq-transfers/bisq-transfers.component.ts b/frontend/src/app/bisq/bisq-transfers/bisq-transfers.component.ts new file mode 100644 index 000000000..be391305c --- /dev/null +++ b/frontend/src/app/bisq/bisq-transfers/bisq-transfers.component.ts @@ -0,0 +1,42 @@ +import { Component, OnInit, ChangeDetectionStrategy, Input, OnChanges } from '@angular/core'; +import { BisqTransaction } from 'src/app/bisq/bisq.interfaces'; +import { StateService } from 'src/app/services/state.service'; +import { map } from 'rxjs/operators'; +import { Observable } from 'rxjs'; +import { Block } from 'src/app/interfaces/electrs.interface'; + +@Component({ + selector: 'app-bisq-transfers', + templateUrl: './bisq-transfers.component.html', + styleUrls: ['./bisq-transfers.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class BisqTransfersComponent implements OnInit, OnChanges { + @Input() tx: BisqTransaction; + @Input() showConfirmations = false; + + totalOutput: number; + latestBlock$: Observable; + + constructor( + private stateService: StateService, + ) { } + + trackByIndexFn(index: number) { + return index; + } + + ngOnInit() { + this.latestBlock$ = this.stateService.blocks$.pipe(map(([block]) => block)); + } + + ngOnChanges() { + this.totalOutput = this.tx.outputs.filter((output) => output.isVerified).reduce((acc, output) => acc + output.bsqAmount, 0);; + } + + switchCurrency() { + const oldvalue = !this.stateService.viewFiat$.value; + this.stateService.viewFiat$.next(oldvalue); + } + +} diff --git a/frontend/src/app/bisq/bisq.interfaces.ts b/frontend/src/app/bisq/bisq.interfaces.ts new file mode 100644 index 000000000..710bada2a --- /dev/null +++ b/frontend/src/app/bisq/bisq.interfaces.ts @@ -0,0 +1,82 @@ + +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; +} + +export interface BisqStats { + minted: number; + burnt: number; + addresses: number; + unspent_txos: number; + spent_txos: number; +} + +interface BisqScriptPubKey { + addresses: string[]; + asm: string; + hex: string; + reqSigs: number; + type: string; +} + +interface SpentInfo { + height: number; + inputIndex: number; + txId: string; +} diff --git a/frontend/src/app/bisq/bisq.module.ts b/frontend/src/app/bisq/bisq.module.ts new file mode 100644 index 000000000..29dd1c0c0 --- /dev/null +++ b/frontend/src/app/bisq/bisq.module.ts @@ -0,0 +1,60 @@ +import { NgModule } from '@angular/core'; +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'; +import { BisqTransactionComponent } from './bisq-transaction/bisq-transaction.component'; +import { BisqBlockComponent } from './bisq-block/bisq-block.component'; +import { BisqIconComponent } from './bisq-icon/bisq-icon.component'; +import { BisqTransactionDetailsComponent } from './bisq-transaction-details/bisq-transaction-details.component'; +import { BisqTransfersComponent } from './bisq-transfers/bisq-transfers.component'; +import { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontawesome'; +import { faLeaf, faQuestion, faExclamationTriangle, faRocket, faRetweet, faFileAlt, faMoneyBill, + faEye, faEyeSlash, faLock, faLockOpen } from '@fortawesome/free-solid-svg-icons'; +import { BisqBlocksComponent } from './bisq-blocks/bisq-blocks.component'; +import { BisqExplorerComponent } from './bisq-explorer/bisq-explorer.component'; +import { BisqApiService } from './bisq-api.service'; +import { BisqAddressComponent } from './bisq-address/bisq-address.component'; +import { BisqStatsComponent } from './bisq-stats/bisq-stats.component'; +import { BsqAmountComponent } from './bsq-amount/bsq-amount.component'; + +@NgModule({ + declarations: [ + BisqTransactionsComponent, + BisqTransactionComponent, + BisqBlockComponent, + BisqTransactionComponent, + BisqIconComponent, + BisqTransactionDetailsComponent, + BisqTransfersComponent, + BisqBlocksComponent, + BisqExplorerComponent, + BisqAddressComponent, + BisqStatsComponent, + BsqAmountComponent, + ], + imports: [ + BisqRoutingModule, + SharedModule, + NgbPaginationModule, + FontAwesomeModule, + ], + providers: [ + BisqApiService, + ] +}) +export class BisqModule { + 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); + } +} 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..fdac7de60 --- /dev/null +++ b/frontend/src/app/bisq/bisq.routing.module.ts @@ -0,0 +1,59 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { AboutComponent } from '../components/about/about.component'; +import { AddressComponent } from '../components/address/address.component'; +import { BisqTransactionsComponent } from './bisq-transactions/bisq-transactions.component'; +import { BisqTransactionComponent } from './bisq-transaction/bisq-transaction.component'; +import { BisqBlockComponent } from './bisq-block/bisq-block.component'; +import { BisqBlocksComponent } from './bisq-blocks/bisq-blocks.component'; +import { BisqExplorerComponent } from './bisq-explorer/bisq-explorer.component'; +import { BisqAddressComponent } from './bisq-address/bisq-address.component'; +import { BisqStatsComponent } from './bisq-stats/bisq-stats.component'; + +const routes: Routes = [ + { + path: '', + component: BisqExplorerComponent, + children: [ + { + path: '', + component: BisqTransactionsComponent + }, + { + path: 'tx/:id', + component: BisqTransactionComponent + }, + { + path: 'blocks', + children: [], + component: BisqBlocksComponent + }, + { + path: 'block/:id', + component: BisqBlockComponent, + }, + { + path: 'address/:id', + component: BisqAddressComponent, + }, + { + path: 'stats', + component: BisqStatsComponent, + }, + { + path: 'about', + component: AboutComponent, + }, + { + path: '**', + redirectTo: '' + } + ] + } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class BisqRoutingModule { } diff --git a/frontend/src/app/bisq/bsq-amount/bsq-amount.component.html b/frontend/src/app/bisq/bsq-amount/bsq-amount.component.html new file mode 100644 index 000000000..f5de4cea6 --- /dev/null +++ b/frontend/src/app/bisq/bsq-amount/bsq-amount.component.html @@ -0,0 +1,6 @@ + + {{ conversions.USD * bsq / 100 * (bsqPrice$ | async) / 100000000 | currency:'USD':'symbol':'1.2-2' }} + + + {{ bsq / 100 | number : digitsInfo }} BSQ + diff --git a/frontend/src/app/bisq/bsq-amount/bsq-amount.component.scss b/frontend/src/app/bisq/bsq-amount/bsq-amount.component.scss new file mode 100644 index 000000000..843bd58b6 --- /dev/null +++ b/frontend/src/app/bisq/bsq-amount/bsq-amount.component.scss @@ -0,0 +1,3 @@ +.green-color { + color: #3bcc49; +} diff --git a/frontend/src/app/bisq/bsq-amount/bsq-amount.component.ts b/frontend/src/app/bisq/bsq-amount/bsq-amount.component.ts new file mode 100644 index 000000000..263b9d7f7 --- /dev/null +++ b/frontend/src/app/bisq/bsq-amount/bsq-amount.component.ts @@ -0,0 +1,30 @@ +import { Component, OnInit, ChangeDetectionStrategy, Input } from '@angular/core'; +import { StateService } from 'src/app/services/state.service'; +import { Observable } from 'rxjs'; + +@Component({ + selector: 'app-bsq-amount', + templateUrl: './bsq-amount.component.html', + styleUrls: ['./bsq-amount.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class BsqAmountComponent implements OnInit { + conversions$: Observable; + viewFiat$: Observable; + bsqPrice$: Observable; + + @Input() bsq: number; + @Input() digitsInfo = '1.2-2'; + @Input() forceFiat = false; + @Input() green = false; + + constructor( + private stateService: StateService, + ) { } + + ngOnInit() { + this.viewFiat$ = this.stateService.viewFiat$.asObservable(); + this.conversions$ = this.stateService.conversions$.asObservable(); + this.bsqPrice$ = this.stateService.bsqPrice$; + } +} diff --git a/frontend/src/app/components/about/about.component.html b/frontend/src/app/components/about/about.component.html index df9c17b71..78aacaf48 100644 --- a/frontend/src/app/components/about/about.component.html +++ b/frontend/src/app/components/about/about.component.html @@ -4,15 +4,14 @@

-

Contributors

+

Contributors

Development @softsimon_
Operations @wiz -
Design @markjborg

-

Github

+

Open source

@@ -29,54 +28,88 @@

-

HTTP API

+

API

- - - - - - - - - -
Fee API -
- -
-
Mempool blocks -
- -
-
+
+ +


diff --git a/frontend/src/app/components/about/about.component.scss b/frontend/src/app/components/about/about.component.scss index 3cb679ce3..5e208bcf8 100644 --- a/frontend/src/app/components/about/about.component.scss +++ b/frontend/src/app/components/about/about.component.scss @@ -9,4 +9,8 @@ tr { white-space: inherit; -} \ No newline at end of file +} + +.nowrap { + white-space: nowrap; +} diff --git a/frontend/src/app/components/about/about.component.ts b/frontend/src/app/components/about/about.component.ts index 7d5f52f3d..9b2f31adf 100644 --- a/frontend/src/app/components/about/about.component.ts +++ b/frontend/src/app/components/about/about.component.ts @@ -1,6 +1,7 @@ import { Component, OnInit } from '@angular/core'; import { WebsocketService } from '../../services/websocket.service'; import { SeoService } from 'src/app/services/seo.service'; +import { StateService } from 'src/app/services/state.service'; @Component({ selector: 'app-about', @@ -8,15 +9,24 @@ import { SeoService } from 'src/app/services/seo.service'; styleUrls: ['./about.component.scss'] }) export class AboutComponent implements OnInit { + active = 1; + hostname = document.location.hostname; + constructor( private websocketService: WebsocketService, private seoService: SeoService, + private stateService: StateService, ) { } ngOnInit() { this.seoService.setTitle('Contributors'); this.websocketService.want(['blocks']); + if (this.stateService.network === 'bisq') { + this.active = 2; + } + if (document.location.port !== '') { + this.hostname = this.hostname + ':' + document.location.port; + } } - } diff --git a/frontend/src/app/components/block/block.component.html b/frontend/src/app/components/block/block.component.html index e1e088b2c..5cb60e00d 100644 --- a/frontend/src/app/components/block/block.component.html +++ b/frontend/src/app/components/block/block.component.html @@ -157,7 +157,7 @@
Error loading block data. -
+

{{ error.error }}
diff --git a/frontend/src/app/components/block/block.component.ts b/frontend/src/app/components/block/block.component.ts index 9199a2e5b..7b43cf416 100644 --- a/frontend/src/app/components/block/block.component.ts +++ b/frontend/src/app/components/block/block.component.ts @@ -4,7 +4,7 @@ import { ActivatedRoute, ParamMap, Router } from '@angular/router'; import { ElectrsApiService } from '../../services/electrs-api.service'; import { switchMap, tap, debounceTime, catchError } from 'rxjs/operators'; import { Block, Transaction, Vout } from '../../interfaces/electrs.interface'; -import { of } from 'rxjs'; +import { of, Subscription } from 'rxjs'; import { StateService } from '../../services/state.service'; import { SeoService } from 'src/app/services/seo.service'; import { env } from 'src/app/app.constants'; @@ -25,6 +25,7 @@ export class BlockComponent implements OnInit, OnDestroy { isLoadingTransactions = true; error: any; blockSubsidy: number; + subscription: Subscription; fees: number; paginationMaxSize: number; page = 1; @@ -43,7 +44,7 @@ export class BlockComponent implements OnInit, OnDestroy { this.paginationMaxSize = window.matchMedia('(max-width: 700px)').matches ? 3 : 5; this.network = this.stateService.network; - this.route.paramMap + this.subscription = this.route.paramMap .pipe( switchMap((params: ParamMap) => { const blockHash: string = params.get('id') || ''; @@ -129,6 +130,7 @@ export class BlockComponent implements OnInit, OnDestroy { ngOnDestroy() { this.stateService.markBlock$.next({}); + this.subscription.unsubscribe(); } setBlockSubsidy() { 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..099f346b7 100644 --- a/frontend/src/app/components/master-page/master-page.component.html +++ b/frontend/src/app/components/master-page/master-page.component.html @@ -1,36 +1,52 @@