Merge pull request #6090 from mempool/mononaut/fractional-fee-recs

decimal fee recommendations
This commit is contained in:
wiz 2025-12-02 18:47:59 +09:00 committed by GitHub
commit 38416b14fe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 47 additions and 34 deletions

View file

@ -129,17 +129,7 @@ class BitcoinRoutes {
res.send('Service Unavailable');
return;
}
let minFee = 0;
if (req.query.min) {
try {
minFee = parseFloat(req.query.min as string);
} catch (e) {
res.statusCode = 400;
res.send('Invalid minimum fee');
return;
}
}
const result = feeApi.getPreciseRecommendedFee(minFee);
const result = feeApi.getPreciseRecommendedFee();
res.json(result);
}

View file

@ -18,6 +18,9 @@ class FeeApi {
constructor() { }
minimumIncrement = isLiquid ? 0.1 : 1;
minFastestFee = isLiquid ? 0.1 : 1;
minHalfHourFee = isLiquid ? 0.1 : 0.5;
priorityFactor = isLiquid ? 0 : 0.5;
public getRecommendedFee(): RecommendedFees {
const pBlocks = projectedBlocks.getMempoolBlocks();
@ -26,17 +29,17 @@ class FeeApi {
return this.calculateRecommendedFee(pBlocks, mPool);
}
public getPreciseRecommendedFee(minimum: number = 0): RecommendedFees {
public getPreciseRecommendedFee(): RecommendedFees {
const pBlocks = projectedBlocks.getMempoolBlocks();
const mPool = mempool.getMempoolInfo();
// minimum non-zero minrelaytxfee / incrementalrelayfee is 1 sat/kvB = 0.001 sat/vB
return this.calculateRecommendedFee(pBlocks, mPool, minimum, 0.001);
return this.calculateRecommendedFee(pBlocks, mPool, 0.001);
}
public calculateRecommendedFee(pBlocks: MempoolBlock[], mPool: IBitcoinApi.MempoolInfo, minimumRecommendation: number = 0, minIncrement: number = this.minimumIncrement): RecommendedFees {
public calculateRecommendedFee(pBlocks: MempoolBlock[], mPool: IBitcoinApi.MempoolInfo, minIncrement: number = this.minimumIncrement): RecommendedFees {
const purgeRate = this.roundUpToNearest(mPool.mempoolminfee * 100000, minIncrement);
const minimumFee = Math.max(purgeRate, minimumRecommendation, minIncrement);
const minimumFee = Math.max(purgeRate, minIncrement);
if (!pBlocks.length) {
return {
@ -67,8 +70,8 @@ class FeeApi {
hourFee = Math.max(hourFee, economyFee);
return {
'fastestFee': this.roundToNearest(fastestFee, minIncrement),
'halfHourFee': this.roundToNearest(halfHourFee, minIncrement),
'fastestFee': Math.max(this.roundToNearest(fastestFee + this.priorityFactor, minIncrement), this.minFastestFee),
'halfHourFee': Math.max(this.roundToNearest(halfHourFee + (this.priorityFactor / 2), minIncrement), this.minHalfHourFee),
'hourFee': this.roundToNearest(hourFee, minIncrement),
'economyFee': this.roundToNearest(economyFee, minIncrement),
'minimumFee': this.roundToNearest(minimumFee, minIncrement),

View file

@ -102,7 +102,7 @@ class WebsocketHandler {
'backendInfo': backendInfo.getBackendInfo(),
'loadingIndicators': loadingIndicators.getLoadingIndicators(),
'da': da?.previousTime ? da : undefined,
'fees': feeApi.getRecommendedFee(),
'fees': feeApi.getPreciseRecommendedFee(),
});
}
@ -639,7 +639,7 @@ class WebsocketHandler {
}
memPool.removeFromSpendMap(deletedTransactions);
memPool.addToSpendMap(newTransactions);
const recommendedFees = feeApi.getRecommendedFee();
const recommendedFees = feeApi.getPreciseRecommendedFee();
const latestTransactions = memPool.getLatestTransactions();
@ -1137,7 +1137,7 @@ class WebsocketHandler {
const mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas();
const da = difficultyAdjustment.getDifficultyAdjustment();
const fees = feeApi.getRecommendedFee();
const fees = feeApi.getPreciseRecommendedFee();
const mempoolInfo = memPool.getMempoolInfo();
// pre-compute address transactions

View file

@ -46,7 +46,7 @@
<div class="stats top right">
<p class="label" i18n="fees-box.high-priority">High Priority</p>
<p *ngIf="recommendedFees$ | async as recommendedFees;">
<app-fee-rate [fee]="recommendedFees.fastestFee" unitClass="" rounding="1.0-0"></app-fee-rate>
<app-fee-rate [fee]="recommendedFees.fastestFee" unitClass="" rounding="1.0-2"></app-fee-rate>
</p>
</div>
<div *ngIf="mode !== 'mempool' && blocks?.length" class="stats bottom left">

View file

@ -10,26 +10,27 @@
<span class="fee-label prority" i18n="fees-box.high-priority" i18n-ngbTooltip="Transaction feerate tooltip (high priority)" ngbTooltip="Places your transaction in the first mempool block" placement="top">High Priority</span>
</div>
</div>
<div class="fee-estimation-container">
<div class="item">
<div class="card-text">
<div class="fee-text"><app-fee-rate [fee]="recommendedFees.economyFee" rounding="1.0-0"></app-fee-rate></div> <span class="fiat"><app-fiat i18n-ngbTooltip="Transaction fee tooltip" ngbTooltip="Based on average native segwit transaction of 140 vBytes" placement="bottom" [value]="recommendedFees.economyFee * 140" ></app-fiat></span>
<div class="fee-text"><app-fee-rate [fee]="recommendedFees.economyFee" [dp]="1"></app-fee-rate></div> <span class="fiat"><app-fiat i18n-ngbTooltip="Transaction fee tooltip" ngbTooltip="Based on average native segwit transaction of 140 vBytes" placement="bottom" [value]="recommendedFees.economyFee * 140" ></app-fiat></span>
</div>
</div>
<div class="band-separator"></div>
<div class="item">
<div class="card-text">
<div class="fee-text"><app-fee-rate [fee]="recommendedFees.hourFee" rounding="1.0-0"></app-fee-rate></div> <span class="fiat"><app-fiat i18n-ngbTooltip="Transaction fee tooltip" ngbTooltip="Based on average native segwit transaction of 140 vBytes" placement="bottom" [value]="recommendedFees.hourFee * 140" ></app-fiat></span>
<div class="fee-text"><app-fee-rate [fee]="recommendedFees.hourFee" [dp]="1"></app-fee-rate></div> <span class="fiat"><app-fiat i18n-ngbTooltip="Transaction fee tooltip" ngbTooltip="Based on average native segwit transaction of 140 vBytes" placement="bottom" [value]="recommendedFees.hourFee * 140" ></app-fiat></span>
</div>
</div>
<div class="item">
<div class="card-text">
<div class="fee-text"><app-fee-rate [fee]="recommendedFees.halfHourFee" rounding="1.0-0"></app-fee-rate></div> <span class="fiat"><app-fiat i18n-ngbTooltip="Transaction fee tooltip" ngbTooltip="Based on average native segwit transaction of 140 vBytes" placement="bottom" [value]="recommendedFees.halfHourFee * 140" ></app-fiat></span>
<div class="fee-text"><app-fee-rate [fee]="recommendedFees.halfHourFee" [dp]="1"></app-fee-rate></div> <span class="fiat"><app-fiat i18n-ngbTooltip="Transaction fee tooltip" ngbTooltip="Based on average native segwit transaction of 140 vBytes" placement="bottom" [value]="recommendedFees.halfHourFee * 140" ></app-fiat></span>
</div>
</div>
<div class="item">
<div class="card-text">
<div class="fee-text"><app-fee-rate [fee]="recommendedFees.fastestFee" rounding="1.0-0"></app-fee-rate></div> <span class="fiat"><app-fiat i18n-ngbTooltip="Transaction fee tooltip" ngbTooltip="Based on average native segwit transaction of 140 vBytes" placement="bottom" [value]="recommendedFees.fastestFee * 140" ></app-fiat></span>
<div class="fee-text"><app-fee-rate [fee]="recommendedFees.fastestFee" [dp]="1"></app-fee-rate></div> <span class="fiat"><app-fiat i18n-ngbTooltip="Transaction fee tooltip" ngbTooltip="Based on average native segwit transaction of 140 vBytes" placement="bottom" [value]="recommendedFees.fastestFee * 140" ></app-fiat></span>
</div>
</div>
</div>

View file

@ -428,7 +428,9 @@ export class WebsocketService {
}
if (response.fees) {
this.stateService.recommendedFees$.next(response.fees);
this.stateService.recommendedFees$.next({
...response.fees,
});
}
if (response.backendInfo) {

View file

@ -1,7 +1,7 @@
<ng-container *ngIf="rateUnits$ | async as units">
<ng-container *ngIf="fee !== undefined; else noFee">
<ng-container *ngIf="units !== 'wu'">{{ fee / (weight / 4) | feeRounding:rounding }} <span *ngIf="showUnit" [class]="unitClass" [style]="unitStyle" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></ng-container>
<ng-container *ngIf="units === 'wu'">{{ fee / weight | feeRounding:rounding }} <span *ngIf="showUnit" [class]="unitClass" [style]="unitStyle" i18n="shared.sat-weight-units|sat/WU">sat/WU</span></ng-container>
<ng-container *ngIf="units !== 'wu'">{{ fee / (weight / 4) | feeRounding:rounding:dp }} <span *ngIf="showUnit" [class]="unitClass" [style]="unitStyle" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></ng-container>
<ng-container *ngIf="units === 'wu'">{{ fee / weight | feeRounding:rounding:dp }} <span *ngIf="showUnit" [class]="unitClass" [style]="unitStyle" i18n="shared.sat-weight-units|sat/WU">sat/WU</span></ng-container>
</ng-container>
<ng-template #noFee>
<ng-container *ngIf="units !== 'wu'">- <span *ngIf="showUnit" [class]="unitClass" [style]="unitStyle" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></ng-container>

View file

@ -1,6 +1,7 @@
import { Component, Input, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { StateService } from '@app/services/state.service';
import { FeeRoundingPipe } from '@app/shared/pipes/fee-rounding/fee-rounding.pipe';
@Component({
selector: 'app-fee-rate',
@ -12,6 +13,8 @@ export class FeeRateComponent implements OnInit {
@Input() fee: number | undefined;
@Input() weight: number = 4;
@Input() rounding: string = null;
@Input() dp: number = null;
@Input() softDecimals: boolean = false;
@Input() showUnit: boolean = true;
@Input() unitClass: string = 'symbol';
@Input() unitStyle: any;
@ -20,9 +23,22 @@ export class FeeRateComponent implements OnInit {
constructor(
private stateService: StateService,
private feeRoundingPipe: FeeRoundingPipe,
) { }
ngOnInit() {
this.rateUnits$ = this.stateService.rateUnits$;
}
getIntegerPart(rate: number): string {
const formatted = this.feeRoundingPipe.transform(rate, this.rounding, this.dp);
const decimalIndex = formatted.indexOf('.');
return decimalIndex === -1 ? formatted : formatted.substring(0, decimalIndex);
}
getDecimalPart(rate: number): string {
const formatted = this.feeRoundingPipe.transform(rate, this.rounding, this.dp);
const decimalIndex = formatted.indexOf('.');
return decimalIndex === -1 ? ' ' : formatted.substring(decimalIndex) + ' ';
}
}

View file

@ -10,16 +10,16 @@ export class FeeRoundingPipe implements PipeTransform {
@Inject(LOCALE_ID) private locale: string,
) {}
transform(fee: number, rounding = null): string {
transform(fee: number, rounding = null, dp = 3): string {
if (rounding) {
return formatNumber(fee, this.locale, rounding);
}
if (fee >= 100) {
return formatNumber(fee, this.locale, '1.0-0')
} else if (fee < 10) {
return formatNumber(fee, this.locale, '1.2-2')
if (fee >= Math.pow(10, (dp || 3) - 1)) {
return formatNumber(fee, this.locale, '1.0-0');
} else if (fee < Math.pow(10, (dp || 3) - 2)) {
return formatNumber(fee, this.locale, '1.2-2');
}
return formatNumber(fee, this.locale, '1.1-1')
return formatNumber(fee, this.locale, '1.1-1');
}
}

View file

@ -279,6 +279,7 @@ import { GithubLogin } from '@components/github-login.component/github-login.com
ShortenStringPipe,
CapAddressPipe,
AmountShortenerPipe,
FeeRoundingPipe,
],
exports: [
MenuComponent,