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="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> </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> | ||||
|  | ||||
| @ -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"> | ||||
|  | ||||
| @ -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(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| import { AddressTxSummary, Block, ChainStats, Transaction } from "./electrs.interface"; | ||||
| import { AddressTxSummary, Block, ChainStats } from "./electrs.interface"; | ||||
| 
 | ||||
| export interface OptimizedMempoolStats { | ||||
|   added: number; | ||||
|  | ||||
| @ -147,7 +147,11 @@ export class ElectrsApiService { | ||||
|     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 })), | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user