fix: precise fee estimation and oscillation protection (Supersedes #195)#264
Conversation
Calculate precise script values Ensure dust oscillation converges and doesn't infinite loop
WalkthroughImplements iterative fee estimation and coin-selection in OnchainWallet, adds address-based output sizing to the tx size estimator, centralizes DUST_AMOUNT, updates send/unroll/bump flows to be dust-aware, minor provider refactor, and expands wallet tests for fee/change/dust scenarios. Changes
Sequence Diagram(s)sequenceDiagram
participant Caller as Caller (send / bump)
participant FeeEstimator as estimateFeesAndSelectCoins
participant CoinSelector as selectCoins
participant TxEstimator as TxWeightEstimator
participant Validator as Dust/Fee Validator
participant Broadcaster as broadcast()
Caller->>FeeEstimator: coins, amount, feeRate, recipientAddress
loop converge
FeeEstimator->>CoinSelector: request selection for target (amount + feeGuess)
CoinSelector-->>FeeEstimator: selectedCoins
FeeEstimator->>TxEstimator: addInput(...) for each selected coin
FeeEstimator->>TxEstimator: addOutputAddress(recipient)
FeeEstimator->>TxEstimator: addOutputAddress(change) if changeGuess ≥ 0
FeeEstimator->>TxEstimator: vsize()
TxEstimator-->>FeeEstimator: estimatedVSize
FeeEstimator->>Validator: compute fee = vsize * feeRate, check dust/convergence
Validator-->>FeeEstimator: converge / adjust feeGuess or fail
end
FeeEstimator-->>Caller: inputs, changeAmount, fee
Caller->>Broadcaster: submit raw tx
Broadcaster-->>Caller: txid
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@src/wallet/unroll.ts`:
- Line 302: The call to TxWeightEstimator.addTxOutput is invalid; replace it
with the address-based API by calling TxWeightEstimator.addOutputAddress and
pass the wallet network as the second argument so sizing uses the correct
network context (e.g., replace usages of addTxOutput(outputAddress) with
addOutputAddress(outputAddress, wallet.network) — adjust the wallet network
accessor name if your wallet object exposes it under a different property).
In `@test/wallet.test.ts`:
- Around line 266-471: The tests mock Esplora fee responses as a plain number
but EsploraProvider.getFeeRate expects an object with fees["1"], so update all
fee-rate mocks in these tests (calls that currently resolve to mockFeeRate) to
return an object with the "1" key (e.g. { "1": mockFeeRate }) so
EsploraProvider.getFeeRate reads the correct value; search for
mockFetch.mockResolvedValueOnce(...) returning mockFeeRate in this file and
replace each with the map-shaped response to validate variable fee-rate
behavior.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@src/utils/txSizeEstimator.ts`:
- Around line 16-21: The getVarIntSize function has off-by-one bugs at the
boundary checks; update its comparisons so boundary values are handled
inclusively (e.g., change checks to n <= 0xffff and n <= 0xffffffff) or use
upper-exclusive thresholds like n < 0x10000 and n < 0x100000000 to correctly
return 3 bytes for 0xffff and 5 bytes for 0xffffffff; modify the getVarIntSize
implementation accordingly to ensure each VarInt range maps to the correct byte
size.
louisinger
left a comment
There was a problem hiding this comment.
thanks! LGTM except some nit comments
| Array.isArray(tip) && | ||
| tip.every((t) => { | ||
| t && | ||
| return ( |
There was a problem hiding this comment.
This is quite an important fix: isValidBlocksTip has been returning false all the time.
shubertm
left a comment
There was a problem hiding this comment.
LGTM. Thanks @jgmcalpine
| * @param n - The integer value to check | ||
| * @returns The size in bytes (1, 3, 5, or 9) | ||
| */ | ||
| const getVarIntSize = (n: number): number => { |
| */ | ||
| addOutputScript(script: Uint8Array): TxWeightEstimator { | ||
| this.outputCount++; | ||
| this.outputSize += 8 + getVarIntSize(script.length) + script.length; |
Description
This PR implements precise fee estimation using address decoding and resolves the "dust oscillation" loop reported by CodeRabbit to close #63.
Reason for new PR:
This supersedes PR #195 due to inactivity. I have built upon the original work by @shubertm and added fixes requested by @louisinger and CodeRabbit which were blocking the merge.
Changes
@scure/btc-signeraddress decoding. This ensures correct fee calculation for Legacy, Segwit, and Taproot inputs/outputs (Fixes @louisinger feedback).estimateFeesAndSelectCoinsto handle the "dust paradox" where removing a change output drops the fee below the budget. This prevents infinite loops/timeouts.Co-authored-by: @shubertm
Summary by CodeRabbit
New Features
Behavior Changes
Bug Fixes / Refinements
Tests
✏️ Tip: You can customize this high-level summary in your review settings.