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 + 'validate-address/:address', this.validateAddress)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/rbf', this.getRbfHistory)
|
.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 + '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)
|
.post(config.MEMPOOL.API_URL_PREFIX + 'tx/push', this.$postTransactionForm)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'donations', async (req, res) => {
|
.get(config.MEMPOOL.API_URL_PREFIX + 'donations', async (req, res) => {
|
||||||
try {
|
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) {
|
private async getCachedTx(req: Request, res: Response) {
|
||||||
try {
|
try {
|
||||||
const result = rbfCache.getTx(req.params.txId);
|
const result = rbfCache.getTx(req.params.txId);
|
||||||
|
@ -73,6 +73,33 @@ class RbfCache {
|
|||||||
return this.rbfChains.get(this.chainMap.get(txId) || '') || [];
|
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
|
// get map of rbf chains that have been updated since the last call
|
||||||
public getRbfChanges(): { chains: {[root: string]: RbfChain }, map: { [txid: string]: string }} {
|
public getRbfChanges(): { chains: {[root: string]: RbfChain }, map: { [txid: string]: string }} {
|
||||||
const changes: { 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;
|
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
|
// flag a transaction as removed from the mempool
|
||||||
public evict(txid): void {
|
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') {
|
if (parsedMessage.action === 'init') {
|
||||||
const _blocks = blocks.getBlocks().slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT);
|
const _blocks = blocks.getBlocks().slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT);
|
||||||
if (!_blocks) {
|
if (!_blocks) {
|
||||||
@ -279,6 +287,12 @@ class WebsocketHandler {
|
|||||||
const da = difficultyAdjustment.getDifficultyAdjustment();
|
const da = difficultyAdjustment.getDifficultyAdjustment();
|
||||||
memPool.handleRbfTransactions(rbfTransactions);
|
memPool.handleRbfTransactions(rbfTransactions);
|
||||||
const rbfChanges = rbfCache.getRbfChanges();
|
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();
|
const recommendedFees = feeApi.getRecommendedFee();
|
||||||
|
|
||||||
this.wss.clients.forEach(async (client) => {
|
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) {
|
if (Object.keys(response).length) {
|
||||||
client.send(JSON.stringify(response));
|
client.send(JSON.stringify(response));
|
||||||
}
|
}
|
||||||
@ -506,7 +527,7 @@ class WebsocketHandler {
|
|||||||
// Update mempool to remove transactions included in the new block
|
// Update mempool to remove transactions included in the new block
|
||||||
for (const txId of txIds) {
|
for (const txId of txIds) {
|
||||||
delete _memPool[txId];
|
delete _memPool[txId];
|
||||||
rbfCache.evict(txId);
|
rbfCache.mined(txId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.MEMPOOL.ADVANCED_GBT_MEMPOOL) {
|
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 { BisqMasterPageComponent } from './components/bisq-master-page/bisq-master-page.component';
|
||||||
import { PushTransactionComponent } from './components/push-transaction/push-transaction.component';
|
import { PushTransactionComponent } from './components/push-transaction/push-transaction.component';
|
||||||
import { BlocksList } from './components/blocks-list/blocks-list.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 { LiquidMasterPageComponent } from './components/liquid-master-page/liquid-master-page.component';
|
||||||
import { AssetGroupComponent } from './components/assets/asset-group/asset-group.component';
|
import { AssetGroupComponent } from './components/assets/asset-group/asset-group.component';
|
||||||
import { AssetsFeaturedComponent } from './components/assets/assets-featured/assets-featured.component';
|
import { AssetsFeaturedComponent } from './components/assets/assets-featured/assets-featured.component';
|
||||||
@ -56,6 +57,10 @@ let routes: Routes = [
|
|||||||
path: 'blocks',
|
path: 'blocks',
|
||||||
component: BlocksList,
|
component: BlocksList,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'rbf',
|
||||||
|
component: RbfList,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'terms-of-service',
|
path: 'terms-of-service',
|
||||||
component: TermsOfServiceComponent
|
component: TermsOfServiceComponent
|
||||||
@ -162,6 +167,10 @@ let routes: Routes = [
|
|||||||
path: 'blocks',
|
path: 'blocks',
|
||||||
component: BlocksList,
|
component: BlocksList,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'rbf',
|
||||||
|
component: RbfList,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'terms-of-service',
|
path: 'terms-of-service',
|
||||||
component: TermsOfServiceComponent
|
component: TermsOfServiceComponent
|
||||||
@ -264,6 +273,10 @@ let routes: Routes = [
|
|||||||
path: 'blocks',
|
path: 'blocks',
|
||||||
component: BlocksList,
|
component: BlocksList,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'rbf',
|
||||||
|
component: RbfList,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'terms-of-service',
|
path: 'terms-of-service',
|
||||||
component: TermsOfServiceComponent
|
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="timeline">
|
||||||
<div class="intervals">
|
<div class="intervals">
|
||||||
<ng-container *ngFor="let replacement of replacements; let i = index;">
|
<ng-container *ngFor="let replacement of replacements; let i = index;">
|
||||||
@ -15,7 +15,7 @@
|
|||||||
<div class="interval-spacer" *ngIf="i > 0">
|
<div class="interval-spacer" *ngIf="i > 0">
|
||||||
<div class="track"></div>
|
<div class="track"></div>
|
||||||
</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>
|
<div class="track"></div>
|
||||||
<a class="shape-border" [class.rbf]="replacement.tx.rbf" [routerLink]="['/tx/' | relativeUrl, replacement.tx.txid]" [title]="replacement.tx.txid">
|
<a class="shape-border" [class.rbf]="replacement.tx.rbf" [routerLink]="['/tx/' | relativeUrl, replacement.tx.txid]" [title]="replacement.tx.txid">
|
||||||
<div class="shape"></div>
|
<div class="shape"></div>
|
||||||
|
@ -126,6 +126,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.mined {
|
||||||
|
.shape-border {
|
||||||
|
background: #1a9436;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.shape-border:hover {
|
.shape-border:hover {
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
.shape {
|
.shape {
|
||||||
|
@ -12,6 +12,7 @@ import { ApiService } from '../../services/api.service';
|
|||||||
export class RbfTimelineComponent implements OnInit, OnChanges {
|
export class RbfTimelineComponent implements OnInit, OnChanges {
|
||||||
@Input() replacements: RbfInfo[];
|
@Input() replacements: RbfInfo[];
|
||||||
@Input() txid: string;
|
@Input() txid: string;
|
||||||
|
mined: boolean;
|
||||||
|
|
||||||
dir: 'rtl' | 'ltr' = 'ltr';
|
dir: 'rtl' | 'ltr' = 'ltr';
|
||||||
|
|
||||||
@ -27,10 +28,10 @@ export class RbfTimelineComponent implements OnInit, OnChanges {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
this.mined = this.replacements.some(entry => entry.mined);
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnChanges(): void {
|
ngOnChanges(): void {
|
||||||
|
this.mined = this.replacements.some(entry => entry.mined);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,8 @@ export interface CpfpInfo {
|
|||||||
|
|
||||||
export interface RbfInfo {
|
export interface RbfInfo {
|
||||||
tx: RbfTransaction,
|
tx: RbfTransaction,
|
||||||
time: number
|
time: number,
|
||||||
|
mined?: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DifficultyAdjustment {
|
export interface DifficultyAdjustment {
|
||||||
|
@ -17,6 +17,7 @@ export interface WebsocketResponse {
|
|||||||
rbfTransaction?: ReplacedTransaction;
|
rbfTransaction?: ReplacedTransaction;
|
||||||
txReplaced?: ReplacedTransaction;
|
txReplaced?: ReplacedTransaction;
|
||||||
rbfInfo?: RbfInfo[];
|
rbfInfo?: RbfInfo[];
|
||||||
|
rbfLatest?: RbfInfo[][];
|
||||||
utxoSpent?: object;
|
utxoSpent?: object;
|
||||||
transactions?: TransactionStripped[];
|
transactions?: TransactionStripped[];
|
||||||
loadingIndicators?: ILoadingIndicators;
|
loadingIndicators?: ILoadingIndicators;
|
||||||
@ -27,6 +28,7 @@ export interface WebsocketResponse {
|
|||||||
'track-address'?: string;
|
'track-address'?: string;
|
||||||
'track-asset'?: string;
|
'track-asset'?: string;
|
||||||
'track-mempool-block'?: number;
|
'track-mempool-block'?: number;
|
||||||
|
'track-rbf'?: string;
|
||||||
'watch-mempool'?: boolean;
|
'watch-mempool'?: boolean;
|
||||||
'track-bisq-market'?: string;
|
'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');
|
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[]> {
|
listLiquidPegsMonth$(): Observable<LiquidPegs[]> {
|
||||||
return this.httpClient.get<LiquidPegs[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/pegs/month');
|
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>();
|
mempoolBlockDelta$ = new Subject<MempoolBlockDelta>();
|
||||||
txReplaced$ = new Subject<ReplacedTransaction>();
|
txReplaced$ = new Subject<ReplacedTransaction>();
|
||||||
txRbfInfo$ = new Subject<RbfInfo[]>();
|
txRbfInfo$ = new Subject<RbfInfo[]>();
|
||||||
|
rbfLatest$ = new Subject<RbfInfo[][]>();
|
||||||
utxoSpent$ = new Subject<object>();
|
utxoSpent$ = new Subject<object>();
|
||||||
difficultyAdjustment$ = new ReplaySubject<DifficultyAdjustment>(1);
|
difficultyAdjustment$ = new ReplaySubject<DifficultyAdjustment>(1);
|
||||||
mempoolTransactions$ = new Subject<Transaction>();
|
mempoolTransactions$ = new Subject<Transaction>();
|
||||||
|
@ -28,6 +28,7 @@ export class WebsocketService {
|
|||||||
private isTrackingTx = false;
|
private isTrackingTx = false;
|
||||||
private trackingTxId: string;
|
private trackingTxId: string;
|
||||||
private isTrackingMempoolBlock = false;
|
private isTrackingMempoolBlock = false;
|
||||||
|
private isTrackingRbf = false;
|
||||||
private trackingMempoolBlock: number;
|
private trackingMempoolBlock: number;
|
||||||
private latestGitCommit = '';
|
private latestGitCommit = '';
|
||||||
private onlineCheckTimeout: number;
|
private onlineCheckTimeout: number;
|
||||||
@ -173,6 +174,16 @@ export class WebsocketService {
|
|||||||
this.isTrackingMempoolBlock = false
|
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) {
|
startTrackBisqMarket(market: string) {
|
||||||
this.websocketSubject.next({ 'track-bisq-market': market });
|
this.websocketSubject.next({ 'track-bisq-market': market });
|
||||||
}
|
}
|
||||||
@ -261,6 +272,10 @@ export class WebsocketService {
|
|||||||
this.stateService.txRbfInfo$.next(response.rbfInfo);
|
this.stateService.txRbfInfo$.next(response.rbfInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (response.rbfLatest) {
|
||||||
|
this.stateService.rbfLatest$.next(response.rbfLatest);
|
||||||
|
}
|
||||||
|
|
||||||
if (response.txReplaced) {
|
if (response.txReplaced) {
|
||||||
this.stateService.txReplaced$.next(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 { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontawesome';
|
||||||
import { faFilter, faAngleDown, faAngleUp, faAngleRight, faAngleLeft, faBolt, faChartArea, faCogs, faCubes, faHammer, faDatabase, faExchangeAlt, faInfoCircle,
|
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,
|
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 { InfiniteScrollModule } from 'ngx-infinite-scroll';
|
||||||
import { MasterPageComponent } from '../components/master-page/master-page.component';
|
import { MasterPageComponent } from '../components/master-page/master-page.component';
|
||||||
import { PreviewTitleComponent } from '../components/master-page-preview/preview-title.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 { AmountShortenerPipe } from '../shared/pipes/amount-shortener.pipe';
|
||||||
import { DifficultyAdjustmentsTable } from '../components/difficulty-adjustments-table/difficulty-adjustments-table.components';
|
import { DifficultyAdjustmentsTable } from '../components/difficulty-adjustments-table/difficulty-adjustments-table.components';
|
||||||
import { BlocksList } from '../components/blocks-list/blocks-list.component';
|
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 { RewardStatsComponent } from '../components/reward-stats/reward-stats.component';
|
||||||
import { DataCyDirective } from '../data-cy.directive';
|
import { DataCyDirective } from '../data-cy.directive';
|
||||||
import { LoadingIndicatorComponent } from '../components/loading-indicator/loading-indicator.component';
|
import { LoadingIndicatorComponent } from '../components/loading-indicator/loading-indicator.component';
|
||||||
@ -153,6 +154,7 @@ import { TestnetAlertComponent } from './components/testnet-alert/testnet-alert.
|
|||||||
AmountShortenerPipe,
|
AmountShortenerPipe,
|
||||||
DifficultyAdjustmentsTable,
|
DifficultyAdjustmentsTable,
|
||||||
BlocksList,
|
BlocksList,
|
||||||
|
RbfList,
|
||||||
DataCyDirective,
|
DataCyDirective,
|
||||||
RewardStatsComponent,
|
RewardStatsComponent,
|
||||||
LoadingIndicatorComponent,
|
LoadingIndicatorComponent,
|
||||||
@ -313,6 +315,7 @@ export class SharedModule {
|
|||||||
library.addIcons(faDownload);
|
library.addIcons(faDownload);
|
||||||
library.addIcons(faQrcode);
|
library.addIcons(faQrcode);
|
||||||
library.addIcons(faArrowRightArrowLeft);
|
library.addIcons(faArrowRightArrowLeft);
|
||||||
|
library.addIcons(faArrowRight);
|
||||||
library.addIcons(faExchangeAlt);
|
library.addIcons(faExchangeAlt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user