diff --git a/cw_bitcoin/lib/electrum.dart b/cw_bitcoin/lib/electrum.dart index 44aa096c4..b3bc6e843 100644 --- a/cw_bitcoin/lib/electrum.dart +++ b/cw_bitcoin/lib/electrum.dart @@ -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> 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 (_) { diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 5d3af65f5..857317a90 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -750,6 +750,9 @@ abstract class ElectrumWalletBase } } + static bool _isValidFeeRates(List feeRates) => + feeRates.length == 3 && feeRates.every((rate) => rate > 0); + @action Future 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 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 fetchTransactionsForAddressTypeBatch( Map 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 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 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 addresses) { for (int i = addresses.length - 1; i >= 0; i--) {