mirror of
https://github.com/mempool/mempool.git
synced 2025-01-18 21:32:55 +01:00
Address tracking and notification sounds.
This commit is contained in:
parent
3709b652ae
commit
a122432c24
@ -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);
|
||||
}
|
||||
});
|
||||
|
@ -9,7 +9,7 @@
|
||||
"ws": true
|
||||
},
|
||||
"/electrs": {
|
||||
"target": "https://www.blockstream.info/api/",
|
||||
"target": "https://www.blockstream.info/testnet/api/",
|
||||
"secure": false,
|
||||
"pathRewrite": {
|
||||
"^/electrs": ""
|
||||
|
@ -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]
|
||||
})
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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');
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
21
frontend/src/app/services/audio.service.ts
Normal file
21
frontend/src/app/services/audio.service.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
BIN
frontend/src/assets/sounds/cha-ching.mp3
Normal file
BIN
frontend/src/assets/sounds/cha-ching.mp3
Normal file
Binary file not shown.
BIN
frontend/src/assets/sounds/chime.mp3
Normal file
BIN
frontend/src/assets/sounds/chime.mp3
Normal file
Binary file not shown.
BIN
frontend/src/assets/sounds/magic.mp3
Normal file
BIN
frontend/src/assets/sounds/magic.mp3
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user