Merge pull request #5562 from mempool/mononaut/wallet-transactions
Wallet page transactions
This commit is contained in:
commit
803b005880
@ -281,9 +281,11 @@
|
|||||||
<div class="col" style="max-height: 410px" [style.order]="isMobile && widget.mobileOrder || 8">
|
<div class="col" style="max-height: 410px" [style.order]="isMobile && widget.mobileOrder || 8">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<span class="title-link">
|
<a class="title-link mb-0" style="margin-top: -2px" href="" [routerLink]="['/wallet/' + widget.props.wallet | relativeUrl]">
|
||||||
<h5 class="card-title d-inline" i18n="dashboard.treasury-transactions">Treasury Transactions</h5>
|
<h5 class="card-title d-inline" i18n="dashboard.treasury-transactions">Treasury Transactions</h5>
|
||||||
</span>
|
<span> </span>
|
||||||
|
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: var(--title-fg)"></fa-icon>
|
||||||
|
</a>
|
||||||
<app-address-transactions-widget [addressSummary$]="walletSummary$"></app-address-transactions-widget>
|
<app-address-transactions-widget [addressSummary$]="walletSummary$"></app-address-transactions-widget>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<div class="container-xl" [class.liquid-address]="network === 'liquid' || network === 'liquidtestnet'">
|
<div class="container-xl" [class.liquid-address]="network === 'liquid' || network === 'liquidtestnet'">
|
||||||
<div class="title-address">
|
<div class="title-address">
|
||||||
<h1 i18n="shared.wallet">Wallet</h1>
|
<h1>{{ walletName }}</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="clearfix"></div>
|
<div class="clearfix"></div>
|
||||||
@ -74,6 +74,36 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<div class="title-tx">
|
||||||
|
<h2 class="text-left" i18n="address.transactions">Transactions</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<app-transactions-list [transactions]="transactions" [showConfirmations]="true" [addresses]="addressStrings" (loadMore)="loadMore()"></app-transactions-list>
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<ng-template [ngIf]="isLoadingTransactions">
|
||||||
|
<div class="header-bg box">
|
||||||
|
<div class="row" style="height: 107px;">
|
||||||
|
<div class="col-sm">
|
||||||
|
<span class="skeleton-loader"></span>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm">
|
||||||
|
<span class="skeleton-loader"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<ng-template [ngIf]="retryLoadMore">
|
||||||
|
<br>
|
||||||
|
<button type="button" class="btn btn-outline-info btn-sm" (click)="loadMore()"><fa-icon [icon]="['fas', 'redo-alt']" [fixedWidth]="true"></fa-icon></button>
|
||||||
|
</ng-template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<ng-template #loadingTemplate>
|
<ng-template #loadingTemplate>
|
||||||
|
|
||||||
<div class="box" *ngIf="!error; else errorTemplate">
|
<div class="box" *ngIf="!error; else errorTemplate">
|
||||||
|
@ -9,6 +9,8 @@ import { of, Observable, Subscription } from 'rxjs';
|
|||||||
import { SeoService } from '@app/services/seo.service';
|
import { SeoService } from '@app/services/seo.service';
|
||||||
import { seoDescriptionNetwork } from '@app/shared/common.utils';
|
import { seoDescriptionNetwork } from '@app/shared/common.utils';
|
||||||
import { WalletAddress } from '@interfaces/node-api.interface';
|
import { WalletAddress } from '@interfaces/node-api.interface';
|
||||||
|
import { ElectrsApiService } from '@app/services/electrs-api.service';
|
||||||
|
import { AudioService } from '@app/services/audio.service';
|
||||||
|
|
||||||
class WalletStats implements ChainStats {
|
class WalletStats implements ChainStats {
|
||||||
addresses: string[];
|
addresses: string[];
|
||||||
@ -24,6 +26,7 @@ class WalletStats implements ChainStats {
|
|||||||
acc.funded_txo_sum += stat.funded_txo_sum;
|
acc.funded_txo_sum += stat.funded_txo_sum;
|
||||||
acc.spent_txo_count += stat.spent_txo_count;
|
acc.spent_txo_count += stat.spent_txo_count;
|
||||||
acc.spent_txo_sum += stat.spent_txo_sum;
|
acc.spent_txo_sum += stat.spent_txo_sum;
|
||||||
|
acc.tx_count += stat.tx_count;
|
||||||
return acc;
|
return acc;
|
||||||
}, {
|
}, {
|
||||||
funded_txo_count: 0,
|
funded_txo_count: 0,
|
||||||
@ -109,12 +112,17 @@ export class WalletComponent implements OnInit, OnDestroy {
|
|||||||
addressStrings: string[] = [];
|
addressStrings: string[] = [];
|
||||||
walletName: string;
|
walletName: string;
|
||||||
isLoadingWallet = true;
|
isLoadingWallet = true;
|
||||||
|
isLoadingTransactions = true;
|
||||||
|
transactions: Transaction[];
|
||||||
|
totalTransactionCount: number;
|
||||||
|
retryLoadMore = false;
|
||||||
wallet$: Observable<Record<string, WalletAddress>>;
|
wallet$: Observable<Record<string, WalletAddress>>;
|
||||||
walletAddresses$: Observable<Record<string, Address>>;
|
walletAddresses$: Observable<Record<string, Address>>;
|
||||||
walletSummary$: Observable<AddressTxSummary[]>;
|
walletSummary$: Observable<AddressTxSummary[]>;
|
||||||
walletStats$: Observable<WalletStats>;
|
walletStats$: Observable<WalletStats>;
|
||||||
error: any;
|
error: any;
|
||||||
walletSubscription: Subscription;
|
walletSubscription: Subscription;
|
||||||
|
transactionSubscription: Subscription;
|
||||||
|
|
||||||
collapseAddresses: boolean = true;
|
collapseAddresses: boolean = true;
|
||||||
|
|
||||||
@ -129,6 +137,8 @@ export class WalletComponent implements OnInit, OnDestroy {
|
|||||||
private websocketService: WebsocketService,
|
private websocketService: WebsocketService,
|
||||||
private stateService: StateService,
|
private stateService: StateService,
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
|
private electrsApiService: ElectrsApiService,
|
||||||
|
private audioService: AudioService,
|
||||||
private seoService: SeoService,
|
private seoService: SeoService,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
@ -172,6 +182,21 @@ export class WalletComponent implements OnInit, OnDestroy {
|
|||||||
}),
|
}),
|
||||||
switchMap(initial => this.stateService.walletTransactions$.pipe(
|
switchMap(initial => this.stateService.walletTransactions$.pipe(
|
||||||
startWith(null),
|
startWith(null),
|
||||||
|
tap((transactions) => {
|
||||||
|
if (!transactions?.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const transaction of transactions) {
|
||||||
|
const tx = this.transactions.find((t) => t.txid === transaction.txid);
|
||||||
|
if (tx) {
|
||||||
|
tx.status = transaction.status;
|
||||||
|
} else {
|
||||||
|
this.transactions.unshift(transaction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.transactions = this.transactions.slice();
|
||||||
|
this.audioService.playSound('magic');
|
||||||
|
}),
|
||||||
scan((wallet, walletTransactions) => {
|
scan((wallet, walletTransactions) => {
|
||||||
for (const tx of (walletTransactions || [])) {
|
for (const tx of (walletTransactions || [])) {
|
||||||
const funded: Record<string, number> = {};
|
const funded: Record<string, number> = {};
|
||||||
@ -267,8 +292,57 @@ export class WalletComponent implements OnInit, OnDestroy {
|
|||||||
return stats;
|
return stats;
|
||||||
}, walletStats),
|
}, walletStats),
|
||||||
);
|
);
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.transactionSubscription = this.wallet$.pipe(
|
||||||
|
switchMap(wallet => {
|
||||||
|
const addresses = Object.keys(wallet).map(addr => this.normalizeAddress(addr));
|
||||||
|
return this.electrsApiService.getAddressesTransactions$(addresses);
|
||||||
|
}),
|
||||||
|
map(transactions => {
|
||||||
|
// only confirmed transactions supported for now
|
||||||
|
return transactions.filter(tx => tx.status.confirmed).sort((a, b) => b.status.block_height - a.status.block_height);
|
||||||
|
}),
|
||||||
|
catchError((error) => {
|
||||||
|
console.log(error);
|
||||||
|
this.error = error;
|
||||||
|
this.seoService.logSoft404();
|
||||||
|
this.isLoadingWallet = false;
|
||||||
|
return of([]);
|
||||||
|
})
|
||||||
|
).subscribe((transactions: Transaction[] | null) => {
|
||||||
|
if (!transactions) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.transactions = transactions;
|
||||||
|
this.isLoadingTransactions = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadMore(): void {
|
||||||
|
if (this.isLoadingTransactions || this.fullyLoaded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.isLoadingTransactions = true;
|
||||||
|
this.retryLoadMore = false;
|
||||||
|
this.electrsApiService.getAddressesTransactions$(this.addressStrings, this.transactions[this.transactions.length - 1].txid)
|
||||||
|
.subscribe((transactions: Transaction[]) => {
|
||||||
|
if (transactions && transactions.length) {
|
||||||
|
this.transactions = this.transactions.concat(transactions.sort((a, b) => b.status.block_height - a.status.block_height));
|
||||||
|
} else {
|
||||||
|
this.fullyLoaded = true;
|
||||||
|
}
|
||||||
|
this.isLoadingTransactions = false;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
this.isLoadingTransactions = false;
|
||||||
|
this.retryLoadMore = true;
|
||||||
|
// In the unlikely event of the txid wasn't found in the mempool anymore and we must reload the page.
|
||||||
|
if (error.status === 422) {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
deduplicateWalletTransactions(walletTransactions: AddressTxSummary[]): AddressTxSummary[] {
|
deduplicateWalletTransactions(walletTransactions: AddressTxSummary[]): AddressTxSummary[] {
|
||||||
@ -299,5 +373,6 @@ export class WalletComponent implements OnInit, OnDestroy {
|
|||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.websocketService.stopTrackingWallet();
|
this.websocketService.stopTrackingWallet();
|
||||||
this.walletSubscription.unsubscribe();
|
this.walletSubscription.unsubscribe();
|
||||||
|
this.transactionSubscription.unsubscribe();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { AddressTxSummary, Block, ChainStats, Transaction } from "./electrs.interface";
|
import { AddressTxSummary, Block, ChainStats } from "./electrs.interface";
|
||||||
|
|
||||||
export interface OptimizedMempoolStats {
|
export interface OptimizedMempoolStats {
|
||||||
added: number;
|
added: number;
|
||||||
|
@ -142,12 +142,16 @@ export class ElectrsApiService {
|
|||||||
return this.httpClient.get<Transaction[]>(this.apiBaseUrl + this.apiBasePath + '/api/address/' + address + '/txs', { params });
|
return this.httpClient.get<Transaction[]>(this.apiBaseUrl + this.apiBasePath + '/api/address/' + address + '/txs', { params });
|
||||||
}
|
}
|
||||||
|
|
||||||
getAddressesTransactions$(addresses: string[], txid?: string): Observable<Transaction[]> {
|
getAddressesTransactions$(addresses: string[], txid?: string): Observable<Transaction[]> {
|
||||||
let params = new HttpParams();
|
let params = new HttpParams();
|
||||||
if (txid) {
|
if (txid) {
|
||||||
params = params.append('after_txid', txid);
|
params = params.append('after_txid', txid);
|
||||||
}
|
}
|
||||||
return this.httpClient.get<Transaction[]>(this.apiBaseUrl + this.apiBasePath + `/api/addresses/txs?addresses=${addresses.join(',')}`, { params });
|
return this.httpClient.post<Transaction[]>(
|
||||||
|
this.apiBaseUrl + this.apiBasePath + '/api/addresses/txs',
|
||||||
|
addresses,
|
||||||
|
{ params }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getAddressSummary$(address: string, txid?: string): Observable<AddressTxSummary[]> {
|
getAddressSummary$(address: string, txid?: string): Observable<AddressTxSummary[]> {
|
||||||
@ -163,7 +167,7 @@ export class ElectrsApiService {
|
|||||||
if (txid) {
|
if (txid) {
|
||||||
params = params.append('after_txid', txid);
|
params = params.append('after_txid', txid);
|
||||||
}
|
}
|
||||||
return this.httpClient.get<AddressTxSummary[]>(this.apiBaseUrl + this.apiBasePath + `/api/addresses/txs/summary?addresses=${addresses.join(',')}`, { params });
|
return this.httpClient.post<AddressTxSummary[]>(this.apiBaseUrl + this.apiBasePath + '/api/addresses/txs/summary', addresses, { params });
|
||||||
}
|
}
|
||||||
|
|
||||||
getScriptHashTransactions$(script: string, txid?: string): Observable<Transaction[]> {
|
getScriptHashTransactions$(script: string, txid?: string): Observable<Transaction[]> {
|
||||||
@ -182,7 +186,7 @@ export class ElectrsApiService {
|
|||||||
params = params.append('after_txid', txid);
|
params = params.append('after_txid', txid);
|
||||||
}
|
}
|
||||||
return from(Promise.all(scripts.map(script => calcScriptHash$(script)))).pipe(
|
return from(Promise.all(scripts.map(script => calcScriptHash$(script)))).pipe(
|
||||||
switchMap(scriptHashes => this.httpClient.get<Transaction[]>(this.apiBaseUrl + this.apiBasePath + `/api/scripthashes/txs?scripthashes=${scriptHashes.join(',')}`, { params })),
|
switchMap(scriptHashes => this.httpClient.post<Transaction[]>(this.apiBaseUrl + this.apiBasePath + '/api/scripthashes/txs', scriptHashes, { params })),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -212,7 +216,7 @@ export class ElectrsApiService {
|
|||||||
params = params.append('after_txid', txid);
|
params = params.append('after_txid', txid);
|
||||||
}
|
}
|
||||||
return from(Promise.all(scripts.map(script => calcScriptHash$(script)))).pipe(
|
return from(Promise.all(scripts.map(script => calcScriptHash$(script)))).pipe(
|
||||||
switchMap(scriptHashes => this.httpClient.get<AddressTxSummary[]>(this.apiBaseUrl + this.apiBasePath + `/api/scripthashes/txs/summary?scripthashes=${scriptHashes.join(',')}`, { params })),
|
switchMap(scriptHashes => this.httpClient.post<AddressTxSummary[]>(this.apiBaseUrl + this.apiBasePath + '/api/scripthashes/txs/summary', scriptHashes, { params })),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user