CW-1200-Dust-amount-error-when-sending-All (#2495)
* Add isSendAll parameter to exchange provider trade creation * Improve send-all transaction handling and fee calculation change.
Serhii committed
Sep 5, 2025 at 05:26 UTC
2d52d4585f87601977d9de979d4286d8a05a65f3
5 files changed
+104
-1
cw_bitcoin/lib/electrum_wallet.dart
+100
@@ -851,6 +851,39 @@ abstract class ElectrumWalletBase
851
throw BitcoinTransactionNoDustException();
852
}
853
854
+
855
+ // If there is only one output, and the amount to send is more than the max spendable amount
856
+ // then it is actually a send all transaction
857
+
858
+ if (outputs.length == 1) {
859
+ final maxSpendable = await _maxSpendableNoChangeAmount(
860
+ initialOutput: outputs.first,
861
+ feeRate: feeRate,
862
+ memo: memo,
863
+ hasSilentPayment: hasSilentPayment,
864
+ coinTypeToSpendFrom: coinTypeToSpendFrom,
865
+ );
866
+ if (credentialsAmount > maxSpendable) {
867
+ throw BitcoinTransactionWrongBalanceException();
868
+ }
869
+ if (credentialsAmount >= maxSpendable) {
870
+ final estimateOutput = [
871
+ BitcoinOutput(
872
+ address: outputs.first.address,
873
+ value: BigInt.zero,
874
+ isSilentPayment: outputs.first.isSilentPayment,
875
+ )
876
+ ];
877
+ return estimateSendAllTx(
878
+ estimateOutput,
879
+ feeRate,
880
+ memo: memo,
881
+ hasSilentPayment: hasSilentPayment,
882
+ coinTypeToSpendFrom: coinTypeToSpendFrom,
883
+ );
884
+ }
885
+ }
886
+
887
final utxoDetails = _createUTXOS(
888
sendAll: false,
889
credentialsAmount: credentialsAmount,
@@ -952,7 +985,38 @@ abstract class ElectrumWalletBase
985
updatedOutputs.removeLast();
986
outputs.removeLast();
987
988
+ // If the computed change is negative or below dust:
989
+ // - negative: try a no-change tx (recalculate fee without change)
990
+ // - non-negative but dust: drop change and add remainder to fee
991
if (amountLeftForChange < 0) {
992
+ final tempNoChange = outputs.map((o) => o).toList();
993
+ final feeNoChange = await calcFee(
994
+ utxos: utxoDetails.utxos,
995
+ outputs: tempNoChange,
996
+ network: network,
997
+ memo: memo,
998
+ feeRate: feeRate,
999
+ inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos,
1000
+ vinOutpoints: utxoDetails.vinOutpoints,
1001
+ );
1002
+ final leftover = utxoDetails.allInputsAmount - credentialsAmount - feeNoChange;
1003
+
1004
+ if (leftover >= 0) {
1005
+ final finalFee = feeNoChange + leftover; // absorb tiny remainder
1006
+ return EstimatedTxResult(
1007
+ utxos: utxoDetails.utxos,
1008
+ inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos,
1009
+ publicKeys: utxoDetails.publicKeys,
1010
+ fee: finalFee,
1011
+ amount: amount,
1012
+ hasChange: false,
1013
+ isSendAll: spendingAllCoins,
1014
+ memo: memo,
1015
+ spendsUnconfirmedTX: utxoDetails.spendsUnconfirmedTX,
1016
+ spendsSilentPayment: utxoDetails.spendsSilentPayment,
1017
+ );
1018
+ }
1019
+
1020
if (!spendingAllCoins) {
1021
return estimateTxForAmount(
1022
credentialsAmount,
@@ -1016,6 +1080,42 @@ abstract class ElectrumWalletBase
1080
}
1081
}
1082
1083
+ Future<int> _maxSpendableNoChangeAmount({
1084
+ required BitcoinOutput initialOutput,
1085
+ required int feeRate,
1086
+ String? memo,
1087
+ bool hasSilentPayment = false,
1088
+ UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any,
1089
+ }) async {
1090
+
1091
+ final utxoDetailsAll = _createUTXOS(
1092
+ sendAll: true,
1093
+ paysToSilentPayment: hasSilentPayment,
1094
+ coinTypeToSpendFrom: coinTypeToSpendFrom,
1095
+ );
1096
+
1097
+ final output = [
1098
+ BitcoinOutput(
1099
+ address: initialOutput.address,
1100
+ value: BigInt.zero,
1101
+ isSilentPayment: initialOutput.isSilentPayment,
1102
+ )
1103
+ ];
1104
+
1105
+ final feeNoChange = await calcFee(
1106
+ utxos: utxoDetailsAll.utxos,
1107
+ outputs: output,
1108
+ network: network,
1109
+ memo: memo,
1110
+ feeRate: feeRate,
1111
+ inputPrivKeyInfos: utxoDetailsAll.inputPrivKeyInfos,
1112
+ vinOutpoints: utxoDetailsAll.vinOutpoints,
1113
+ );
1114
+
1115
+ final maxSpendable = utxoDetailsAll.allInputsAmount - feeNoChange;
1116
+ return maxSpendable > 0 ? maxSpendable : 0;
1117
+ }
1118
+
1119
Future<int> calcFee({
1120
required List<UtxoWithAddress> utxos,
1121
required List<BitcoinBaseOutput> outputs,
lib/exchange/provider/letsexchange_exchange_provider.dart
+1
@@ -213,6 +213,7 @@ class LetsExchangeExchangeProvider extends ExchangeProvider {
213
extraId: extraId,
214
userCurrencyFromRaw: '${request.fromCurrency.title}_${request.fromCurrency.tag ?? ''}',
215
userCurrencyToRaw: '${request.toCurrency.title}_${request.toCurrency.tag ?? ''}',
216
+ isSendAll: isSendAll,
217
);
218
} catch (e) {
219
log(e.toString());
lib/exchange/provider/stealth_ex_exchange_provider.dart
+1
@@ -200,6 +200,7 @@ class StealthExExchangeProvider extends ExchangeProvider {
200
extraId: extraId,
201
userCurrencyFromRaw: '${request.fromCurrency.title}_${request.fromCurrency.tag ?? ''}',
202
userCurrencyToRaw: '${request.toCurrency.title}_${request.toCurrency.tag ?? ''}',
203
+ isSendAll: isSendAll,
204
);
205
} catch (e) {
206
log(e.toString());
lib/exchange/provider/xoswap_exchange_provider.dart
+1
@@ -260,6 +260,7 @@ class XOSwapExchangeProvider extends ExchangeProvider {
260
extraId: extraId,
261
userCurrencyFromRaw: '${request.fromCurrency.title}_${request.fromCurrency.tag ?? ''}',
262
userCurrencyToRaw: '${request.toCurrency.title}_${request.toCurrency.tag ?? ''}',
263
+ isSendAll: isSendAll,
264
);
265
} catch (e) {
266
printV(e.toString());
lib/exchange/trade.dart
+1
-1
@@ -119,7 +119,7 @@ class Trade extends HiveObject {
119
bool? isRefund;
120
121
@HiveField(21)
122
- bool? isSendAll;
122
+ bool? isSendAll; /// Must be set on createTrade;
123
124
@HiveField(22)
125
String? router;