diff --git a/backend/src/api/bitcoin/electrs-api.ts b/backend/src/api/bitcoin/electrs-api.ts index 34c210a99..d4100e88b 100644 --- a/backend/src/api/bitcoin/electrs-api.ts +++ b/backend/src/api/bitcoin/electrs-api.ts @@ -70,7 +70,7 @@ class ElectrsApi { }); } - getTxIdsForBlock(hash: string): Promise { + getTxIdsForBlock(hash: string): Promise { return new Promise((resolve, reject) => { request(config.ELECTRS_API_URL + '/block/' + hash + '/txids', { json: true, timeout: 10000 }, (err, res, response) => { if (err) { diff --git a/backend/src/api/blocks.ts b/backend/src/api/blocks.ts index 3dc6b075d..0fd49b612 100644 --- a/backend/src/api/blocks.ts +++ b/backend/src/api/blocks.ts @@ -1,6 +1,7 @@ const config = require('../../mempool-config.json'); import bitcoinApi from './bitcoin/electrs-api'; -import { Block } from '../interfaces'; +import memPool from './mempool'; +import { Block, TransactionExtended } from '../interfaces'; class Blocks { private blocks: Block[] = []; @@ -39,15 +40,36 @@ class Blocks { const block = await bitcoinApi.getBlock(blockHash); const txIds = await bitcoinApi.getTxIdsForBlock(blockHash); - block.medianFee = 2; - block.feeRange = [1, 3]; + const mempool = memPool.getMempool(); + let found = 0; + let notFound = 0; + + const transactions: TransactionExtended[] = []; + + for (let i = 1; i < txIds.length; i++) { + if (mempool[txIds[i]]) { + transactions.push(mempool[txIds[i]]); + found++; + } else { + console.log(`Fetching block tx ${i} of ${txIds.length}`); + const tx = await memPool.getTransactionExtended(txIds[i]); + if (tx) { + transactions.push(tx); + } + notFound++; + } + } + + transactions.sort((a, b) => b.feePerVsize - a.feePerVsize); + block.medianFee = this.median(transactions.map((tx) => tx.feePerVsize)); + block.feeRange = this.getFeesInRange(transactions, 8); this.blocks.push(block); if (this.blocks.length > config.KEEP_BLOCK_AMOUNT) { this.blocks.shift(); } - this.newBlockCallback(block, txIds); + this.newBlockCallback(block, txIds, transactions); } } catch (err) { diff --git a/backend/src/api/mempool-blocks.ts b/backend/src/api/mempool-blocks.ts index fe9ed4acb..d0bae4dd0 100644 --- a/backend/src/api/mempool-blocks.ts +++ b/backend/src/api/mempool-blocks.ts @@ -1,5 +1,5 @@ const config = require('../../mempool-config.json'); -import { MempoolBlock, SimpleTransaction } from '../interfaces'; +import { MempoolBlock, TransactionExtended } from '../interfaces'; class MempoolBlocks { private mempoolBlocks: MempoolBlock[] = []; @@ -10,9 +10,9 @@ class MempoolBlocks { return this.mempoolBlocks; } - public updateMempoolBlocks(memPool: { [txid: string]: SimpleTransaction }): void { + public updateMempoolBlocks(memPool: { [txid: string]: TransactionExtended }): void { const latestMempool = memPool; - const memPoolArray: SimpleTransaction[] = []; + const memPoolArray: TransactionExtended[] = []; for (const i in latestMempool) { if (latestMempool.hasOwnProperty(i)) { memPoolArray.push(latestMempool[i]); @@ -23,11 +23,11 @@ class MempoolBlocks { this.mempoolBlocks = this.calculateMempoolBlocks(transactionsSorted); } - private calculateMempoolBlocks(transactionsSorted: SimpleTransaction[]): MempoolBlock[] { + private calculateMempoolBlocks(transactionsSorted: TransactionExtended[]): MempoolBlock[] { const mempoolBlocks: MempoolBlock[] = []; let blockWeight = 0; let blockSize = 0; - let transactions: SimpleTransaction[] = []; + let transactions: TransactionExtended[] = []; transactionsSorted.forEach((tx) => { if (blockWeight + tx.vsize < 1000000 || mempoolBlocks.length === config.DEFAULT_PROJECTED_BLOCKS_AMOUNT) { blockWeight += tx.vsize; @@ -46,7 +46,7 @@ class MempoolBlocks { return mempoolBlocks; } - private dataToMempoolBlocks(transactions: SimpleTransaction[], blockSize: number, blockVSize: number, blocksIndex: number): MempoolBlock { + private dataToMempoolBlocks(transactions: TransactionExtended[], blockSize: number, blockVSize: number, blocksIndex: number): MempoolBlock { let rangeLength = 3; if (blocksIndex === 0) { rangeLength = 8; @@ -79,7 +79,7 @@ class MempoolBlocks { return medianNr; } - private getFeesInRange(transactions: SimpleTransaction[], rangeLength: number) { + private getFeesInRange(transactions: TransactionExtended[], rangeLength: number) { const arr = [transactions[transactions.length - 1].feePerVsize]; const chunk = 1 / (rangeLength - 1); let itemsToAdd = rangeLength - 2; diff --git a/backend/src/api/mempool.ts b/backend/src/api/mempool.ts index 0ef0de801..a7b48d9e9 100644 --- a/backend/src/api/mempool.ts +++ b/backend/src/api/mempool.ts @@ -1,6 +1,6 @@ const config = require('../../mempool-config.json'); import bitcoinApi from './bitcoin/electrs-api'; -import { MempoolInfo, SimpleTransaction, Transaction } from '../interfaces'; +import { MempoolInfo, TransactionExtended, Transaction } from '../interfaces'; class Mempool { private mempoolCache: any = {}; @@ -21,7 +21,7 @@ class Mempool { this.mempoolChangedCallback = fn; } - public getMempool(): { [txid: string]: SimpleTransaction } { + public getMempool(): { [txid: string]: TransactionExtended } { return this.mempoolCache; } @@ -52,16 +52,13 @@ class Mempool { return this.vBytesPerSecond; } - public async getRawTransaction(txId: string): Promise { + public async getTransactionExtended(txId: string): Promise { try { const transaction: Transaction = await bitcoinApi.getRawTransaction(txId); - return { - txid: transaction.txid, - fee: transaction.fee, - size: transaction.size, + return Object.assign({ vsize: transaction.weight / 4, - feePerVsize: transaction.fee / (transaction.weight / 4) - }; + feePerVsize: transaction.fee / (transaction.weight / 4), + }, transaction); } catch (e) { console.log(txId + ' not found'); return false; @@ -76,10 +73,11 @@ class Mempool { try { const transactions = await bitcoinApi.getRawMempool(); const diff = transactions.length - Object.keys(this.mempoolCache).length; + const newTransactions: TransactionExtended[] = []; for (const txid of transactions) { if (!this.mempoolCache[txid]) { - const transaction = await this.getRawTransaction(txid); + const transaction = await this.getTransactionExtended(txid); if (transaction) { this.mempoolCache[txid] = transaction; txCount++; @@ -94,6 +92,7 @@ class Mempool { } else { console.log('Fetched transaction ' + txCount); } + newTransactions.push(transaction); } else { console.log('Error finding transaction in mempool.'); } @@ -117,7 +116,7 @@ class Mempool { this.mempoolCache = newMempool; if (hasChange && this.mempoolChangedCallback) { - this.mempoolChangedCallback(this.mempoolCache); + this.mempoolChangedCallback(this.mempoolCache, newTransactions); } const end = new Date().getTime(); diff --git a/backend/src/api/statistics.ts b/backend/src/api/statistics.ts index 9550d4935..aae76934c 100644 --- a/backend/src/api/statistics.ts +++ b/backend/src/api/statistics.ts @@ -1,7 +1,7 @@ import memPool from './mempool'; import { DB } from '../database'; -import { Statistic, SimpleTransaction, OptimizedStatistic } from '../interfaces'; +import { Statistic, TransactionExtended, OptimizedStatistic } from '../interfaces'; class Statistics { protected intervalTimer: NodeJS.Timer | undefined; @@ -37,7 +37,7 @@ class Statistics { console.log('Running statistics'); - let memPoolArray: SimpleTransaction[] = []; + let memPoolArray: TransactionExtended[] = []; for (const i in currentMempool) { if (currentMempool.hasOwnProperty(i)) { memPoolArray.push(currentMempool[i]); diff --git a/backend/src/index.ts b/backend/src/index.ts index 439506d2b..5d2fb145b 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -13,7 +13,7 @@ import mempoolBlocks from './api/mempool-blocks'; import diskCache from './api/disk-cache'; import statistics from './api/statistics'; -import { Block, SimpleTransaction, Statistic } from './interfaces'; +import { Block, TransactionExtended, Statistic } from './interfaces'; import fiatConversion from './api/fiat-conversion'; @@ -98,6 +98,15 @@ class Server { } } + if (parsedMessage && parsedMessage['track-address']) { + if (/^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[a-z]{2,5}1[ac-hj-np-z02-9]{8,87})$/ + .test(parsedMessage['track-address'])) { + client['track-address'] = parsedMessage['track-address']; + } else { + client['track-address'] = null; + } + } + if (parsedMessage.action === 'init') { const _blocks = blocks.getBlocks(); if (!_blocks) { @@ -133,7 +142,7 @@ class Server { }); }); - blocks.setNewBlockCallback((block: Block, txIds: string[]) => { + blocks.setNewBlockCallback((block: Block, txIds: string[], transactions: TransactionExtended[]) => { this.wss.clients.forEach((client) => { if (client.readyState !== WebSocket.OPEN) { return; @@ -143,21 +152,40 @@ class Server { return; } + const response = { + 'block': block + }; + if (client['track-tx'] && txIds.indexOf(client['track-tx']) > -1) { client['track-tx'] = null; - client.send(JSON.stringify({ - 'block': block, - 'txConfirmed': true, - })); - } else { - client.send(JSON.stringify({ - 'block': block, - })); + response['txConfirmed'] = true; } + + if (client['track-address']) { + const foundTransactions: TransactionExtended[] = []; + + transactions.forEach((tx) => { + const someVin = tx.vin.some((vin) => vin.prevout.scriptpubkey_address === client['track-address']); + if (someVin) { + foundTransactions.push(tx); + return; + } + const someVout = tx.vout.some((vout) => vout.scriptpubkey_address === client['track-address']); + if (someVout) { + foundTransactions.push(tx); + } + }); + + if (foundTransactions.length) { + response['address-block-transactions'] = foundTransactions; + } + } + + client.send(JSON.stringify(response)); }); }); - memPool.setMempoolChangedCallback((newMempool: { [txid: string]: SimpleTransaction }) => { + memPool.setMempoolChangedCallback((newMempool: { [txid: string]: TransactionExtended }, newTransactions: TransactionExtended[]) => { mempoolBlocks.updateMempoolBlocks(newMempool); const mBlocks = mempoolBlocks.getMempoolBlocks(); const mempoolInfo = memPool.getMempoolInfo(); @@ -179,6 +207,27 @@ class Server { response['mempool-blocks'] = mBlocks; } + // Send all new incoming transactions related to tracked address + if (client['track-address']) { + const foundTransactions: TransactionExtended[] = []; + + newTransactions.forEach((tx) => { + const someVin = tx.vin.some((vin) => vin.prevout.scriptpubkey_address === client['track-address']); + if (someVin) { + foundTransactions.push(tx); + return; + } + const someVout = tx.vout.some((vout) => vout.scriptpubkey_address === client['track-address']); + if (someVout) { + foundTransactions.push(tx); + } + }); + + if (foundTransactions.length) { + response['address-transactions'] = foundTransactions; + } + } + if (Object.keys(response).length) { client.send(JSON.stringify(response)); } diff --git a/backend/src/interfaces.ts b/backend/src/interfaces.ts index dc01e4f41..d7bf8b15b 100644 --- a/backend/src/interfaces.ts +++ b/backend/src/interfaces.ts @@ -27,7 +27,7 @@ export interface Transaction { status: Status; } -export interface SimpleTransaction { +export interface TransactionExtended extends Transaction { txid: string; fee: number; size: number; diff --git a/frontend/src/app/components/address/address.component.html b/frontend/src/app/components/address/address.component.html index 2c681059e..a418f74ce 100644 --- a/frontend/src/app/components/address/address.component.html +++ b/frontend/src/app/components/address/address.component.html @@ -40,7 +40,7 @@
-

{{ transactions?.length || '?' }} of {{ address.chain_stats.tx_count + address.mempool_stats.tx_count }} transactions

+

{{ transactions?.length || '?' }} of {{ address.chain_stats.tx_count + address.mempool_stats.tx_count + addedTransactions }} transactions

diff --git a/frontend/src/app/components/address/address.component.ts b/frontend/src/app/components/address/address.component.ts index 89eecb4ca..329b82aea 100644 --- a/frontend/src/app/components/address/address.component.ts +++ b/frontend/src/app/components/address/address.component.ts @@ -1,28 +1,35 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, OnDestroy } from '@angular/core'; import { ActivatedRoute, ParamMap } from '@angular/router'; import { ElectrsApiService } from '../../services/electrs-api.service'; import { switchMap } from 'rxjs/operators'; import { Address, Transaction } from '../../interfaces/electrs.interface'; +import { WebsocketService } from 'src/app/services/websocket.service'; +import { StateService } from 'src/app/services/state.service'; @Component({ selector: 'app-address', templateUrl: './address.component.html', styleUrls: ['./address.component.scss'] }) -export class AddressComponent implements OnInit { +export class AddressComponent implements OnInit, OnDestroy { address: Address; addressString: string; isLoadingAddress = true; transactions: Transaction[]; isLoadingTransactions = true; error: any; + addedTransactions = 0; constructor( private route: ActivatedRoute, private electrsApiService: ElectrsApiService, + private websocketService: WebsocketService, + private stateService: StateService, ) { } ngOnInit() { + this.websocketService.want(['blocks', 'mempool-blocks']); + this.route.paramMap.pipe( switchMap((params: ParamMap) => { this.error = undefined; @@ -35,6 +42,7 @@ export class AddressComponent implements OnInit { ) .subscribe((address) => { this.address = address; + this.websocketService.startTrackAddress(address.address); this.isLoadingAddress = false; document.body.scrollTo({ top: 0, behavior: 'smooth' }); this.getAddressTransactions(address.address); @@ -44,6 +52,28 @@ export class AddressComponent implements OnInit { this.error = error; this.isLoadingAddress = false; }); + + this.stateService.mempoolTransactions$ + .subscribe((transaction) => { + this.transactions.unshift(transaction); + this.addedTransactions++; + }); + + this.stateService.blockTransactions$ + .subscribe((transaction) => { + const tx = this.transactions.find((t) => t.txid === transaction.txid); + if (tx) { + tx.status = transaction.status; + } + }); + + this.stateService.isOffline$ + .subscribe((state) => { + if (!state && this.transactions && this.transactions.length) { + this.isLoadingTransactions = true; + this.getAddressTransactions(this.address.address); + } + }); } getAddressTransactions(address: string) { @@ -62,4 +92,8 @@ export class AddressComponent implements OnInit { this.isLoadingTransactions = false; }); } + + ngOnDestroy() { + this.websocketService.startTrackAddress('stop'); + } } 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 d9dedd870..c80a0ce33 100644 --- a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.ts +++ b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.ts @@ -69,13 +69,13 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy { if (window.innerWidth <= 768) { return { top: 155 * this.blocks.indexOf(block) + 'px', - background: `repeating-linear-gradient(#2d3348, #2d3348 ${greenBackgroundHeight}%, + background: `repeating-linear-gradient(to right, #2d3348, #2d3348 ${greenBackgroundHeight}%, #9339f4 ${Math.max(greenBackgroundHeight, 0)}%, #105fb0 100%)`, }; } else { return { left: 155 * this.blocks.indexOf(block) + 'px', - background: `repeating-linear-gradient(#2d3348, #2d3348 ${greenBackgroundHeight}%, + background: `repeating-linear-gradient(to right, #2d3348, #2d3348 ${greenBackgroundHeight}%, #9339f4 ${Math.max(greenBackgroundHeight, 0)}%, #105fb0 100%)`, }; } diff --git a/frontend/src/app/components/blockchain/blockchain.component.html b/frontend/src/app/components/blockchain/blockchain.component.html index de335bec9..48b4e7990 100644 --- a/frontend/src/app/components/blockchain/blockchain.component.html +++ b/frontend/src/app/components/blockchain/blockchain.component.html @@ -1,14 +1,17 @@ -
-

Waiting for blocks...

-
-
-
-
-
+
+ +
+

Waiting for blocks...

+
+
+
+
+ +
\ No newline at end of file diff --git a/frontend/src/app/components/blockchain/blockchain.component.scss b/frontend/src/app/components/blockchain/blockchain.component.scss index c2a77b3be..d1da713b9 100644 --- a/frontend/src/app/components/blockchain/blockchain.component.scss +++ b/frontend/src/app/components/blockchain/blockchain.component.scss @@ -51,6 +51,7 @@ position: absolute; text-align: center; margin: auto; - width: 100%; - top: 80px; + width: 300px; + left: -150px; + top: 0px; } \ No newline at end of file diff --git a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts index d4f79385a..f77f7468d 100644 --- a/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts +++ b/frontend/src/app/components/mempool-blocks/mempool-blocks.component.ts @@ -76,7 +76,6 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy { const feePosition = feeRangeChunkSize * feeRangeIndex + chunkPositionOffset; const blockedFilledPercentage = (block.blockVSize > 1000000 ? 1000000 : block.blockVSize) / 1000000; - console.log(txInBlockIndex); const arrowRightPosition = txInBlockIndex * (this.blockWidth + this.blockPadding) + ((1 - feePosition) * blockedFilledPercentage * this.blockWidth); diff --git a/frontend/src/app/components/search-form/search-form.component.html b/frontend/src/app/components/search-form/search-form.component.html index 90e06f221..756efefbf 100644 --- a/frontend/src/app/components/search-form/search-form.component.html +++ b/frontend/src/app/components/search-form/search-form.component.html @@ -1,4 +1,4 @@ -
+
diff --git a/frontend/src/app/components/transaction/transaction.component.html b/frontend/src/app/components/transaction/transaction.component.html index afd2dd616..fabee1dbd 100644 --- a/frontend/src/app/components/transaction/transaction.component.html +++ b/frontend/src/app/components/transaction/transaction.component.html @@ -11,7 +11,7 @@ - +
diff --git a/frontend/src/app/components/transaction/transaction.component.ts b/frontend/src/app/components/transaction/transaction.component.ts index 08e3d182a..0259b5aea 100644 --- a/frontend/src/app/components/transaction/transaction.component.ts +++ b/frontend/src/app/components/transaction/transaction.component.ts @@ -65,7 +65,7 @@ export class TransactionComponent implements OnInit, OnDestroy { this.stateService.blocks$ .subscribe((block) => this.latestBlock = block); - this.stateService.txConfirmed + this.stateService.txConfirmed$ .subscribe((block) => { this.tx.status = { confirmed: true, diff --git a/frontend/src/app/services/state.service.ts b/frontend/src/app/services/state.service.ts index c8b811f58..be00cf9cc 100644 --- a/frontend/src/app/services/state.service.ts +++ b/frontend/src/app/services/state.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@angular/core'; import { ReplaySubject, BehaviorSubject, Subject } from 'rxjs'; -import { Block } from '../interfaces/electrs.interface'; +import { Block, Transaction } from '../interfaces/electrs.interface'; import { MempoolBlock, MemPoolState } from '../interfaces/websocket.interface'; import { OptimizedMempoolStats } from '../interfaces/node-api.interface'; @@ -13,7 +13,10 @@ export class StateService { conversions$ = new ReplaySubject(1); mempoolStats$ = new ReplaySubject(); mempoolBlocks$ = new ReplaySubject(1); - txConfirmed = new Subject(); + txConfirmed$ = new Subject(); + mempoolTransactions$ = new Subject(); + blockTransactions$ = new Subject(); + live2Chart$ = new Subject(); viewFiat$ = new BehaviorSubject(false); diff --git a/frontend/src/app/services/websocket.service.ts b/frontend/src/app/services/websocket.service.ts index 47d2405d3..8b167ea2a 100644 --- a/frontend/src/app/services/websocket.service.ts +++ b/frontend/src/app/services/websocket.service.ts @@ -3,7 +3,7 @@ import { webSocket, WebSocketSubject } from 'rxjs/webSocket'; import { WebsocketResponse } from '../interfaces/websocket.interface'; import { retryWhen, tap, delay } from 'rxjs/operators'; import { StateService } from './state.service'; -import { Block } from '../interfaces/electrs.interface'; +import { Block, Transaction } from '../interfaces/electrs.interface'; const WEB_SOCKET_PROTOCOL = (document.location.protocol === 'https:') ? 'wss:' : 'ws:'; const WEB_SOCKET_URL = WEB_SOCKET_PROTOCOL + '//' + document.location.hostname + ':8999'; @@ -58,7 +58,7 @@ export class WebsocketService { if (response.txConfirmed) { this.trackingTxId = null; - this.stateService.txConfirmed.next(response.block); + this.stateService.txConfirmed$.next(response.block); } } @@ -70,6 +70,18 @@ export class WebsocketService { this.stateService.mempoolBlocks$.next(response['mempool-blocks']); } + if (response['address-transactions']) { + response['address-transactions'].forEach((addressTransaction: Transaction) => { + this.stateService.mempoolTransactions$.next(addressTransaction); + }); + } + + if (response['address-block-transactions']) { + response['address-block-transactions'].forEach((addressTransaction: Transaction) => { + this.stateService.blockTransactions$.next(addressTransaction); + }); + } + if (response['live-2h-chart']) { this.stateService.live2Chart$.next(response['live-2h-chart']); }