rbf-fixes-recomended-fee-rate (#1684)
* rbf fixes * Revert "rbf fixes" * fix replaced transactions * re-format electrum_wallet.dart [skip ci] * minor fixes [skip ci] --------- Co-authored-by: OmarHatem <omarh.ismail1@gmail.com>
Serhii committed
Sep 20, 2024 at 23:57 UTC
32e119e24fcd1a78fbe397e065f54701dbc767f1
13 files changed
+105
-45
cw_bitcoin/lib/electrum_transaction_info.dart
+8
-1
@@ -35,6 +35,7 @@ class ElectrumTransactionInfo extends TransactionInfo {
35
List<String>? outputAddresses,
36
required TransactionDirection direction,
37
required bool isPending,
38
+ required bool isReplaced,
39
required DateTime date,
40
required int confirmations,
41
String? to,
@@ -50,6 +51,7 @@ class ElectrumTransactionInfo extends TransactionInfo {
51
this.direction = direction;
52
this.date = date;
53
this.isPending = isPending;
54
+ this.isReplaced = isReplaced;
55
this.confirmations = confirmations;
56
this.to = to;
57
}
@@ -98,6 +100,7 @@ class ElectrumTransactionInfo extends TransactionInfo {
100
id: id,
101
height: height,
102
isPending: false,
103
+ isReplaced: false,
104
fee: fee,
105
direction: direction,
106
amount: amount,
@@ -173,6 +176,7 @@ class ElectrumTransactionInfo extends TransactionInfo {
176
id: bundle.originalTransaction.txId(),
177
height: height,
178
isPending: bundle.confirmations == 0,
179
+ isReplaced: false,
180
inputAddresses: inputAddresses,
181
outputAddresses: outputAddresses,
182
fee: fee,
@@ -196,6 +200,7 @@ class ElectrumTransactionInfo extends TransactionInfo {
200
direction: parseTransactionDirectionFromInt(data['direction'] as int),
201
date: DateTime.fromMillisecondsSinceEpoch(data['date'] as int),
202
isPending: data['isPending'] as bool,
203
+ isReplaced: data['isReplaced'] as bool? ?? false,
204
confirmations: data['confirmations'] as int,
205
inputAddresses:
206
inputAddresses.isEmpty ? [] : inputAddresses.map((e) => e.toString()).toList(),
@@ -238,6 +243,7 @@ class ElectrumTransactionInfo extends TransactionInfo {
243
direction: direction,
244
date: date,
245
isPending: isPending,
246
+ isReplaced: isReplaced ?? false,
247
inputAddresses: inputAddresses,
248
outputAddresses: outputAddresses,
249
confirmations: info.confirmations);
@@ -251,6 +257,7 @@ class ElectrumTransactionInfo extends TransactionInfo {
257
m['direction'] = direction.index;
258
m['date'] = date.millisecondsSinceEpoch;
259
m['isPending'] = isPending;
260
+ m['isReplaced'] = isReplaced;
261
m['confirmations'] = confirmations;
262
m['fee'] = fee;
263
m['to'] = to;
@@ -262,6 +269,6 @@ class ElectrumTransactionInfo extends TransactionInfo {
269
}
270
271
String toString() {
265
- return 'ElectrumTransactionInfo(id: $id, height: $height, amount: $amount, fee: $fee, direction: $direction, date: $date, isPending: $isPending, confirmations: $confirmations, to: $to, unspent: $unspents, inputAddresses: $inputAddresses, outputAddresses: $outputAddresses)';
272
+ return 'ElectrumTransactionInfo(id: $id, height: $height, amount: $amount, fee: $fee, direction: $direction, date: $date, isPending: $isPending, isReplaced: $isReplaced, confirmations: $confirmations, to: $to, unspent: $unspents, inputAddresses: $inputAddresses, outputAddresses: $outputAddresses)';
273
}
274
}
cw_bitcoin/lib/electrum_wallet.dart
+15
-4
@@ -1400,14 +1400,16 @@ abstract class ElectrumWalletBase
1400
}
1401
}
1402
1403
- Future<bool> canReplaceByFee(ElectrumTransactionInfo tx) async {
1403
+ int transactionVSize(String transactionHex) => BtcTransaction.fromRaw(transactionHex).getVSize();
1404
+
1405
+ Future<String?> canReplaceByFee(ElectrumTransactionInfo tx) async {
1406
try {
1407
final bundle = await getTransactionExpanded(hash: tx.txHash);
1408
_updateInputsAndOutputs(tx, bundle);
1407
- if (bundle.confirmations > 0) return false;
1408
- return bundle.originalTransaction.canReplaceByFee;
1409
+ if (bundle.confirmations > 0) return null;
1410
+ return bundle.originalTransaction.canReplaceByFee ? bundle.originalTransaction.toHex() : null;
1411
} catch (e) {
1410
- return false;
1412
+ return null;
1413
}
1414
}
1415
@@ -1589,6 +1591,13 @@ abstract class ElectrumWalletBase
1591
hasChange: changeOutputs.isNotEmpty,
1592
feeRate: newFee.toString(),
1593
)..addListener((transaction) async {
1594
+ transactionHistory.transactions.values.forEach((tx) {
1595
+ if (tx.id == hash) {
1596
+ tx.isReplaced = true;
1597
+ tx.isPending = false;
1598
+ transactionHistory.addOne(tx);
1599
+ }
1600
+ });
1601
transactionHistory.addOne(transaction);
1602
await updateBalance();
1603
});
@@ -2317,6 +2326,7 @@ Future<void> startRefresh(ScanData scanData) async {
2326
fee: 0,
2327
direction: TransactionDirection.incoming,
2328
isPending: false,
2329
+ isReplaced: false,
2330
date: scanData.network == BitcoinNetwork.mainnet
2331
? getDateByBitcoinHeight(tweakHeight)
2332
: DateTime.now(),
@@ -2424,6 +2434,7 @@ class EstimatedTxResult {
2434
final int fee;
2435
final int amount;
2436
final bool spendsSilentPayment;
2437
+
2438
// final bool sendsToSilentPayment;
2439
final bool hasChange;
2440
final bool isSendAll;
cw_bitcoin/lib/pending_bitcoin_transaction.dart
+1
@@ -110,6 +110,7 @@ class PendingBitcoinTransaction with PendingTransaction {
110
direction: TransactionDirection.outgoing,
111
date: DateTime.now(),
112
isPending: true,
113
+ isReplaced: false,
114
confirmations: 0,
115
fee: fee);
116
}
cw_core/lib/transaction_info.dart
+1
@@ -18,6 +18,7 @@ abstract class TransactionInfo extends Object with Keyable {
18
String? to;
19
String? from;
20
String? evmSignatureName;
21
+ bool? isReplaced;
22
List<String>? inputAddresses;
23
List<String>? outputAddresses;
24
lib/bitcoin/cw_bitcoin.dart
+7
-1
@@ -411,12 +411,18 @@ class CWBitcoin extends Bitcoin {
411
}
412
413
@override
414
- Future<bool> canReplaceByFee(Object wallet, Object transactionInfo) async {
414
+ Future<String?> canReplaceByFee(Object wallet, Object transactionInfo) async {
415
final bitcoinWallet = wallet as ElectrumWallet;
416
final tx = transactionInfo as ElectrumTransactionInfo;
417
return bitcoinWallet.canReplaceByFee(tx);
418
}
419
420
+ @override
421
+ int getTransactionVSize(Object wallet, String transactionHex) {
422
+ final bitcoinWallet = wallet as ElectrumWallet;
423
+ return bitcoinWallet.transactionVSize(transactionHex);
424
+ }
425
+
426
@override
427
Future<bool> isChangeSufficientForFee(Object wallet, String txId, String newFee) async {
428
final bitcoinWallet = wallet as ElectrumWallet;
lib/di.dart
+32
-18
@@ -1080,21 +1080,40 @@ Future<void> setup({
1080
param1: derivations,
1081
)));
1082
1083
- getIt.registerFactoryParam<TransactionDetailsViewModel, TransactionInfo, void>(
1084
- (TransactionInfo transactionInfo, _) {
1085
- final wallet = getIt.get<AppStore>().wallet!;
1086
- return TransactionDetailsViewModel(
1087
- transactionInfo: transactionInfo,
1088
- transactionDescriptionBox: _transactionDescriptionBox,
1089
- wallet: wallet,
1090
- settingsStore: getIt.get<SettingsStore>(),
1091
- sendViewModel: getIt.get<SendViewModel>());
1092
- });
1083
+ getIt.registerFactoryParam<TransactionDetailsViewModel, List<dynamic>, void>(
1084
+ (params, _) {
1085
+ final transactionInfo = params[0] as TransactionInfo;
1086
+ final canReplaceByFee = params[1] as bool? ?? false;
1087
+ final wallet = getIt.get<AppStore>().wallet!;
1088
+
1089
+ return TransactionDetailsViewModel(
1090
+ transactionInfo: transactionInfo,
1091
+ transactionDescriptionBox: _transactionDescriptionBox,
1092
+ wallet: wallet,
1093
+ settingsStore: getIt.get<SettingsStore>(),
1094
+ sendViewModel: getIt.get<SendViewModel>(),
1095
+ canReplaceByFee: canReplaceByFee,
1096
+ );
1097
+ }
1098
+ );
1099
1100
getIt.registerFactoryParam<TransactionDetailsPage, TransactionInfo, void>(
1095
- (TransactionInfo transactionInfo, _) => TransactionDetailsPage(
1096
- transactionDetailsViewModel:
1097
- getIt.get<TransactionDetailsViewModel>(param1: transactionInfo)));
1101
+ (TransactionInfo transactionInfo, _) => TransactionDetailsPage(
1102
+ transactionDetailsViewModel: getIt.get<TransactionDetailsViewModel>(
1103
+ param1: [transactionInfo, false])));
1104
+
1105
+ getIt.registerFactoryParam<RBFDetailsPage, List<dynamic>, void>(
1106
+ (params, _) {
1107
+ final transactionInfo = params[0] as TransactionInfo;
1108
+ final txHex = params[1] as String;
1109
+ return RBFDetailsPage(
1110
+ transactionDetailsViewModel: getIt.get<TransactionDetailsViewModel>(
1111
+ param1: [transactionInfo, true],
1112
+ ),
1113
+ rawTransaction: txHex,
1114
+ );
1115
+ }
1116
+ );
1117
1118
getIt.registerFactoryParam<NewWalletTypePage, NewWalletTypeArguments, void>(
1119
(newWalletTypeArguments, _) {
@@ -1265,11 +1284,6 @@ Future<void> setup({
1284
1285
getIt.registerFactory(() => CakePayAccountPage(getIt.get<CakePayAccountViewModel>()));
1286
1268
- getIt.registerFactoryParam<RBFDetailsPage, TransactionInfo, void>(
1269
- (TransactionInfo transactionInfo, _) => RBFDetailsPage(
1270
- transactionDetailsViewModel:
1271
- getIt.get<TransactionDetailsViewModel>(param1: transactionInfo)));
1272
-
1287
getIt.registerFactory(() => AnonPayApi(
1288
useTorOnly: getIt.get<SettingsStore>().exchangeStatus == ExchangeApiMode.torOnly,
1289
wallet: getIt.get<AppStore>().wallet!));
lib/router.dart
+1
-1
@@ -375,7 +375,7 @@ Route<dynamic> createRoute(RouteSettings settings) {
375
case Routes.bumpFeePage:
376
return CupertinoPageRoute<void>(
377
fullscreenDialog: true,
378
- builder: (_) => getIt.get<RBFDetailsPage>(param1: settings.arguments as TransactionInfo));
378
+ builder: (_) => getIt.get<RBFDetailsPage>(param1: settings.arguments as List<dynamic>));
379
380
case Routes.newSubaddress:
381
return CupertinoPageRoute<void>(
lib/src/screens/dashboard/pages/transactions_page.dart
+2
-5
@@ -84,10 +84,7 @@ class TransactionsPage extends StatelessWidget {
84
}
85
86
final transaction = item.transaction;
87
- final transactionType = dashboardViewModel.type == WalletType.ethereum &&
88
- transaction.evmSignatureName == 'approval'
89
- ? ' (${transaction.evmSignatureName})'
90
- : '';
87
+ final transactionType = dashboardViewModel.getTransactionType(transaction);
88
89
return Observer(
90
builder: (_) => TransactionRow(
@@ -102,7 +99,7 @@ class TransactionsPage extends StatelessWidget {
99
: item.formattedFiatAmount,
100
isPending: transaction.isPending,
101
title:
105
- item.formattedTitle + item.formattedStatus + ' $transactionType',
102
+ item.formattedTitle + item.formattedStatus + transactionType,
103
isReceivedSilentPayment:
104
dashboardViewModel.type == WalletType.bitcoin &&
105
bitcoin!.txIsReceivedSilentPayment(transaction),
lib/src/screens/transaction_details/rbf_details_page.dart
+8
-3
@@ -24,19 +24,22 @@ import 'package:flutter_mobx/flutter_mobx.dart';
24
import 'package:mobx/mobx.dart';
25
26
class RBFDetailsPage extends BasePage {
27
- RBFDetailsPage({required this.transactionDetailsViewModel});
27
+ RBFDetailsPage({required this.transactionDetailsViewModel, required this.rawTransaction}) {
28
+ transactionDetailsViewModel.addBumpFeesListItems(
29
+ transactionDetailsViewModel.transactionInfo, rawTransaction);
30
+ }
31
32
@override
33
String get title => S.current.bump_fee;
34
35
final TransactionDetailsViewModel transactionDetailsViewModel;
36
+ final String rawTransaction;
37
38
bool _effectsInstalled = false;
39
40
@override
41
Widget body(BuildContext context) {
42
_setEffects(context);
39
-
43
return Column(
44
children: [
45
Expanded(
@@ -166,7 +169,9 @@ class RBFDetailsPage extends BasePage {
169
actionRightButton: () async {
170
Navigator.of(popupContext).pop();
171
await transactionDetailsViewModel.sendViewModel.commitTransaction();
169
- // transactionStatePopup();
172
+ try {
173
+ Navigator.of(popupContext).pop();
174
+ } catch (_) {}
175
},
176
actionLeftButton: () => Navigator.of(popupContext).pop(),
177
feeFiatAmount:
lib/src/screens/transaction_details/transaction_details_page.dart
+1
-1
@@ -75,7 +75,7 @@ class TransactionDetailsPage extends BasePage {
75
text: S.of(context).bump_fee,
76
onTap: () async {
77
Navigator.of(context).pushNamed(Routes.bumpFeePage,
78
- arguments: transactionDetailsViewModel.transactionInfo);
78
+ arguments: [transactionDetailsViewModel.transactionInfo, transactionDetailsViewModel.rawTransaction]);
79
},
80
),
81
);
lib/view_model/dashboard/dashboard_view_model.dart
+10
@@ -764,6 +764,16 @@ abstract class DashboardViewModelBase with Store {
764
}
765
}
766
767
+ String getTransactionType(TransactionInfo tx) {
768
+ if (wallet.type == WalletType.bitcoin) {
769
+ if (tx.isReplaced == true) return ' (replaced)';
770
+ }
771
+
772
+ if (wallet.type == WalletType.ethereum && tx.evmSignatureName == 'approval')
773
+ return ' (${tx.evmSignatureName})';
774
+ return '';
775
+ }
776
+
777
Future<void> refreshDashboard() async {
778
reconnect();
779
}
lib/view_model/transaction_details_view_model.dart
+17
-10
@@ -36,7 +36,8 @@ abstract class TransactionDetailsViewModelBase with Store {
36
required this.transactionDescriptionBox,
37
required this.wallet,
38
required this.settingsStore,
39
- required this.sendViewModel})
39
+ required this.sendViewModel,
40
+ this.canReplaceByFee = false})
41
: items = [],
42
RBFListItems = [],
43
newFee = 0,
@@ -51,8 +52,7 @@ abstract class TransactionDetailsViewModelBase with Store {
52
break;
53
case WalletType.bitcoin:
54
_addElectrumListItems(tx, dateFormat);
54
- _addBumpFeesListItems(tx);
55
- _checkForRBF(tx);
55
+ if(!canReplaceByFee)_checkForRBF(tx);
56
break;
57
case WalletType.litecoin:
58
case WalletType.bitcoinCash:
@@ -139,13 +139,11 @@ abstract class TransactionDetailsViewModelBase with Store {
139
bool showRecipientAddress;
140
bool isRecipientAddressShown;
141
int newFee;
142
+ String? rawTransaction;
143
TransactionPriority? transactionPriority;
144
145
@observable
145
- bool _canReplaceByFee = false;
146
-
147
- @computed
148
- bool get canReplaceByFee => _canReplaceByFee /*&& transactionInfo.confirmations <= 0*/;
146
+ bool canReplaceByFee;
147
148
String _explorerUrl(WalletType type, String txId) {
149
switch (type) {
@@ -347,7 +345,7 @@ abstract class TransactionDetailsViewModelBase with Store {
345
items.addAll(_items);
346
}
347
350
- void _addBumpFeesListItems(TransactionInfo tx) {
348
+ void addBumpFeesListItems(TransactionInfo tx, String rawTransaction) {
349
transactionPriority = bitcoin!.getBitcoinTransactionPriorityMedium();
350
final inputsCount = (transactionInfo.inputAddresses?.isEmpty ?? true)
351
? 1
@@ -361,6 +359,14 @@ abstract class TransactionDetailsViewModelBase with Store {
359
360
RBFListItems.add(StandartListItem(title: S.current.old_fee, value: tx.feeFormatted() ?? '0.0'));
361
362
+ if (transactionInfo.fee != null && rawTransaction.isNotEmpty) {
363
+ final size = bitcoin!.getTransactionVSize(wallet, rawTransaction);
364
+ final recommendedRate = (transactionInfo.fee! / size).round() + 1;
365
+
366
+ RBFListItems.add(
367
+ StandartListItem(title: 'New recommended fee rate', value: '$recommendedRate sat/byte'));
368
+ }
369
+
370
final priorities = priorityForWalletType(wallet.type);
371
final selectedItem = priorities.indexOf(sendViewModel.transactionPriority);
372
final customItem = priorities
@@ -429,8 +435,9 @@ abstract class TransactionDetailsViewModelBase with Store {
435
Future<void> _checkForRBF(TransactionInfo tx) async {
436
if (wallet.type == WalletType.bitcoin &&
437
transactionInfo.direction == TransactionDirection.outgoing) {
432
- if (await bitcoin!.canReplaceByFee(wallet, tx)) {
433
- _canReplaceByFee = true;
438
+ rawTransaction = await bitcoin!.canReplaceByFee(wallet, tx);
439
+ if (rawTransaction != null) {
440
+ canReplaceByFee = true;
441
}
442
}
443
}
tool/configure.dart
+2
-1
@@ -205,7 +205,8 @@ abstract class Bitcoin {
205
bool isTestnet(Object wallet);
206
207
Future<PendingTransaction> replaceByFee(Object wallet, String transactionHash, String fee);
208
- Future<bool> canReplaceByFee(Object wallet, Object tx);
208
+ Future<String?> canReplaceByFee(Object wallet, Object tx);
209
+ int getTransactionVSize(Object wallet, String txHex);
210
Future<bool> isChangeSufficientForFee(Object wallet, String txId, String newFee);
211
int getFeeAmountForPriority(Object wallet, TransactionPriority priority, int inputsCount, int outputsCount, {int? size});
212
int getEstimatedFeeWithFeeRate(Object wallet, int feeRate, int? amount,