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;