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