Conversation
makeSpend ran the fee rate through parseInt, so a 1.8 sat/vB custom fee became 1 sat/vB and the user paid roughly half what they asked for. The GUI passes the typed value straight through, and calcMinerFeePerByte returns a custom rate verbatim, so the truncation was the only place the decimal was lost. Parse the rate with Number and validate it explicitly, since parseInt was also acting as an accidental guard against unparseable input. Number rejects a string unless all of it is a number. parseFloat would stop at the comma in '1,8', which is how a comma decimal locale writes 1.8, and charge 1 sat/vB without an error. The picker now accepts fractional rates via feeRateOrNaN, while output values keep using uintOrNaN and stay whole satoshis. Every satoshi amount derived from a rate goes through feeForBytes, which rounds up so a fractional rate can never underpay or produce a fractional output value. It snaps the product to 12 significant digits first, because 1.1 * 10 evaluates to 11.000000000000002 and would otherwise cost an extra satoshi.
Exercises the real makeSpend path with a custom rate of 1.8 sat/vB and asserts on the returned EdgeTransaction: the decimal survives into feeRateUsed, networkFee is a whole satoshi, inputs minus outputs equals the fee, the target is paid exactly, and the signed transaction lands within a percent of the requested rate. It also asserts that a rate that is not entirely a number, such as '1,8', is rejected rather than read as 1.
The custom fee path already honors decimals. The presets did not: four separate places rounded or clamped a rate back to a whole sat/vB. Fetch from mempool.space's /fees/precise instead of /fees/recommended. Both run the same calculation over the same projected blocks, but /recommended rounds each step to a whole sat/vB and never reports less than 1, so the fractional rates were never visible to us. /precise works in steps of 0.001 sat/vB and adds a priority offset to its two fastest tiers, which keeps fastestFee at 1 or more and halfHourFee at 0.5 or more. Of the presets, only lowFee can fall below 1 sat/vB. Drop the hardcoded LOW_FEE floor of 2 sat/vB. Bitcoin Core lowered its minrelaytxfee default to 0.1 sat/vB, so the floor sat 20x above what the network enforces and above the rate most blocks are clearing at. Nodes older than Core 28.3 or 29.1 still refuse anything under 1 sat/vB, so Edge's own Blockbook servers need one of those versions before this ships, or they will not see transactions sent at those rates. Multiply through biggystring rather than JS numbers, since 0.575 * 1.3 evaluates to 0.7474999999999999. Round the presets to three decimal places rather than to an integer, matching the precision /fees/precise reports. Note biggystring's round takes the power of ten to round at, so three decimals is -3, while div takes a count of decimal places. The interpolated standard rate needs the latter, having previously truncated to a whole sat/vB. Rounding a preset to an integer was not only imprecise, it was fatal below 1 sat/vB: a 0.1 rate became 0, which makeSpend then rejects as an invalid fee rate.
Accelerate derives its rate by doubling the replaced transaction's, then rounded the result. That truncated the decimal the rest of the fee path now preserves, and worse, Math.round returns 0 for anything below 0.5, so any replaced rate under 0.25 sat/vB doubled to a zero fee rate. Zero is not caught downstream. The feeRate > 0 guard lives in makeSpend, which accelerate does not go through: it takes an EdgeTransaction, never an EdgeSpendInfo, and never calls fees.getRate. The picker's own check is feeRateOrNaN, and isFinite(0) is true, so a zero-fee replacement would be built and signed. That was unreachable while the presets floored at 2 sat/vB. It is not anymore, now that Bitcoin relays at 0.1 and we pass those rates through. calcReplacementFeeRate now doubles the rate and rounds it up to FEE_RATE_DECIMALS places. Rounding up means the replacement never pays less than double and a positive rate never reaches zero. Rounding to three places keeps feeRateUsed readable, since the GUI shows it verbatim in a transaction's advanced details. Pulling the calculation out of the engine also gives it the direct test coverage accelerate had none of.
peachbits
force-pushed
the
matthew/decimal-custom-fee
branch
from
September 16, 2026 20:31
a95e0e5 to
a8890fe
Compare
j0ntz
approved these changes
Sep 18, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
CHANGELOG
Does this branch warrant an entry to the CHANGELOG?
Dependencies
none
Description
Asana: https://app.asana.com/1/9976422036640/project/1213843652804305/task/1208974186635443
A 1.8 sat/vB custom fee was charged as 1 sat/vB, and the fee presets were rounded to whole sat/vB and floored at 2. Fee rates now keep their decimals from input to signed transaction.
Do not release until Edge's BTC blockbook nodes are upgraded.
btc-wusa1(Core 28.0.0) andbtc-eu1(Core 29.0.0) refuse transactions under 1 sat/vB, and the Low preset can now go below 1 (mempool.space floorshalfHourFeeat 0.5). Broadcasts still succeed through Trezor and NOWNodes, but a wallet syncing through our nodes won't see its pending transaction until it confirms. Both nodes need Core 29.1 or newer first.1.
Honor fractional custom fee ratesmakeSpendran the rate throughparseInt. It now parses withNumber, which rejects a comma-decimal1,8instead of reading it as 1. Comma-decimal locales will see that error until the GUI normalizes the custom fee input. Every satoshi amount derived from a rate goes throughfeeForBytes, which rounds up.2.
Add engine-level coverage for fractional custom feesmakeSpendandsignTxtests at fractional rates, plus rejection of rates that aren't plain numbers.3.
Support fractional fee presetsFetch mempool.space
/api/v1/fees/preciseinstead of/recommended, drop the 2 sat/vBLOW_FEEfloor, and round presets to 3 decimals instead of integers. A 0.1 sat/vB preset used to round to 0 and fail the send.4.
Keep accelerated transactions at a fractional fee rateAccelerate doubled the replaced rate through
Math.round, which sent anything under 0.25 sat/vB to zero. The doubled rate now rounds up to 3 decimals.Testing
tsc, eslint and the full suite on its own.Note
Medium Risk
Changes core UTXO fee math, preset sourcing, and Doge floors—incorrect rounding could underpay or break sends; sub-1 sat/vB BTC txs may not appear on Edge Blockbook until nodes upgrade.
Overview
UTXO wallets now treat fee rates as fractional sat/vB from quote through coin selection and signing, instead of truncating or rounding to whole sat/vB.
Presets and mempool data: Bitcoin fee presets come from mempool.space
/fees/precise, keep up to three decimal places, and no longer use the old 2 sat/vB floor or integer rounding incalcMinerFeePerByte(sub-1 rates no longer become0and fail sends). Standard-tier interpolation keeps decimal precision in the biggystring math.Spends:
makeSpendparses rates withNumber(notparseInt), rejects invalid strings like comma decimals, and passes the rate into pickers that compute fees viafeeForBytes(ceil to whole satoshis). RBF accelerate doubles the replaced tx rate withcalcReplacementFeeRate(ceil to 3 decimals) so low rates are not doubled to zero.Dogecoin: Custom fee clamping uses the 100 koinu/vB relay floor, not the 1000 mining default (fixes silent 10× bumps).
Note for release: Low BTC presets can fall below 1 sat/vB; Edge’s own Blockbook nodes on older Core may not show those pending txs until nodes are upgraded (per PR description).
Reviewed by Cursor Bugbot for commit a95e0e5. Bugbot is set up for automated code reviews on this repo. Configure here.