Address tracking and notification sounds.

This commit is contained in:
softsimon 2020-02-26 04:29:57 +07:00
parent 3709b652ae
commit a122432c24
13 changed files with 110 additions and 25 deletions

View File

@ -165,13 +165,11 @@ class Server {
const foundTransactions: TransactionExtended[] = [];
transactions.forEach((tx) => {
const someVin = tx.vin.some((vin) => vin.prevout.scriptpubkey_address === client['track-address']);
if (someVin) {
if (tx.vin.some((vin) => vin.prevout.scriptpubkey_address === client['track-address'])) {
foundTransactions.push(tx);
return;
}
const someVout = tx.vout.some((vout) => vout.scriptpubkey_address === client['track-address']);
if (someVout) {
if (tx.vout.some((vout) => vout.scriptpubkey_address === client['track-address'])) {
foundTransactions.push(tx);
}
});

View File

@ -9,7 +9,7 @@
"ws": true
},
"/electrs": {
"target": "https://www.blockstream.info/api/",
"target": "https://www.blockstream.info/testnet/api/",
"secure": false,
"pathRewrite": {
"^/electrs": ""

View File

@ -40,6 +40,7 @@ import { BlockchainBlocksComponent } from './components/blockchain-blocks/blockc
import { BlockchainComponent } from './components/blockchain/blockchain.component';
import { FooterComponent } from './components/footer/footer.component';
import { ExplorerComponent } from './components/explorer/explorer.component';
import { AudioService } from './services/audio.service';
@NgModule({
declarations: [
@ -87,6 +88,7 @@ import { ExplorerComponent } from './components/explorer/explorer.component';
StateService,
WebsocketService,
VbytesPipe,
AudioService,
],
bootstrap: [AppComponent]
})

View File

@ -6,13 +6,31 @@
<h1>Contributors</h1>
<p>Mempool.Space is a realtime Bitcoin blockchain explorer and mempool visualizer.</p>
<p>Development <a href="https://twitter.com/softbtc">@softbtc</a>
<p>Development <a href="https://twitter.com/softsimon_">@softsimon_</a>
<br />Operations <a href="https://twitter.com/wiz">@wiz</a>
<br />Design <a href="https://instagram.com/markjborg">@markjborg</a>
<br><br>
<h2>Github</h2>
<a class="b2812e30 f2874b88 fw6 mb3 mt2 truncate black-80 f4 link" rel="noopener noreferrer nofollow" href="https://github.com/mempool-space/mempool.space">
<span class="_9e13d83d dib v-mid">
<svg style="height: 16px;margin-right: 8px;" viewBox="0 0 92 92" version="1.1" xmlns="http://www.w3.org/2000/svg">
<title>Git</title>
<g stroke="none" fill="#FFFFFF">
<path d="M90.155,41.965 L50.036,1.847 C47.726,-0.464 43.979,-0.464 41.667,1.847 L33.336,10.179 L43.904,20.747 C46.36,19.917 49.176,20.474 51.133,22.431 C53.102,24.401 53.654,27.241 52.803,29.706 L62.989,39.891 C65.454,39.041 68.295,39.59 70.264,41.562 C73.014,44.311 73.014,48.768 70.264,51.519 C67.512,54.271 63.056,54.271 60.303,51.519 C58.235,49.449 57.723,46.409 58.772,43.861 L49.272,34.362 L49.272,59.358 C49.942,59.69 50.575,60.133 51.133,60.69 C53.883,63.44 53.883,67.896 51.133,70.65 C48.383,73.399 43.924,73.399 41.176,70.65 C38.426,67.896 38.426,63.44 41.176,60.69 C41.856,60.011 42.643,59.497 43.483,59.153 L43.483,33.925 C42.643,33.582 41.858,33.072 41.176,32.389 C39.093,30.307 38.592,27.249 39.661,24.691 L29.243,14.271 L1.733,41.779 C-0.578,44.092 -0.578,47.839 1.733,50.15 L41.854,90.268 C44.164,92.578 47.91,92.578 50.223,90.268 L90.155,50.336 C92.466,48.025 92.466,44.275 90.155,41.965"></path>
</g>
</svg>
</span>
<span>github.com/mempool-space/mempool.space</span></a>
</div>
<h2>HTTP API</h2>
<br><br>
<div class="text-center">
<h2>HTTP API</h2>
</div>
<table class="table">
<tr>
@ -33,7 +51,11 @@
</tr>
</table>
<h2>WebSocket API</h2>
<br><br>
<div class="text-center">
<h2>WebSocket API</h2>
</div>
<table class="table">
<tr>

View File

@ -13,17 +13,17 @@
<div class="col">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td>Number of transactions</td>
<td>{{ address.chain_stats.tx_count + address.mempool_stats.tx_count }}</td>
</tr>
<tr>
<td>Total received</td>
<td>{{ (address.chain_stats.funded_txo_sum + address.mempool_stats.funded_txo_sum) / 100000000 | number: '1.2-2' }} BTC</td>
<td>{{ receieved / 100000000 | number: '1.2-8' }} BTC</td>
</tr>
<tr>
<td>Total sent</td>
<td>{{ (address.chain_stats.spent_txo_sum + address.mempool_stats.spent_txo_sum) / 100000000 | number: '1.2-2' }} BTC</td>
<td>{{ sent / 100000000 | number: '1.2-8' }} BTC</td>
</tr>
<tr>
<td>Balance</td>
<td>{{ (receieved - sent) / 100000000 | number: '1.2-8' }} BTC</td>
</tr>
</tbody>
</table>
@ -40,7 +40,7 @@
<br>
<h2><ng-template [ngIf]="transactions?.length">{{ transactions?.length || '?' }} of </ng-template>{{ address.chain_stats.tx_count + address.mempool_stats.tx_count + addedTransactions }} transactions</h2>
<h2><ng-template [ngIf]="transactions?.length">{{ transactions?.length || '?' }} of </ng-template>{{ txCount }} transactions</h2>
<app-transactions-list [transactions]="transactions" [showConfirmations]="true"></app-transactions-list>
@ -49,7 +49,7 @@
<div class="spinner-border"></div>
<br><br>
</ng-template>
<button *ngIf="transactions?.length && transactions?.length !== (address.chain_stats.tx_count + address.mempool_stats.tx_count)" type="button" class="btn btn-primary" (click)="loadMore()">Load more</button>
<button *ngIf="transactions?.length && transactions?.length !== txCount" type="button" class="btn btn-primary" (click)="loadMore()">Load more</button>
</div>
</ng-template>

View File

@ -5,6 +5,7 @@ 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';
import { AudioService } from 'src/app/services/audio.service';
@Component({
selector: 'app-address',
@ -18,13 +19,17 @@ export class AddressComponent implements OnInit, OnDestroy {
transactions: Transaction[];
isLoadingTransactions = true;
error: any;
addedTransactions = 0;
txCount = 0;
receieved = 0;
sent = 0;
constructor(
private route: ActivatedRoute,
private electrsApiService: ElectrsApiService,
private websocketService: WebsocketService,
private stateService: StateService,
private audioService: AudioService,
) { }
ngOnInit() {
@ -43,9 +48,10 @@ export class AddressComponent implements OnInit, OnDestroy {
)
.subscribe((address) => {
this.address = address;
this.updateChainStats();
this.websocketService.startTrackAddress(address.address);
this.isLoadingAddress = false;
this.getAddressTransactions(address.address);
this.reloadAddressTransactions(address.address);
},
(error) => {
console.log(error);
@ -56,7 +62,25 @@ export class AddressComponent implements OnInit, OnDestroy {
this.stateService.mempoolTransactions$
.subscribe((transaction) => {
this.transactions.unshift(transaction);
this.addedTransactions++;
this.transactions = this.transactions.slice();
this.txCount++;
if (transaction.vout.some((vout) => vout.scriptpubkey_address === this.address.address)) {
this.audioService.playSound('cha-ching');
} else {
this.audioService.playSound('chime');
}
transaction.vin.forEach((vin) => {
if (vin.prevout.scriptpubkey_address === this.address.address) {
this.sent += vin.prevout.value;
}
});
transaction.vout.forEach((vout) => {
if (vout.scriptpubkey_address === this.address.address) {
this.receieved += vout.value;
}
});
});
this.stateService.blockTransactions$
@ -64,6 +88,8 @@ export class AddressComponent implements OnInit, OnDestroy {
const tx = this.transactions.find((t) => t.txid === transaction.txid);
if (tx) {
tx.status = transaction.status;
this.transactions = this.transactions.slice();
this.audioService.playSound('magic');
}
});
@ -71,15 +97,24 @@ export class AddressComponent implements OnInit, OnDestroy {
.subscribe((state) => {
if (!state && this.transactions && this.transactions.length) {
this.isLoadingTransactions = true;
this.getAddressTransactions(this.address.address);
this.reloadAddressTransactions(this.address.address);
}
});
}
getAddressTransactions(address: string) {
updateChainStats() {
this.receieved = this.address.chain_stats.funded_txo_sum + this.address.mempool_stats.funded_txo_sum;
this.sent = this.address.chain_stats.spent_txo_sum + this.address.mempool_stats.spent_txo_sum;
this.txCount = this.address.chain_stats.tx_count + this.address.mempool_stats.tx_count;
}
reloadAddressTransactions(address: string) {
this.isLoadingTransactions = true;
this.electrsApiService.getAddressTransactions$(address)
.subscribe((transactions: any) => {
this.transactions = transactions;
this.updateChainStats();
this.isLoadingTransactions = false;
});
}

View File

@ -59,6 +59,7 @@ export class BlockComponent implements OnInit {
this.block = block;
this.blockHeight = block.height;
this.isLoadingBlock = false;
this.setBlockSubsidy();
this.getBlockTransactions(block.id);
},
(error) => {
@ -74,6 +75,9 @@ export class BlockComponent implements OnInit {
this.conversions = conversions;
});
}
setBlockSubsidy() {
let halvenings = Math.floor(this.block.height / 210000);
while (halvenings > 0) {
this.blockSubsidy = this.blockSubsidy / 2;

View File

@ -6,6 +6,7 @@ import { Transaction, Block } from '../../interfaces/electrs.interface';
import { of } from 'rxjs';
import { StateService } from '../../services/state.service';
import { WebsocketService } from '../../services/websocket.service';
import { AudioService } from 'src/app/services/audio.service';
@Component({
selector: 'app-transaction',
@ -28,6 +29,7 @@ export class TransactionComponent implements OnInit, OnDestroy {
private electrsApiService: ElectrsApiService,
private stateService: StateService,
private websocketService: WebsocketService,
private audioService: AudioService,
) { }
ngOnInit() {
@ -73,6 +75,7 @@ export class TransactionComponent implements OnInit, OnDestroy {
block_hash: block.id,
block_time: block.timestamp,
};
this.audioService.playSound('magic');
});
}

View File

@ -1,7 +1,7 @@
import { Component, OnInit, Input, ChangeDetectionStrategy, OnChanges, ChangeDetectorRef } from '@angular/core';
import { StateService } from '../../services/state.service';
import { Observable, forkJoin } from 'rxjs';
import { Block, Outspend } from '../../interfaces/electrs.interface';
import { Block, Outspend, Transaction } from '../../interfaces/electrs.interface';
import { ElectrsApiService } from '../../services/electrs-api.service';
@Component({
@ -62,7 +62,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
this.stateService.viewFiat$.next(oldvalue);
}
trackByFn(index: number) {
return index;
trackByFn(index: number, tx: Transaction) {
return tx.txid;
}
}

View File

@ -0,0 +1,21 @@
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class AudioService {
audio = new Audio();
constructor() { }
public playSound(name: 'magic' | 'chime' | 'cha-ching') {
try {
this.audio.src = '../../../assets/sounds/' + name + '.mp3';
this.audio.load();
this.audio.play();
} catch (e) {
console.log('Play sound failed', e);
}
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.