Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,52 @@ describe('BitcoinFeeService', () => {
});
});

// --- getSendFeeRate() minimum fee rate floor Tests --- //

describe('getSendFeeRate() minimum fee rate floor', () => {
const feeConfig = { allowUnconfirmedUtxos: false, cpfpFeeMultiplier: 2.0, defaultFeeMultiplier: 1.5 };

beforeEach(() => {
Object.defineProperty(service, 'feeConfig', { get: () => feeConfig });
});

it('should leave a fee rate comfortably above the minimum relay fee unaffected', async () => {
mockClient.estimateSmartFee.mockResolvedValueOnce(10); // 10 * defaultFeeMultiplier (1.5) = 15

const result = await service.getSendFeeRate();

expect(result).toBe(15);
});

it('should clamp a below-floor estimate to the minimum relay fee', async () => {
// Reproduces a production incident: estimateSmartFee returned 0.664 sat/vB during a
// period of very low mempool activity. Even after the default safety multiplier
// (0.664 * 1.5 = 0.996), the result still undercut the node's 1 sat/vB minimum relay
// fee, so the broadcast was rejected. The clamp must raise this to exactly 1.
mockClient.estimateSmartFee.mockResolvedValueOnce(0.664);

const result = await service.getSendFeeRate();

expect(result).toBe(1);
});

it('should act as a floor and not override values already above the minimum', async () => {
mockClient.estimateSmartFee.mockResolvedValueOnce(0.7); // 0.7 * 1.5 = 1.05

const result = await service.getSendFeeRate();

expect(result).toBe(1.05);
});

it('should not clamp the underlying recommended fee rate, only the send fee rate', async () => {
mockClient.estimateSmartFee.mockResolvedValueOnce(0.664);

const recommended = await service.getRecommendedFeeRate();

expect(recommended).toBe(0.664);
});
});

// --- getTxFeeRate() Tests --- //

describe('getTxFeeRate()', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ export interface FeeConfig {
defaultFeeMultiplier: number;
}

// Node's own minimum relay fee floor (sat/vB); broadcasts below this are rejected outright.
const MIN_FEE_RATE_SAT_VB = 1;

export abstract class BitcoinBasedFeeService {
private readonly logger = new DfxLogger(BitcoinBasedFeeService);

Expand Down Expand Up @@ -92,6 +95,9 @@ export abstract class BitcoinBasedFeeService {

// Bitcoin Core's send RPC parses fee_rate with decimals=3; un-rounded floating-point
// products (e.g. 1.935 * 2 = 3.8699999999999997) are rejected with "Invalid amount".
return Util.round(baseRate * multiplier, 3);
// estimateSmartFee can also return an estimate below the node's own minimum relay fee
// during periods of very low mempool activity, so clamp the value that is actually
// broadcast to that floor; the raw estimate from getRecommendedFeeRate() stays unclamped.
return Math.max(Util.round(baseRate * multiplier, 3), MIN_FEE_RATE_SAT_VB);
}
}
Loading