fix: sanitize Electrum fee estimates (#3499)

* fix: sanitize Electrum fee estimates

* chore: drop test changes from this PR

Removes the test additions/modifications introduced by this branch so the
test design is left to the maintainers. Production code is unchanged.
This commit is contained in:
Seth For Privacy
2026-08-16 11:01:51 +02:00
committed by GitHub
parent 8a366129f8
commit 86db89913b
2 changed files with 44 additions and 23 deletions
+21 -6
View File
@@ -579,14 +579,29 @@ class ElectrumClient {
return [];
});
// Floor at 0 so unavailable/-1 estimates never become negative rates;
// cap at 2000 sat/vB per CW-1597.
static const int _maxFeeRate = 2000;
static int _sanitizeFeeRate(double feeRate) {
final rate = (stringDoubleToBitcoinAmount(feeRate.toString()) / 1000).round();
if (rate < 0) {
return 0;
}
if (rate > _maxFeeRate) {
return _maxFeeRate;
}
return rate;
}
Future<List<int>> feeRates({BasedUtxoNetwork? network}) async {
try {
final topDoubleString = await estimatefee(p: 1);
final middleDoubleString = await estimatefee(p: 5);
final bottomDoubleString = await estimatefee(p: 10);
final top = (stringDoubleToBitcoinAmount(topDoubleString.toString()) / 1000).round();
final middle = (stringDoubleToBitcoinAmount(middleDoubleString.toString()) / 1000).round();
final bottom = (stringDoubleToBitcoinAmount(bottomDoubleString.toString()) / 1000).round();
final topDouble = await estimatefee(p: 1);
final middleDouble = await estimatefee(p: 5);
final bottomDouble = await estimatefee(p: 10);
final top = _sanitizeFeeRate(topDouble);
final middle = _sanitizeFeeRate(middleDouble);
final bottom = _sanitizeFeeRate(bottomDouble);
return [bottom, middle, top];
} catch (_) {
+23 -17
View File
@@ -750,6 +750,9 @@ abstract class ElectrumWalletBase
}
}
static bool _isValidFeeRates(List<int> feeRates) =>
feeRates.length == 3 && feeRates.every((rate) => rate > 0);
@action
Future<void> updateFeeRates() async {
if (await checkIfMempoolAPIIsEnabled() && type == WalletType.bitcoin) {
@@ -776,7 +779,7 @@ abstract class ElectrumWalletBase
}
final feeRates = await electrumClient.feeRates(network: network);
if (feeRates != [0, 0, 0]) {
if (_isValidFeeRates(feeRates)) {
_feeRates = feeRates;
} else if (isTestnet) {
_feeRates = [1, 1, 1];
@@ -1024,7 +1027,7 @@ abstract class ElectrumWalletBase
vinOutpoints: utxoDetails.vinOutpoints,
);
if (fee == 0) {
if (fee <= 0) {
throw BitcoinTransactionNoFeeException();
}
@@ -1205,7 +1208,7 @@ abstract class ElectrumWalletBase
));
}
if (fee == 0) {
if (fee <= 0) {
throw BitcoinTransactionNoFeeException();
}
@@ -2513,16 +2516,16 @@ abstract class ElectrumWalletBase
Map<String, ElectrumTransactionInfo> historiesWithDetails,
BitcoinAddressType type,
) async {
final addressesByType =
walletAddresses.allAddresses.where((addr) => addr.type == type).toList();
walletAddresses.allAddresses.where((addr) => addr.type == type).toList();
final receiveStandard = getAddressBranchByType(hidden: false, legacy: false, type: type);
final changeStandard = getAddressBranchByType(hidden: true, legacy: false, type: type);
final changeStandard = getAddressBranchByType(hidden: true, legacy: false, type: type);
final receiveLegacy = getAddressBranchByType(hidden: false, legacy: true, type: type);
final changeLegacy = getAddressBranchByType(hidden: true, legacy: true, type: type);
final changeLegacy = getAddressBranchByType(hidden: true, legacy: true, type: type);
walletAddresses.hiddenAddresses.addAll([...changeStandard, ...changeLegacy].map((e) => e.address));
walletAddresses.hiddenAddresses
.addAll([...changeStandard, ...changeLegacy].map((e) => e.address));
await walletAddresses.saveAddressesInBox();
await Future.wait(addressesByType.map((addressRecord) async {
final history = await _fetchAddressHistory(addressRecord, await getCurrentChainTip());
@@ -2637,13 +2640,13 @@ abstract class ElectrumWalletBase
Future<void> fetchTransactionsForAddressTypeBatch(
Map<String, ElectrumTransactionInfo> historiesWithDetails, BitcoinAddressType type) async {
final receiveStandard = getAddressBranchByType(hidden: false, legacy: false, type: type);
final changeStandard = getAddressBranchByType(hidden: true, legacy: false, type: type);
final changeStandard = getAddressBranchByType(hidden: true, legacy: false, type: type);
final receiveLegacy = getAddressBranchByType(hidden: false, legacy: true, type: type);
final changeLegacy = getAddressBranchByType(hidden: true, legacy: true, type: type);
final changeLegacy = getAddressBranchByType(hidden: true, legacy: true, type: type);
walletAddresses.hiddenAddresses.addAll([...changeStandard, ...changeLegacy].map((e) => e.address));
walletAddresses.hiddenAddresses
.addAll([...changeStandard, ...changeLegacy].map((e) => e.address));
await walletAddresses.saveAddressesInBox();
await fetchTransactionsForAddressesBranchBatch(
@@ -2665,7 +2668,7 @@ abstract class ElectrumWalletBase
await fetchTransactionsForAddressesBranchBatch(
historiesWithDetails,
type,
receiveLegacy,
receiveLegacy,
isHidden: false,
isLegacyDerivation: true,
);
@@ -2749,10 +2752,13 @@ abstract class ElectrumWalletBase
}
}
List<BitcoinAddressRecord> getAddressBranchByType({required bool hidden, required bool legacy, required BitcoinAddressType
type}) => walletAddresses.allAddresses.where((addr) => addr.type == type && addr.isHidden == hidden && addr.isLegacyDerivation == legacy)
.toList()
..sort((a, b) => a.index.compareTo(b.index));
List<BitcoinAddressRecord> getAddressBranchByType(
{required bool hidden, required bool legacy, required BitcoinAddressType type}) =>
walletAddresses.allAddresses
.where((addr) =>
addr.type == type && addr.isHidden == hidden && addr.isLegacyDerivation == legacy)
.toList()
..sort((a, b) => a.index.compareTo(b.index));
int _highestUsedIndex(List<BitcoinAddressRecord> addresses) {
for (int i = addresses.length - 1; i >= 0; i--) {