Merge pull request #5562 from mempool/mononaut/wallet-transactions

Wallet page transactions
This commit is contained in:
wiz 2025-01-15 16:33:57 +09:00 committed by GitHub
commit 803b005880
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 121 additions and 10 deletions

View File

@ -281,9 +281,11 @@
<div class="col" style="max-height: 410px" [style.order]="isMobile && widget.mobileOrder || 8">
<div class="card">
<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>
</span>
<span>&nbsp;</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>
</div>
</div>

View File

@ -1,6 +1,6 @@
<div class="container-xl" [class.liquid-address]="network === 'liquid' || network === 'liquidtestnet'">
<div class="title-address">
<h1 i18n="shared.wallet">Wallet</h1>
<h1>{{ walletName }}</h1>
</div>
<div class="clearfix"></div>
@ -74,6 +74,36 @@
</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>
<div class="box" *ngIf="!error; else errorTemplate">

View File

@ -9,6 +9,8 @@ import { of, Observable, Subscription } from 'rxjs';
import { SeoService } from '@app/services/seo.service';
import { seoDescriptionNetwork } from '@app/shared/common.utils';
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 {
addresses: string[];
@ -24,6 +26,7 @@ class WalletStats implements ChainStats {
acc.funded_txo_sum += stat.funded_txo_sum;
acc.spent_txo_count += stat.spent_txo_count;
acc.spent_txo_sum += stat.spent_txo_sum;
acc.tx_count += stat.tx_count;
return acc;
}, {
funded_txo_count: 0,
@ -109,12 +112,17 @@ export class WalletComponent implements OnInit, OnDestroy {
addressStrings: string[] = [];
walletName: string;
isLoadingWallet = true;
isLoadingTransactions = true;
transactions: Transaction[];
totalTransactionCount: number;
retryLoadMore = false;
wallet$: Observable<Record<string, WalletAddress>>;
walletAddresses$: Observable<Record<string, Address>>;
walletSummary$: Observable<AddressTxSummary[]>;
walletStats$: Observable<WalletStats>;
error: any;
walletSubscription: Subscription;
transactionSubscription: Subscription;
collapseAddresses: boolean = true;
@ -129,6 +137,8 @@ export class WalletComponent implements OnInit, OnDestroy {
private websocketService: WebsocketService,
private stateService: StateService,
private apiService: ApiService,
private electrsApiService: ElectrsApiService,
private audioService: AudioService,
private seoService: SeoService,
) { }
@ -172,6 +182,21 @@ export class WalletComponent implements OnInit, OnDestroy {
}),
switchMap(initial => this.stateService.walletTransactions$.pipe(
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) => {
for (const tx of (walletTransactions || [])) {
const funded: Record<string, number> = {};
@ -267,8 +292,57 @@ export class WalletComponent implements OnInit, OnDestroy {
return stats;
}, 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[] {
@ -299,5 +373,6 @@ export class WalletComponent implements OnInit, OnDestroy {
ngOnDestroy(): void {
this.websocketService.stopTrackingWallet();
this.walletSubscription.unsubscribe();
this.transactionSubscription.unsubscribe();
}
}

View File

@ -1,4 +1,4 @@
import { AddressTxSummary, Block, ChainStats, Transaction } from "./electrs.interface";
import { AddressTxSummary, Block, ChainStats } from "./electrs.interface";
export interface OptimizedMempoolStats {
added: number;

View File

@ -142,12 +142,16 @@ export class ElectrsApiService {
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();
if (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[]> {
@ -163,7 +167,7 @@ export class ElectrsApiService {
if (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[]> {
@ -182,7 +186,7 @@ export class ElectrsApiService {
params = params.append('after_txid', txid);
}
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);
}
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 })),
);
}