Merge pull request #1305 from mempool/simon/track-utxos
UTXO spent tracking
This commit is contained in:
		
						commit
						8d4bc201ff
					
				| @ -331,21 +331,30 @@ class WebsocketHandler { | |||||||
|         } |         } | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       if (client['track-tx'] && rbfTransactions[client['track-tx']]) { |       if (client['track-tx']) { | ||||||
|         for (const rbfTransaction in rbfTransactions) { |         const utxoSpent = newTransactions.some((tx) => { | ||||||
|           if (client['track-tx'] === rbfTransaction) { |           return tx.vin.some((vin) => vin.txid === client['track-tx']); | ||||||
|             const rbfTx = rbfTransactions[rbfTransaction]; |         }); | ||||||
|             if (config.MEMPOOL.BACKEND !== 'esplora') { |         if (utxoSpent) { | ||||||
|               try { |           response['utxoSpent'] = true; | ||||||
|                 const fullTx = await transactionUtils.$getTransactionExtended(rbfTransaction, true); |         } | ||||||
|                 response['rbfTransaction'] = fullTx; | 
 | ||||||
|               } catch (e) { |         if (rbfTransactions[client['track-tx']]) { | ||||||
|                 logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e)); |           for (const rbfTransaction in rbfTransactions) { | ||||||
|  |             if (client['track-tx'] === rbfTransaction) { | ||||||
|  |               const rbfTx = rbfTransactions[rbfTransaction]; | ||||||
|  |               if (config.MEMPOOL.BACKEND !== 'esplora') { | ||||||
|  |                 try { | ||||||
|  |                   const fullTx = await transactionUtils.$getTransactionExtended(rbfTransaction, true); | ||||||
|  |                   response['rbfTransaction'] = fullTx; | ||||||
|  |                 } catch (e) { | ||||||
|  |                   logger.debug('Error finding transaction in mempool: ' + (e instanceof Error ? e.message : e)); | ||||||
|  |                 } | ||||||
|  |               } else { | ||||||
|  |                 response['rbfTransaction'] = rbfTx; | ||||||
|               } |               } | ||||||
|             } else { |               break; | ||||||
|               response['rbfTransaction'] = rbfTx; |  | ||||||
|             } |             } | ||||||
|             break; |  | ||||||
|           } |           } | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|  | |||||||
| @ -185,15 +185,12 @@ export class TransactionComponent implements OnInit, OnDestroy { | |||||||
|           this.error = undefined; |           this.error = undefined; | ||||||
|           this.waitingForTransaction = false; |           this.waitingForTransaction = false; | ||||||
|           this.setMempoolBlocksSubscription(); |           this.setMempoolBlocksSubscription(); | ||||||
|  |           this.websocketService.startTrackTransaction(tx.txid); | ||||||
| 
 | 
 | ||||||
|           if (!tx.status.confirmed) { |           if (!tx.status.confirmed && tx.firstSeen) { | ||||||
|             this.websocketService.startTrackTransaction(tx.txid); |             this.transactionTime = tx.firstSeen; | ||||||
| 
 |           } else { | ||||||
|             if (tx.firstSeen) { |             this.getTransactionTime(); | ||||||
|               this.transactionTime = tx.firstSeen; |  | ||||||
|             } else { |  | ||||||
|               this.getTransactionTime(); |  | ||||||
|             } |  | ||||||
|           } |           } | ||||||
| 
 | 
 | ||||||
|           if (this.tx.status.confirmed) { |           if (this.tx.status.confirmed) { | ||||||
|  | |||||||
| @ -82,7 +82,7 @@ | |||||||
|                   </ng-template> |                   </ng-template> | ||||||
|                 </td> |                 </td> | ||||||
|               </tr> |               </tr> | ||||||
|               <tr *ngIf="displayDetails"> |               <tr *ngIf="(showDetails$ | async) === true"> | ||||||
|                 <td colspan="3" class="details-container" > |                 <td colspan="3" class="details-container" > | ||||||
|                   <table class="table table-striped table-borderless details-table mb-3"> |                   <table class="table table-striped table-borderless details-table mb-3"> | ||||||
|                     <tbody> |                     <tbody> | ||||||
| @ -183,16 +183,16 @@ | |||||||
|                     <app-amount [satoshis]="vout.value"></app-amount> |                     <app-amount [satoshis]="vout.value"></app-amount> | ||||||
|                   </ng-template> |                   </ng-template> | ||||||
|                 </td> |                 </td> | ||||||
|                 <td class="arrow-td"> |                 <td class="arrow-td" *ngIf="{ value: (outspends$ | async) } as outspends"> | ||||||
|                   <span *ngIf="!outspends[i] || vout.scriptpubkey_type === 'op_return' || vout.scriptpubkey_type === 'fee' ; else outspend" class="grey"> |                   <span *ngIf="!outspends.value || !outspends.value[i] || vout.scriptpubkey_type === 'op_return' || vout.scriptpubkey_type === 'fee' ; else outspend" class="grey"> | ||||||
|                     <fa-icon [icon]="['fas', 'arrow-alt-circle-right']" [fixedWidth]="true"></fa-icon> |                     <fa-icon [icon]="['fas', 'arrow-alt-circle-right']" [fixedWidth]="true"></fa-icon> | ||||||
|                   </span> |                   </span> | ||||||
|                   <ng-template #outspend> |                   <ng-template #outspend> | ||||||
|                     <span *ngIf="!outspends[i][vindex] || !outspends[i][vindex].spent; else spent" class="green"> |                     <span *ngIf="!outspends.value[i][vindex] || !outspends.value[i][vindex].spent; else spent" class="green"> | ||||||
|                       <fa-icon [icon]="['fas', 'arrow-alt-circle-right']" [fixedWidth]="true"></fa-icon> |                       <fa-icon [icon]="['fas', 'arrow-alt-circle-right']" [fixedWidth]="true"></fa-icon> | ||||||
|                     </span> |                     </span> | ||||||
|                     <ng-template #spent> |                     <ng-template #spent> | ||||||
|                       <a *ngIf="outspends[i][vindex].txid else outputNoTxId" [routerLink]="['/tx/' | relativeUrl, outspends[i][vindex].txid]" class="red"> |                       <a *ngIf="outspends.value[i][vindex].txid else outputNoTxId" [routerLink]="['/tx/' | relativeUrl, outspends.value[i][vindex].txid]" class="red"> | ||||||
|                         <fa-icon [icon]="['fas', 'arrow-alt-circle-right']" [fixedWidth]="true"></fa-icon> |                         <fa-icon [icon]="['fas', 'arrow-alt-circle-right']" [fixedWidth]="true"></fa-icon> | ||||||
|                       </a> |                       </a> | ||||||
|                       <ng-template #outputNoTxId> |                       <ng-template #outputNoTxId> | ||||||
| @ -204,7 +204,7 @@ | |||||||
|                   </ng-template> |                   </ng-template> | ||||||
|                 </td> |                 </td> | ||||||
|               </tr> |               </tr> | ||||||
|               <tr *ngIf="displayDetails"> |               <tr *ngIf="(showDetails$ | async) === true"> | ||||||
|                 <td colspan="3" class=" details-container" > |                 <td colspan="3" class=" details-container" > | ||||||
|                   <table class="table table-striped table-borderless details-table mb-3"> |                   <table class="table table-striped table-borderless details-table mb-3"> | ||||||
|                     <tbody> |                     <tbody> | ||||||
|  | |||||||
| @ -1,11 +1,11 @@ | |||||||
| import { Component, OnInit, Input, ChangeDetectionStrategy, OnChanges, ChangeDetectorRef, Output, EventEmitter } from '@angular/core'; | import { Component, OnInit, Input, ChangeDetectionStrategy, OnChanges, Output, EventEmitter } from '@angular/core'; | ||||||
| import { StateService } from '../../services/state.service'; | import { StateService } from '../../services/state.service'; | ||||||
| import { Observable, forkJoin } from 'rxjs'; | import { Observable, forkJoin, ReplaySubject, BehaviorSubject, merge } from 'rxjs'; | ||||||
| import { Outspend, Transaction } from '../../interfaces/electrs.interface'; | import { Outspend, Transaction } from '../../interfaces/electrs.interface'; | ||||||
| import { ElectrsApiService } from '../../services/electrs-api.service'; | import { ElectrsApiService } from '../../services/electrs-api.service'; | ||||||
| import { environment } from 'src/environments/environment'; | import { environment } from 'src/environments/environment'; | ||||||
| import { AssetsService } from 'src/app/services/assets.service'; | import { AssetsService } from 'src/app/services/assets.service'; | ||||||
| import { map } from 'rxjs/operators'; | import { map, share, switchMap } from 'rxjs/operators'; | ||||||
| import { BlockExtended } from 'src/app/interfaces/node-api.interface'; | import { BlockExtended } from 'src/app/interfaces/node-api.interface'; | ||||||
| 
 | 
 | ||||||
| @Component({ | @Component({ | ||||||
| @ -17,7 +17,6 @@ import { BlockExtended } from 'src/app/interfaces/node-api.interface'; | |||||||
| export class TransactionsListComponent implements OnInit, OnChanges { | export class TransactionsListComponent implements OnInit, OnChanges { | ||||||
|   network = ''; |   network = ''; | ||||||
|   nativeAssetId = this.stateService.network === 'liquidtestnet' ? environment.nativeTestAssetId : environment.nativeAssetId; |   nativeAssetId = this.stateService.network === 'liquidtestnet' ? environment.nativeTestAssetId : environment.nativeAssetId; | ||||||
|   displayDetails = false; |  | ||||||
| 
 | 
 | ||||||
|   @Input() transactions: Transaction[]; |   @Input() transactions: Transaction[]; | ||||||
|   @Input() showConfirmations = false; |   @Input() showConfirmations = false; | ||||||
| @ -28,15 +27,41 @@ export class TransactionsListComponent implements OnInit, OnChanges { | |||||||
|   @Output() loadMore = new EventEmitter(); |   @Output() loadMore = new EventEmitter(); | ||||||
| 
 | 
 | ||||||
|   latestBlock$: Observable<BlockExtended>; |   latestBlock$: Observable<BlockExtended>; | ||||||
|   outspends: Outspend[] = []; |   outspends$: Observable<Outspend[]>; | ||||||
|  |   refreshOutspends$: ReplaySubject<object> = new ReplaySubject(); | ||||||
|  |   showDetails$ = new BehaviorSubject<boolean>(false); | ||||||
|  |   _outspends: Outspend[] = []; | ||||||
|   assetsMinimal: any; |   assetsMinimal: any; | ||||||
| 
 | 
 | ||||||
|   constructor( |   constructor( | ||||||
|     public stateService: StateService, |     public stateService: StateService, | ||||||
|     private electrsApiService: ElectrsApiService, |     private electrsApiService: ElectrsApiService, | ||||||
|     private assetsService: AssetsService, |     private assetsService: AssetsService, | ||||||
|     private ref: ChangeDetectorRef, |   ) { | ||||||
|   ) { } |     this.outspends$ = merge( | ||||||
|  |       this.refreshOutspends$, | ||||||
|  |       this.stateService.utxoSpent$ | ||||||
|  |         .pipe( | ||||||
|  |           map(() => { | ||||||
|  |             this._outspends = []; | ||||||
|  |             return { 0: this.electrsApiService.getOutspends$(this.transactions[0].txid) }; | ||||||
|  |           }), | ||||||
|  |         ) | ||||||
|  |     ).pipe( | ||||||
|  |       switchMap((observableObject) => forkJoin(observableObject)), | ||||||
|  |       map((outspends: any) => { | ||||||
|  |         const newOutspends = []; | ||||||
|  |         for (const i in outspends) { | ||||||
|  |           if (outspends.hasOwnProperty(i)) { | ||||||
|  |             newOutspends.push(outspends[i]); | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |         this._outspends = this._outspends.concat(newOutspends); | ||||||
|  |         return this._outspends; | ||||||
|  |       }), | ||||||
|  |       share(), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
| 
 | 
 | ||||||
|   ngOnInit() { |   ngOnInit() { | ||||||
|     this.latestBlock$ = this.stateService.blocks$.pipe(map(([block]) => block)); |     this.latestBlock$ = this.stateService.blocks$.pipe(map(([block]) => block)); | ||||||
| @ -65,23 +90,12 @@ export class TransactionsListComponent implements OnInit, OnChanges { | |||||||
|     this.transactions.forEach((tx, i) => { |     this.transactions.forEach((tx, i) => { | ||||||
|       tx['@voutLimit'] = true; |       tx['@voutLimit'] = true; | ||||||
|       tx['@vinLimit'] = true; |       tx['@vinLimit'] = true; | ||||||
|       if (this.outspends[i]) { |       if (this._outspends[i]) { | ||||||
|         return; |         return; | ||||||
|       } |       } | ||||||
|       observableObject[i] = this.electrsApiService.getOutspends$(tx.txid); |       observableObject[i] = this.electrsApiService.getOutspends$(tx.txid); | ||||||
|     }); |     }); | ||||||
| 
 |     this.refreshOutspends$.next(observableObject); | ||||||
|     forkJoin(observableObject) |  | ||||||
|       .subscribe((outspends: any) => { |  | ||||||
|         const newOutspends = []; |  | ||||||
|         for (const i in outspends) { |  | ||||||
|           if (outspends.hasOwnProperty(i)) { |  | ||||||
|             newOutspends.push(outspends[i]); |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|         this.outspends = this.outspends.concat(newOutspends); |  | ||||||
|         this.ref.markForCheck(); |  | ||||||
|       }); |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   onScroll() { |   onScroll() { | ||||||
| @ -129,7 +143,10 @@ export class TransactionsListComponent implements OnInit, OnChanges { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   toggleDetails() { |   toggleDetails() { | ||||||
|     this.displayDetails = !this.displayDetails; |     if (this.showDetails$.value === true) { | ||||||
|     this.ref.markForCheck(); |       this.showDetails$.next(false); | ||||||
|  |     } else { | ||||||
|  |       this.showDetails$.next(true); | ||||||
|  |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -16,6 +16,7 @@ export interface WebsocketResponse { | |||||||
|   data?: string[]; |   data?: string[]; | ||||||
|   tx?: Transaction; |   tx?: Transaction; | ||||||
|   rbfTransaction?: Transaction; |   rbfTransaction?: Transaction; | ||||||
|  |   utxoSpent?: boolean; | ||||||
|   transactions?: TransactionStripped[]; |   transactions?: TransactionStripped[]; | ||||||
|   loadingIndicators?: ILoadingIndicators; |   loadingIndicators?: ILoadingIndicators; | ||||||
|   backendInfo?: IBackendInfo; |   backendInfo?: IBackendInfo; | ||||||
|  | |||||||
| @ -81,6 +81,7 @@ export class StateService { | |||||||
|   mempoolInfo$ = new ReplaySubject<MempoolInfo>(1); |   mempoolInfo$ = new ReplaySubject<MempoolInfo>(1); | ||||||
|   mempoolBlocks$ = new ReplaySubject<MempoolBlock[]>(1); |   mempoolBlocks$ = new ReplaySubject<MempoolBlock[]>(1); | ||||||
|   txReplaced$ = new Subject<Transaction>(); |   txReplaced$ = new Subject<Transaction>(); | ||||||
|  |   utxoSpent$ = new Subject<null>(); | ||||||
|   mempoolTransactions$ = new Subject<Transaction>(); |   mempoolTransactions$ = new Subject<Transaction>(); | ||||||
|   blockTransactions$ = new Subject<Transaction>(); |   blockTransactions$ = new Subject<Transaction>(); | ||||||
|   isLoadingWebSocket$ = new ReplaySubject<boolean>(1); |   isLoadingWebSocket$ = new ReplaySubject<boolean>(1); | ||||||
|  | |||||||
| @ -251,6 +251,10 @@ export class WebsocketService { | |||||||
|       this.stateService.bsqPrice$.next(response['bsq-price']); |       this.stateService.bsqPrice$.next(response['bsq-price']); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     if (response.utxoSpent) { | ||||||
|  |       this.stateService.utxoSpent$.next(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     if (response.backendInfo) { |     if (response.backendInfo) { | ||||||
|       this.stateService.backendInfo$.next(response.backendInfo); |       this.stateService.backendInfo$.next(response.backendInfo); | ||||||
| 
 | 
 | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user