new page listing recent RBF events
This commit is contained in:
		
							parent
							
								
									7b2a1cfd10
								
							
						
					
					
						commit
						f46296a2bb
					
				@ -34,6 +34,8 @@ class BitcoinRoutes {
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'validate-address/:address', this.validateAddress)
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/rbf', this.getRbfHistory)
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/cached', this.getCachedTx)
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'replacements', this.getRbfReplacements)
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'fullrbf/replacements', this.getFullRbfReplacements)
 | 
			
		||||
      .post(config.MEMPOOL.API_URL_PREFIX + 'tx/push', this.$postTransactionForm)
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'donations', async (req, res) => {
 | 
			
		||||
        try {
 | 
			
		||||
@ -653,6 +655,24 @@ class BitcoinRoutes {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async getRbfReplacements(req: Request, res: Response) {
 | 
			
		||||
    try {
 | 
			
		||||
      const result = rbfCache.getRbfChains(false);
 | 
			
		||||
      res.json(result);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      res.status(500).send(e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async getFullRbfReplacements(req: Request, res: Response) {
 | 
			
		||||
    try {
 | 
			
		||||
      const result = rbfCache.getRbfChains(true);
 | 
			
		||||
      res.json(result);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      res.status(500).send(e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async getCachedTx(req: Request, res: Response) {
 | 
			
		||||
    try {
 | 
			
		||||
      const result = rbfCache.getTx(req.params.txId);
 | 
			
		||||
 | 
			
		||||
@ -73,6 +73,33 @@ class RbfCache {
 | 
			
		||||
    return this.rbfChains.get(this.chainMap.get(txId) || '') || [];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // get a paginated list of RbfChains
 | 
			
		||||
  // ordered by most recent replacement time
 | 
			
		||||
  public getRbfChains(onlyFullRbf: boolean, after?: string): RbfChain[] {
 | 
			
		||||
    const limit = 25;
 | 
			
		||||
    const chains: RbfChain[] = [];
 | 
			
		||||
    const used = new Set<string>();
 | 
			
		||||
    const replacements: string[][] = Array.from(this.replacedBy).reverse();
 | 
			
		||||
    const afterChain = after ? this.chainMap.get(after) : null;
 | 
			
		||||
    let ready = !afterChain;
 | 
			
		||||
    for (let i = 0; i < replacements.length && chains.length <= limit - 1; i++) {
 | 
			
		||||
      const txid = replacements[i][1];
 | 
			
		||||
      const chainRoot = this.chainMap.get(txid) || '';
 | 
			
		||||
      if (chainRoot === afterChain) {
 | 
			
		||||
        ready = true;
 | 
			
		||||
      } else if (ready) {
 | 
			
		||||
        if (!used.has(chainRoot)) {
 | 
			
		||||
          const chain = this.rbfChains.get(chainRoot);
 | 
			
		||||
          used.add(chainRoot);
 | 
			
		||||
          if (chain && (!onlyFullRbf || chain.slice(0, -1).some(entry => !entry.tx.rbf))) {
 | 
			
		||||
            chains.push(chain);
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return chains;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // get map of rbf chains that have been updated since the last call
 | 
			
		||||
  public getRbfChanges(): { chains: {[root: string]: RbfChain }, map: { [txid: string]: string }} {
 | 
			
		||||
    const changes: { chains: {[root: string]: RbfChain }, map: { [txid: string]: string }} = {
 | 
			
		||||
@ -92,6 +119,20 @@ class RbfCache {
 | 
			
		||||
    return changes;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public mined(txid): void {
 | 
			
		||||
    const chainRoot = this.chainMap.get(txid)
 | 
			
		||||
    if (chainRoot && this.rbfChains.has(chainRoot)) {
 | 
			
		||||
      const chain = this.rbfChains.get(chainRoot);
 | 
			
		||||
      if (chain) {
 | 
			
		||||
        const chainEntry = chain.find(entry => entry.tx.txid === txid);
 | 
			
		||||
        if (chainEntry) {
 | 
			
		||||
          chainEntry.mined = true;
 | 
			
		||||
        }
 | 
			
		||||
        this.dirtyChains.add(chainRoot);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    this.evict(txid);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // flag a transaction as removed from the mempool
 | 
			
		||||
  public evict(txid): void {
 | 
			
		||||
 | 
			
		||||
@ -140,6 +140,14 @@ class WebsocketHandler {
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          if (parsedMessage && parsedMessage['track-rbf'] !== undefined) {
 | 
			
		||||
            if (['all', 'fullRbf'].includes(parsedMessage['track-rbf'])) {
 | 
			
		||||
              client['track-rbf'] = parsedMessage['track-rbf'];
 | 
			
		||||
            } else {
 | 
			
		||||
              client['track-rbf'] = false;
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          if (parsedMessage.action === 'init') {
 | 
			
		||||
            const _blocks = blocks.getBlocks().slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT);
 | 
			
		||||
            if (!_blocks) {
 | 
			
		||||
@ -279,6 +287,12 @@ class WebsocketHandler {
 | 
			
		||||
    const da = difficultyAdjustment.getDifficultyAdjustment();
 | 
			
		||||
    memPool.handleRbfTransactions(rbfTransactions);
 | 
			
		||||
    const rbfChanges = rbfCache.getRbfChanges();
 | 
			
		||||
    let rbfReplacements;
 | 
			
		||||
    let fullRbfReplacements;
 | 
			
		||||
    if (Object.keys(rbfChanges.chains).length) {
 | 
			
		||||
      rbfReplacements = rbfCache.getRbfChains(false);
 | 
			
		||||
      fullRbfReplacements = rbfCache.getRbfChains(true);
 | 
			
		||||
    }
 | 
			
		||||
    const recommendedFees = feeApi.getRecommendedFee();
 | 
			
		||||
 | 
			
		||||
    this.wss.clients.forEach(async (client) => {
 | 
			
		||||
@ -428,6 +442,13 @@ class WebsocketHandler {
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      console.log(client['track-rbf']);
 | 
			
		||||
      if (client['track-rbf'] === 'all' && rbfReplacements) {
 | 
			
		||||
        response['rbfLatest'] = rbfReplacements;
 | 
			
		||||
      } else if (client['track-rbf'] === 'fullRbf' && fullRbfReplacements) {
 | 
			
		||||
        response['rbfLatest'] = fullRbfReplacements;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (Object.keys(response).length) {
 | 
			
		||||
        client.send(JSON.stringify(response));
 | 
			
		||||
      }
 | 
			
		||||
@ -506,7 +527,7 @@ class WebsocketHandler {
 | 
			
		||||
    // Update mempool to remove transactions included in the new block
 | 
			
		||||
    for (const txId of txIds) {
 | 
			
		||||
      delete _memPool[txId];
 | 
			
		||||
      rbfCache.evict(txId);
 | 
			
		||||
      rbfCache.mined(txId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (config.MEMPOOL.ADVANCED_GBT_MEMPOOL) {
 | 
			
		||||
 | 
			
		||||
@ -14,6 +14,7 @@ import { TrademarkPolicyComponent } from './components/trademark-policy/trademar
 | 
			
		||||
import { BisqMasterPageComponent } from './components/bisq-master-page/bisq-master-page.component';
 | 
			
		||||
import { PushTransactionComponent } from './components/push-transaction/push-transaction.component';
 | 
			
		||||
import { BlocksList } from './components/blocks-list/blocks-list.component';
 | 
			
		||||
import { RbfList } from './components/rbf-list/rbf-list.component';
 | 
			
		||||
import { LiquidMasterPageComponent } from './components/liquid-master-page/liquid-master-page.component';
 | 
			
		||||
import { AssetGroupComponent } from './components/assets/asset-group/asset-group.component';
 | 
			
		||||
import { AssetsFeaturedComponent } from './components/assets/assets-featured/assets-featured.component';
 | 
			
		||||
@ -56,6 +57,10 @@ let routes: Routes = [
 | 
			
		||||
            path: 'blocks',
 | 
			
		||||
            component: BlocksList,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'rbf',
 | 
			
		||||
            component: RbfList,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'terms-of-service',
 | 
			
		||||
            component: TermsOfServiceComponent
 | 
			
		||||
@ -162,6 +167,10 @@ let routes: Routes = [
 | 
			
		||||
            path: 'blocks',
 | 
			
		||||
            component: BlocksList,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'rbf',
 | 
			
		||||
            component: RbfList,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'terms-of-service',
 | 
			
		||||
            component: TermsOfServiceComponent
 | 
			
		||||
@ -264,6 +273,10 @@ let routes: Routes = [
 | 
			
		||||
        path: 'blocks',
 | 
			
		||||
        component: BlocksList,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'rbf',
 | 
			
		||||
        component: RbfList,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'terms-of-service',
 | 
			
		||||
        component: TermsOfServiceComponent
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										61
									
								
								frontend/src/app/components/rbf-list/rbf-list.component.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								frontend/src/app/components/rbf-list/rbf-list.component.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,61 @@
 | 
			
		||||
<div class="container-xl" style="min-height: 335px">
 | 
			
		||||
  <h1 class="float-left" i18n="page.rbf-replacements">RBF Replacements</h1>
 | 
			
		||||
  <div *ngIf="isLoading" class="spinner-border ml-3" role="status"></div>
 | 
			
		||||
 | 
			
		||||
  <div class="mode-toggle float-right" *ngIf="fullRbfEnabled">
 | 
			
		||||
    <form class="formRadioGroup">
 | 
			
		||||
      <div class="btn-group btn-group-toggle" name="radioBasic">
 | 
			
		||||
        <label class="btn btn-primary btn-sm" [class.active]="!fullRbf">
 | 
			
		||||
          <input type="radio" [value]="'All'" fragment="" [routerLink]="[]"> All
 | 
			
		||||
        </label>
 | 
			
		||||
        <label class="btn btn-primary btn-sm" [class.active]="fullRbf">
 | 
			
		||||
          <input type="radio" [value]="'Full RBF'" fragment="fullrbf" [routerLink]="[]"> Full RBF
 | 
			
		||||
        </label>
 | 
			
		||||
      </div>
 | 
			
		||||
    </form>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <div class="clearfix"></div>
 | 
			
		||||
 | 
			
		||||
  <div class="rbf-chains" style="min-height: 295px">
 | 
			
		||||
    <ng-container *ngIf="rbfChains$ | async as chains">
 | 
			
		||||
      <div *ngFor="let chain of chains" class="chain">
 | 
			
		||||
        <p class="info">
 | 
			
		||||
          <app-time kind="since" [time]="chain[chain.length - 1].time"></app-time>
 | 
			
		||||
          <span class="type">
 | 
			
		||||
            <span *ngIf="isMined(chain)" class="badge badge-success" i18n="transaction.rbf.mined">Mined</span>
 | 
			
		||||
            <span *ngIf="isFullRbf(chain)" class="badge badge-info" i18n="transaction.full-rbf">Full RBF</span>
 | 
			
		||||
          </span>
 | 
			
		||||
        </p>
 | 
			
		||||
        <div class="txids">
 | 
			
		||||
          <span class="txid">
 | 
			
		||||
            <a class="rbf-link" [routerLink]="['/tx/' | relativeUrl, chain[0].tx.txid]">
 | 
			
		||||
              <span class="d-inline">{{ chain[0].tx.txid | shortenString : 24 }}</span>
 | 
			
		||||
            </a>
 | 
			
		||||
          </span>
 | 
			
		||||
          <span class="arrow">
 | 
			
		||||
            <fa-icon [icon]="['fas', 'arrow-right']" [fixedWidth]="true"></fa-icon>
 | 
			
		||||
          </span>
 | 
			
		||||
          <span class="txid right">
 | 
			
		||||
            <a class="rbf-link" [routerLink]="['/tx/' | relativeUrl, chain[chain.length - 1].tx.txid]">
 | 
			
		||||
              <span class="d-inline">{{ chain[chain.length - 1].tx.txid | shortenString : 24 }}</span>
 | 
			
		||||
            </a>
 | 
			
		||||
          </span>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="timeline-wrapper" [class.mined]="isMined(chain)">
 | 
			
		||||
          <app-rbf-timeline [replacements]="chain"></app-rbf-timeline>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div class="no-replacements" *ngIf="!chains?.length">
 | 
			
		||||
        <p i18n="rbf.no-replacements-yet">there are no replacements in the mempool yet!</p>
 | 
			
		||||
      </div>
 | 
			
		||||
    </ng-container>
 | 
			
		||||
    
 | 
			
		||||
    <!-- <ngb-pagination class="pagination-container float-right mt-2" [class]="isLoading ? 'disabled' : ''"
 | 
			
		||||
      [collectionSize]="blocksCount" [rotate]="true" [maxSize]="maxSize" [pageSize]="15" [(page)]="page"
 | 
			
		||||
      (pageChange)="pageChange(page)" [boundaryLinks]="true" [ellipses]="false">
 | 
			
		||||
    </ngb-pagination> -->
 | 
			
		||||
  </div>
 | 
			
		||||
  
 | 
			
		||||
</div>
 | 
			
		||||
							
								
								
									
										51
									
								
								frontend/src/app/components/rbf-list/rbf-list.component.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								frontend/src/app/components/rbf-list/rbf-list.component.scss
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,51 @@
 | 
			
		||||
.spinner-border {
 | 
			
		||||
  height: 25px;
 | 
			
		||||
  width: 25px;
 | 
			
		||||
  margin-top: 13px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.rbf-chains {
 | 
			
		||||
  .info {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: row;
 | 
			
		||||
    justify-content: space-between;
 | 
			
		||||
    align-items: baseline;
 | 
			
		||||
    margin: 0;
 | 
			
		||||
 | 
			
		||||
    .type {
 | 
			
		||||
      .badge {
 | 
			
		||||
        margin-left: .5em;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .chain {
 | 
			
		||||
    margin-bottom: 1em;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .txids {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: row;
 | 
			
		||||
    align-items: baseline;
 | 
			
		||||
    justify-content: space-between;
 | 
			
		||||
    margin-bottom: 2px;
 | 
			
		||||
 | 
			
		||||
    .txid {
 | 
			
		||||
      flex-basis: 0;
 | 
			
		||||
      flex-grow: 1;
 | 
			
		||||
 | 
			
		||||
      &.right {
 | 
			
		||||
        text-align: right;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .timeline-wrapper.mined {
 | 
			
		||||
    border: solid 4px #1a9436;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .no-replacements {
 | 
			
		||||
    margin: 1em;
 | 
			
		||||
    text-align: center;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										86
									
								
								frontend/src/app/components/rbf-list/rbf-list.component.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								frontend/src/app/components/rbf-list/rbf-list.component.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,86 @@
 | 
			
		||||
import { Component, OnInit, ChangeDetectionStrategy, OnDestroy } from '@angular/core';
 | 
			
		||||
import { ActivatedRoute, Router } from '@angular/router';
 | 
			
		||||
import { BehaviorSubject, EMPTY, merge, Observable, Subscription } from 'rxjs';
 | 
			
		||||
import { catchError, switchMap, tap } from 'rxjs/operators';
 | 
			
		||||
import { WebsocketService } from 'src/app/services/websocket.service';
 | 
			
		||||
import { RbfInfo } from '../../interfaces/node-api.interface';
 | 
			
		||||
import { ApiService } from '../../services/api.service';
 | 
			
		||||
import { StateService } from '../../services/state.service';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-rbf-list',
 | 
			
		||||
  templateUrl: './rbf-list.component.html',
 | 
			
		||||
  styleUrls: ['./rbf-list.component.scss'],
 | 
			
		||||
  changeDetection: ChangeDetectionStrategy.OnPush,
 | 
			
		||||
})
 | 
			
		||||
export class RbfList implements OnInit, OnDestroy {
 | 
			
		||||
  rbfChains$: Observable<RbfInfo[][]>;
 | 
			
		||||
  fromChainSubject = new BehaviorSubject(null);
 | 
			
		||||
  urlFragmentSubscription: Subscription;
 | 
			
		||||
  fullRbfEnabled: boolean;
 | 
			
		||||
  fullRbf: boolean;
 | 
			
		||||
  isLoading = true;
 | 
			
		||||
  firstChainId: string;
 | 
			
		||||
  lastChainId: string;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private route: ActivatedRoute,
 | 
			
		||||
    private router: Router,
 | 
			
		||||
    private apiService: ApiService,
 | 
			
		||||
    public stateService: StateService,
 | 
			
		||||
    private websocketService: WebsocketService,
 | 
			
		||||
  ) {
 | 
			
		||||
    this.fullRbfEnabled = stateService.env.FULL_RBF_ENABLED;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    this.urlFragmentSubscription = this.route.fragment.subscribe((fragment) => {
 | 
			
		||||
      this.fullRbf = (fragment === 'fullrbf');
 | 
			
		||||
      this.websocketService.startTrackRbf(this.fullRbf ? 'fullRbf' : 'all');
 | 
			
		||||
      this.fromChainSubject.next(this.firstChainId);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.rbfChains$ = merge(
 | 
			
		||||
      this.fromChainSubject.pipe(
 | 
			
		||||
        switchMap((fromChainId) => {
 | 
			
		||||
          return this.apiService.getRbfList$(this.fullRbf, fromChainId || undefined)
 | 
			
		||||
        }),
 | 
			
		||||
        catchError((e) => {
 | 
			
		||||
          return EMPTY;
 | 
			
		||||
        })
 | 
			
		||||
      ),
 | 
			
		||||
      this.stateService.rbfLatest$
 | 
			
		||||
    )
 | 
			
		||||
    .pipe(
 | 
			
		||||
      tap((result: RbfInfo[][]) => {
 | 
			
		||||
        this.isLoading = false;
 | 
			
		||||
        if (result && result.length && result[0].length) {
 | 
			
		||||
          this.lastChainId = result[result.length - 1][0].tx.txid;
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  toggleFullRbf(event) {
 | 
			
		||||
    this.router.navigate([], {
 | 
			
		||||
      relativeTo: this.route,
 | 
			
		||||
      fragment: this.fullRbf ? null : 'fullrbf'
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  isFullRbf(chain: RbfInfo[]): boolean {
 | 
			
		||||
    return chain.slice(0, -1).some(entry => !entry.tx.rbf);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  isMined(chain: RbfInfo[]): boolean {
 | 
			
		||||
    return chain.some(entry => entry.mined);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // pageChange(page: number) {
 | 
			
		||||
  //   this.fromChainSubject.next(this.lastChainId);
 | 
			
		||||
  // }
 | 
			
		||||
 | 
			
		||||
  ngOnDestroy(): void {
 | 
			
		||||
    this.websocketService.stopTrackRbf();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
<div class="rbf-timeline box">
 | 
			
		||||
<div class="rbf-timeline box" [class.mined]="mined">
 | 
			
		||||
  <div class="timeline">
 | 
			
		||||
    <div class="intervals">
 | 
			
		||||
      <ng-container *ngFor="let replacement of replacements; let i = index;">
 | 
			
		||||
@ -15,7 +15,7 @@
 | 
			
		||||
        <div class="interval-spacer" *ngIf="i > 0">
 | 
			
		||||
          <div class="track"></div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="node" [class.selected]="txid === replacement.tx.txid">
 | 
			
		||||
        <div class="node" [class.selected]="txid === replacement.tx.txid" [class.mined]="replacement.mined">
 | 
			
		||||
          <div class="track"></div>
 | 
			
		||||
          <a class="shape-border" [class.rbf]="replacement.tx.rbf" [routerLink]="['/tx/' | relativeUrl, replacement.tx.txid]" [title]="replacement.tx.txid">
 | 
			
		||||
            <div class="shape"></div>
 | 
			
		||||
 | 
			
		||||
@ -126,6 +126,12 @@
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      &.mined {
 | 
			
		||||
        .shape-border {
 | 
			
		||||
          background: #1a9436;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .shape-border:hover {
 | 
			
		||||
        padding: 0px;
 | 
			
		||||
        .shape {
 | 
			
		||||
 | 
			
		||||
@ -12,6 +12,7 @@ import { ApiService } from '../../services/api.service';
 | 
			
		||||
export class RbfTimelineComponent implements OnInit, OnChanges {
 | 
			
		||||
  @Input() replacements: RbfInfo[];
 | 
			
		||||
  @Input() txid: string;
 | 
			
		||||
  mined: boolean;
 | 
			
		||||
 | 
			
		||||
  dir: 'rtl' | 'ltr' = 'ltr';
 | 
			
		||||
 | 
			
		||||
@ -27,10 +28,10 @@ export class RbfTimelineComponent implements OnInit, OnChanges {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    
 | 
			
		||||
    this.mined = this.replacements.some(entry => entry.mined);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnChanges(): void {
 | 
			
		||||
    
 | 
			
		||||
    this.mined = this.replacements.some(entry => entry.mined);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -28,7 +28,8 @@ export interface CpfpInfo {
 | 
			
		||||
 | 
			
		||||
export interface RbfInfo {
 | 
			
		||||
  tx: RbfTransaction,
 | 
			
		||||
  time: number
 | 
			
		||||
  time: number,
 | 
			
		||||
  mined?: boolean,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface DifficultyAdjustment {
 | 
			
		||||
 | 
			
		||||
@ -17,6 +17,7 @@ export interface WebsocketResponse {
 | 
			
		||||
  rbfTransaction?: ReplacedTransaction;
 | 
			
		||||
  txReplaced?: ReplacedTransaction;
 | 
			
		||||
  rbfInfo?: RbfInfo[];
 | 
			
		||||
  rbfLatest?: RbfInfo[][];
 | 
			
		||||
  utxoSpent?: object;
 | 
			
		||||
  transactions?: TransactionStripped[];
 | 
			
		||||
  loadingIndicators?: ILoadingIndicators;
 | 
			
		||||
@ -27,6 +28,7 @@ export interface WebsocketResponse {
 | 
			
		||||
  'track-address'?: string;
 | 
			
		||||
  'track-asset'?: string;
 | 
			
		||||
  'track-mempool-block'?: number;
 | 
			
		||||
  'track-rbf'?: string;
 | 
			
		||||
  'watch-mempool'?: boolean;
 | 
			
		||||
  'track-bisq-market'?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -132,6 +132,10 @@ export class ApiService {
 | 
			
		||||
    return this.httpClient.get<Transaction>(this.apiBaseUrl + this.apiBasePath + '/api/v1/tx/' + txid + '/cached');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getRbfList$(fullRbf: boolean, after?: string): Observable<RbfInfo[][]> {
 | 
			
		||||
    return this.httpClient.get<RbfInfo[][]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/' + (fullRbf ? 'fullrbf/' : '') + 'replacements/' + (after || ''));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  listLiquidPegsMonth$(): Observable<LiquidPegs[]> {
 | 
			
		||||
    return this.httpClient.get<LiquidPegs[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/pegs/month');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -99,6 +99,7 @@ export class StateService {
 | 
			
		||||
  mempoolBlockDelta$ = new Subject<MempoolBlockDelta>();
 | 
			
		||||
  txReplaced$ = new Subject<ReplacedTransaction>();
 | 
			
		||||
  txRbfInfo$ = new Subject<RbfInfo[]>();
 | 
			
		||||
  rbfLatest$ = new Subject<RbfInfo[][]>();
 | 
			
		||||
  utxoSpent$ = new Subject<object>();
 | 
			
		||||
  difficultyAdjustment$ = new ReplaySubject<DifficultyAdjustment>(1);
 | 
			
		||||
  mempoolTransactions$ = new Subject<Transaction>();
 | 
			
		||||
 | 
			
		||||
@ -28,6 +28,7 @@ export class WebsocketService {
 | 
			
		||||
  private isTrackingTx = false;
 | 
			
		||||
  private trackingTxId: string;
 | 
			
		||||
  private isTrackingMempoolBlock = false;
 | 
			
		||||
  private isTrackingRbf = false;
 | 
			
		||||
  private trackingMempoolBlock: number;
 | 
			
		||||
  private latestGitCommit = '';
 | 
			
		||||
  private onlineCheckTimeout: number;
 | 
			
		||||
@ -173,6 +174,16 @@ export class WebsocketService {
 | 
			
		||||
    this.isTrackingMempoolBlock = false
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  startTrackRbf(mode: 'all' | 'fullRbf') {
 | 
			
		||||
    this.websocketSubject.next({ 'track-rbf': mode });
 | 
			
		||||
    this.isTrackingRbf = true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  stopTrackRbf() {
 | 
			
		||||
    this.websocketSubject.next({ 'track-rbf': 'stop' });
 | 
			
		||||
    this.isTrackingRbf = false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  startTrackBisqMarket(market: string) {
 | 
			
		||||
    this.websocketSubject.next({ 'track-bisq-market': market });
 | 
			
		||||
  }
 | 
			
		||||
@ -261,6 +272,10 @@ export class WebsocketService {
 | 
			
		||||
      this.stateService.txRbfInfo$.next(response.rbfInfo);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (response.rbfLatest) {
 | 
			
		||||
      this.stateService.rbfLatest$.next(response.rbfLatest);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (response.txReplaced) {
 | 
			
		||||
      this.stateService.txReplaced$.next(response.txReplaced);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -4,7 +4,7 @@ import { NgbCollapseModule, NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstra
 | 
			
		||||
import { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontawesome';
 | 
			
		||||
import { faFilter, faAngleDown, faAngleUp, faAngleRight, faAngleLeft, faBolt, faChartArea, faCogs, faCubes, faHammer, faDatabase, faExchangeAlt, faInfoCircle,
 | 
			
		||||
  faLink, faList, faSearch, faCaretUp, faCaretDown, faTachometerAlt, faThList, faTint, faTv, faAngleDoubleDown, faSortUp, faAngleDoubleUp, faChevronDown,
 | 
			
		||||
  faFileAlt, faRedoAlt, faArrowAltCircleRight, faExternalLinkAlt, faBook, faListUl, faDownload, faQrcode, faArrowRightArrowLeft, faArrowsRotate, faCircleLeft } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
  faFileAlt, faRedoAlt, faArrowAltCircleRight, faExternalLinkAlt, faBook, faListUl, faDownload, faQrcode, faArrowRightArrowLeft, faArrowRight, faArrowsRotate, faCircleLeft } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
 | 
			
		||||
import { MasterPageComponent } from '../components/master-page/master-page.component';
 | 
			
		||||
import { PreviewTitleComponent } from '../components/master-page-preview/preview-title.component';
 | 
			
		||||
@ -73,6 +73,7 @@ import { AssetCirculationComponent } from '../components/asset-circulation/asset
 | 
			
		||||
import { AmountShortenerPipe } from '../shared/pipes/amount-shortener.pipe';
 | 
			
		||||
import { DifficultyAdjustmentsTable } from '../components/difficulty-adjustments-table/difficulty-adjustments-table.components';
 | 
			
		||||
import { BlocksList } from '../components/blocks-list/blocks-list.component';
 | 
			
		||||
import { RbfList } from '../components/rbf-list/rbf-list.component';
 | 
			
		||||
import { RewardStatsComponent } from '../components/reward-stats/reward-stats.component';
 | 
			
		||||
import { DataCyDirective } from '../data-cy.directive';
 | 
			
		||||
import { LoadingIndicatorComponent } from '../components/loading-indicator/loading-indicator.component';
 | 
			
		||||
@ -153,6 +154,7 @@ import { TestnetAlertComponent } from './components/testnet-alert/testnet-alert.
 | 
			
		||||
    AmountShortenerPipe,
 | 
			
		||||
    DifficultyAdjustmentsTable,
 | 
			
		||||
    BlocksList,
 | 
			
		||||
    RbfList,
 | 
			
		||||
    DataCyDirective,
 | 
			
		||||
    RewardStatsComponent,
 | 
			
		||||
    LoadingIndicatorComponent,
 | 
			
		||||
@ -313,6 +315,7 @@ export class SharedModule {
 | 
			
		||||
    library.addIcons(faDownload);
 | 
			
		||||
    library.addIcons(faQrcode);
 | 
			
		||||
    library.addIcons(faArrowRightArrowLeft);
 | 
			
		||||
    library.addIcons(faArrowRight);
 | 
			
		||||
    library.addIcons(faExchangeAlt);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user