Enhance bitcoin error message (#1399)

* Enhance bitcoin error message * fix: unconfirmed spends, spend confirmed first, wrong balance exception * Minor fixes --------- Co-authored-by: Rafael Saes <git@rafael.saes.dev>

Omar Hatem committed Apr 26, 2024 at 22:29 UTC 9e4a7f4331d0ed9a26f5d1c73135a7ae06b4d8c8
9 files changed +72 -25
cw_bitcoin/lib/electrum_wallet.dart
+40 -4
@@ -203,10 +203,14 @@ abstract class ElectrumWalletBase
203 List<ECPrivate> privateKeys = [];
204 int allInputsAmount = 0;
205
206 + bool spendsUnconfirmedTX = false;
207 +
208 for (int i = 0; i < unspentCoins.length; i++) {
209 final utx = unspentCoins[i];
210
209 - if (utx.isSending) {
211 + if (utx.isSending && !utx.isFrozen) {
212 + if (!spendsUnconfirmedTX) spendsUnconfirmedTX = utx.confirmations == 0;
213 +
214 allInputsAmount += utx.value;
215
216 final address = addressTypeFromStr(utx.address, network);
@@ -264,6 +268,10 @@ abstract class ElectrumWalletBase
268 // Here, when sending all, the output amount equals to the input value - fee to fully spend every input on the transaction and have no amount left for change
269 int amount = allInputsAmount - fee;
270
271 + if (amount <= 0) {
272 + throw BitcoinTransactionWrongBalanceException();
273 + }
274 +
275 // Attempting to send less than the dust limit
276 if (_isBelowDust(amount)) {
277 throw BitcoinTransactionNoDustException();
@@ -288,6 +296,7 @@ abstract class ElectrumWalletBase
296 isSendAll: true,
297 hasChange: false,
298 memo: memo,
299 + spendsUnconfirmedTX: spendsUnconfirmedTX,
300 );
301 }
302
@@ -297,17 +306,25 @@ abstract class ElectrumWalletBase
306 int feeRate, {
307 int? inputsCount,
308 String? memo,
309 + bool? useUnconfirmed,
310 }) async {
311 final utxos = <UtxoWithAddress>[];
312 List<ECPrivate> privateKeys = [];
313 int allInputsAmount = 0;
314 + bool spendsUnconfirmedTX = false;
315
316 int leftAmount = credentialsAmount;
306 - final sendingCoins = unspentCoins.where((utx) => utx.isSending).toList();
317 + final sendingCoins = unspentCoins.where((utx) => utx.isSending && !utx.isFrozen).toList();
318 + final unconfirmedCoins = sendingCoins.where((utx) => utx.confirmations == 0).toList();
319
320 for (int i = 0; i < sendingCoins.length; i++) {
321 final utx = sendingCoins[i];
322
323 + final isUncormirmed = utx.confirmations == 0;
324 + if (useUnconfirmed != true && isUncormirmed) continue;
325 +
326 + if (!spendsUnconfirmedTX) spendsUnconfirmedTX = isUncormirmed;
327 +
328 allInputsAmount += utx.value;
329 leftAmount = leftAmount - utx.value;
330
@@ -345,11 +362,23 @@ abstract class ElectrumWalletBase
362 }
363
364 final spendingAllCoins = sendingCoins.length == utxos.length;
365 + final spendingAllConfirmedCoins =
366 + !spendsUnconfirmedTX && utxos.length == sendingCoins.length - unconfirmedCoins.length;
367
368 // How much is being spent - how much is being sent
369 int amountLeftForChangeAndFee = allInputsAmount - credentialsAmount;
370
371 if (amountLeftForChangeAndFee <= 0) {
372 + if (!spendingAllCoins) {
373 + return estimateTxForAmount(
374 + credentialsAmount,
375 + outputs,
376 + feeRate,
377 + inputsCount: utxos.length + 1,
378 + memo: memo,
379 + useUnconfirmed: useUnconfirmed ?? spendingAllConfirmedCoins,
380 + );
381 + }
382 throw BitcoinTransactionWrongBalanceException();
383 }
384
@@ -403,6 +432,7 @@ abstract class ElectrumWalletBase
432 feeRate,
433 inputsCount: utxos.length + 1,
434 memo: memo,
435 + useUnconfirmed: useUnconfirmed ?? spendingAllConfirmedCoins,
436 );
437 }
438
@@ -449,6 +479,7 @@ abstract class ElectrumWalletBase
479 feeRate,
480 inputsCount: utxos.length + 1,
481 memo: memo,
482 + useUnconfirmed: useUnconfirmed ?? spendingAllConfirmedCoins,
483 );
484 }
485 }
@@ -461,6 +492,7 @@ abstract class ElectrumWalletBase
492 hasChange: true,
493 isSendAll: false,
494 memo: memo,
495 + spendsUnconfirmedTX: spendsUnconfirmedTX,
496 );
497 }
498
@@ -531,7 +563,7 @@ abstract class ElectrumWalletBase
563 network: network,
564 memo: estimatedTx.memo,
565 outputOrdering: BitcoinOrdering.none,
534 - enableRBF: true,
566 + enableRBF: !estimatedTx.spendsUnconfirmedTX,
567 );
568 } else {
569 txb = BitcoinTransactionBuilder(
@@ -541,7 +573,7 @@ abstract class ElectrumWalletBase
573 network: network,
574 memo: estimatedTx.memo,
575 outputOrdering: BitcoinOrdering.none,
544 - enableRBF: true,
576 + enableRBF: !estimatedTx.spendsUnconfirmedTX,
577 );
578 }
579
@@ -721,6 +753,7 @@ abstract class ElectrumWalletBase
753 final tx = await fetchTransactionInfo(
754 hash: coin.hash, height: 0, myAddresses: addressesSet);
755 coin.isChange = tx?.direction == TransactionDirection.outgoing;
756 + coin.confirmations = tx?.confirmations;
757 updatedUnspentCoins.add(coin);
758 } catch (_) {}
759 }))));
@@ -745,6 +778,7 @@ abstract class ElectrumWalletBase
778 coin.isFrozen = coinInfo.isFrozen;
779 coin.isSending = coinInfo.isSending;
780 coin.note = coinInfo.note;
781 + coin.bitcoinAddressRecord.balance += coinInfo.value;
782 } else {
783 _addCoinInfo(coin);
784 }
@@ -1272,6 +1306,7 @@ class EstimatedTxResult {
1306 required this.hasChange,
1307 required this.isSendAll,
1308 this.memo,
1309 + required this.spendsUnconfirmedTX,
1310 });
1311
1312 final List<UtxoWithAddress> utxos;
@@ -1281,6 +1316,7 @@ class EstimatedTxResult {
1316 final bool hasChange;
1317 final bool isSendAll;
1318 final String? memo;
1319 + final bool spendsUnconfirmedTX;
1320 }
1321
1322 BitcoinBaseAddress addressTypeFromStr(String address, BasedUtxoNetwork network) {
cw_bitcoin/lib/exceptions.dart
+3 -1
@@ -15,7 +15,9 @@ class BitcoinTransactionNoDustOnChangeException extends TransactionNoDustOnChang
15 BitcoinTransactionNoDustOnChangeException(super.max, super.min);
16 }
17
18 -class BitcoinTransactionCommitFailed extends TransactionCommitFailed {}
18 +class BitcoinTransactionCommitFailed extends TransactionCommitFailed {
19 + BitcoinTransactionCommitFailed({super.errorMessage});
20 +}
21
22 class BitcoinTransactionCommitFailedDustChange extends TransactionCommitFailedDustChange {}
23
cw_bitcoin/lib/pending_bitcoin_transaction.dart
+2
@@ -73,7 +73,9 @@ class PendingBitcoinTransaction with PendingTransaction {
73 if (error.contains("bad-txns-vout-negative")) {
74 throw BitcoinTransactionCommitFailedVoutNegative();
75 }
76 + throw BitcoinTransactionCommitFailed(errorMessage: error);
77 }
78 +
79 throw BitcoinTransactionCommitFailed();
80 }
81
cw_bitcoin/pubspec.lock
+17 -17
@@ -21,10 +21,10 @@ packages:
21 dependency: transitive
22 description:
23 name: args
24 - sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596
24 + sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a"
25 url: "https://pub.dev"
26 source: hosted
27 - version: "2.4.2"
27 + version: "2.5.0"
28 asn1lib:
29 dependency: transitive
30 description:
@@ -80,7 +80,7 @@ packages:
80 description:
81 path: "."
82 ref: cake-update-v2
83 - resolved-ref: "3fd81d238b990bb767fc7a4fdd5053a22a142e2e"
83 + resolved-ref: "01d844a5f5a520a31df5254e34169af4664aa769"
84 url: "https://github.com/cake-tech/bitcoin_base.git"
85 source: git
86 version: "4.2.0"
@@ -153,10 +153,10 @@ packages:
153 dependency: "direct dev"
154 description:
155 name: build_runner
156 - sha256: "581bacf68f89ec8792f5e5a0b2c4decd1c948e97ce659dc783688c8a88fbec21"
156 + sha256: "3ac61a79bfb6f6cc11f693591063a7f19a7af628dc52f141743edac5c16e8c22"
157 url: "https://pub.dev"
158 source: hosted
159 - version: "2.4.8"
159 + version: "2.4.9"
160 build_runner_core:
161 dependency: transitive
162 description:
@@ -177,10 +177,10 @@ packages:
177 dependency: transitive
178 description:
179 name: built_value
180 - sha256: a3ec2e0f967bc47f69f95009bb93db936288d61d5343b9436e378b28a2f830c6
180 + sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb
181 url: "https://pub.dev"
182 source: hosted
183 - version: "8.9.0"
183 + version: "8.9.2"
184 characters:
185 dependency: transitive
186 description:
@@ -309,10 +309,10 @@ packages:
309 dependency: "direct main"
310 description:
311 name: flutter_mobx
312 - sha256: "4a5d062ff85ed3759f4aac6410ff0ffae32e324b2e71ca722ae1b37b32e865f4"
312 + sha256: "859fbf452fa9c2519d2700b125dd7fb14c508bbdd7fb65e26ca8ff6c92280e2e"
313 url: "https://pub.dev"
314 source: hosted
315 - version: "2.2.0+2"
315 + version: "2.2.1+1"
316 flutter_test:
317 dependency: "direct dev"
318 description: flutter
@@ -322,10 +322,10 @@ packages:
322 dependency: transitive
323 description:
324 name: frontend_server_client
325 - sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612"
325 + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
326 url: "https://pub.dev"
327 source: hosted
328 - version: "3.2.0"
328 + version: "4.0.0"
329 glob:
330 dependency: transitive
331 description:
@@ -466,10 +466,10 @@ packages:
466 dependency: "direct main"
467 description:
468 name: mobx
469 - sha256: "74ee54012dc7c1b3276eaa960a600a7418ef5f9997565deb8fca1fd88fb36b78"
469 + sha256: "63920b27b32ad1910adfe767ab1750e4c212e8923232a1f891597b362074ea5e"
470 url: "https://pub.dev"
471 source: hosted
472 - version: "2.3.0+1"
472 + version: "2.3.3+2"
473 mobx_codegen:
474 dependency: "direct dev"
475 description:
@@ -570,10 +570,10 @@ packages:
570 dependency: transitive
571 description:
572 name: pointycastle
573 - sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29"
573 + sha256: "70fe966348fe08c34bf929582f1d8247d9d9408130723206472b4687227e4333"
574 url: "https://pub.dev"
575 source: hosted
576 - version: "3.7.4"
576 + version: "3.8.0"
577 pool:
578 dependency: transitive
579 description:
@@ -586,10 +586,10 @@ packages:
586 dependency: transitive
587 description:
588 name: provider
589 - sha256: "9a96a0a19b594dbc5bf0f1f27d2bc67d5f95957359b461cd9feb44ed6ae75096"
589 + sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c
590 url: "https://pub.dev"
591 source: hosted
592 - version: "6.1.1"
592 + version: "6.1.2"
593 pub_semver:
594 dependency: transitive
595 description:
cw_bitcoin_cash/lib/src/pending_bitcoin_cash_transaction.dart
+2
@@ -62,7 +62,9 @@ class PendingBitcoinCashTransaction with PendingTransaction {
62 if (error.contains("bad-txns-vout-negative")) {
63 throw BitcoinTransactionCommitFailedVoutNegative();
64 }
65 + throw BitcoinTransactionCommitFailed(errorMessage: error);
66 }
67 +
68 throw BitcoinTransactionCommitFailed();
69 }
70
cw_core/lib/exceptions.dart
+5 -1
@@ -19,7 +19,11 @@ class TransactionNoDustOnChangeException implements Exception {
19 final String min;
20 }
21
22 -class TransactionCommitFailed implements Exception {}
22 +class TransactionCommitFailed implements Exception {
23 + final String? errorMessage;
24 +
25 + TransactionCommitFailed({this.errorMessage});
26 +}
27
28 class TransactionCommitFailedDustChange implements Exception {}
29
cw_core/lib/unspent_transaction_output.dart
+1
@@ -14,6 +14,7 @@ class Unspent {
14 bool isChange;
15 bool isSending;
16 bool isFrozen;
17 + int? confirmations;
18 String note;
19
20 bool get isP2wpkh => address.startsWith('bc') || address.startsWith('ltc');
lib/src/screens/send/send_page.dart
+1 -1
@@ -100,7 +100,7 @@ class SendPage extends BasePage {
100 AppBarStyle get appBarStyle => AppBarStyle.transparent;
101
102 double _sendCardHeight(BuildContext context) {
103 - double initialHeight = 450;
103 + double initialHeight = 480;
104 if (sendViewModel.hasCoinControl) {
105 initialHeight += 35;
106 }
lib/view_model/send/send_view_model.dart
+1 -1
@@ -562,7 +562,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
562 return S.current.tx_no_dust_exception;
563 }
564 if (error is TransactionCommitFailed) {
565 - return S.current.tx_commit_failed;
565 + return "${S.current.tx_commit_failed}${error.errorMessage != null ? "\n\n${error.errorMessage}" : ""}";
566 }
567 if (error is TransactionCommitFailedDustChange) {
568 return S.current.tx_rejected_dust_change;