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>
This commit is contained in:
Serhii 2024-09-20 23:57:43 +03:00 committed by GitHub
parent 4e2e5e708c
commit 32e119e24f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 105 additions and 45 deletions

View file

@ -35,6 +35,7 @@ class ElectrumTransactionInfo extends TransactionInfo {
List<String>? outputAddresses,
required TransactionDirection direction,
required bool isPending,
required bool isReplaced,
required DateTime date,
required int confirmations,
String? to,
@ -50,6 +51,7 @@ class ElectrumTransactionInfo extends TransactionInfo {
this.direction = direction;
this.date = date;
this.isPending = isPending;
this.isReplaced = isReplaced;
this.confirmations = confirmations;
this.to = to;
}
@ -98,6 +100,7 @@ class ElectrumTransactionInfo extends TransactionInfo {
id: id,
height: height,
isPending: false,
isReplaced: false,
fee: fee,
direction: direction,
amount: amount,
@ -173,6 +176,7 @@ class ElectrumTransactionInfo extends TransactionInfo {
id: bundle.originalTransaction.txId(),
height: height,
isPending: bundle.confirmations == 0,
isReplaced: false,
inputAddresses: inputAddresses,
outputAddresses: outputAddresses,
fee: fee,
@ -196,6 +200,7 @@ class ElectrumTransactionInfo extends TransactionInfo {
direction: parseTransactionDirectionFromInt(data['direction'] as int),
date: DateTime.fromMillisecondsSinceEpoch(data['date'] as int),
isPending: data['isPending'] as bool,
isReplaced: data['isReplaced'] as bool? ?? false,
confirmations: data['confirmations'] as int,
inputAddresses:
inputAddresses.isEmpty ? [] : inputAddresses.map((e) => e.toString()).toList(),
@ -238,6 +243,7 @@ class ElectrumTransactionInfo extends TransactionInfo {
direction: direction,
date: date,
isPending: isPending,
isReplaced: isReplaced ?? false,
inputAddresses: inputAddresses,
outputAddresses: outputAddresses,
confirmations: info.confirmations);
@ -251,6 +257,7 @@ class ElectrumTransactionInfo extends TransactionInfo {
m['direction'] = direction.index;
m['date'] = date.millisecondsSinceEpoch;
m['isPending'] = isPending;
m['isReplaced'] = isReplaced;
m['confirmations'] = confirmations;
m['fee'] = fee;
m['to'] = to;
@ -262,6 +269,6 @@ class ElectrumTransactionInfo extends TransactionInfo {
}
String toString() {
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)';
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)';
}
}

View file

@ -1400,14 +1400,16 @@ abstract class ElectrumWalletBase
}
}
Future<bool> canReplaceByFee(ElectrumTransactionInfo tx) async {
int transactionVSize(String transactionHex) => BtcTransaction.fromRaw(transactionHex).getVSize();
Future<String?> canReplaceByFee(ElectrumTransactionInfo tx) async {
try {
final bundle = await getTransactionExpanded(hash: tx.txHash);
_updateInputsAndOutputs(tx, bundle);
if (bundle.confirmations > 0) return false;
return bundle.originalTransaction.canReplaceByFee;
if (bundle.confirmations > 0) return null;
return bundle.originalTransaction.canReplaceByFee ? bundle.originalTransaction.toHex() : null;
} catch (e) {
return false;
return null;
}
}
@ -1589,6 +1591,13 @@ abstract class ElectrumWalletBase
hasChange: changeOutputs.isNotEmpty,
feeRate: newFee.toString(),
)..addListener((transaction) async {
transactionHistory.transactions.values.forEach((tx) {
if (tx.id == hash) {
tx.isReplaced = true;
tx.isPending = false;
transactionHistory.addOne(tx);
}
});
transactionHistory.addOne(transaction);
await updateBalance();
});
@ -2317,6 +2326,7 @@ Future<void> startRefresh(ScanData scanData) async {
fee: 0,
direction: TransactionDirection.incoming,
isPending: false,
isReplaced: false,
date: scanData.network == BitcoinNetwork.mainnet
? getDateByBitcoinHeight(tweakHeight)
: DateTime.now(),
@ -2424,6 +2434,7 @@ class EstimatedTxResult {
final int fee;
final int amount;
final bool spendsSilentPayment;
// final bool sendsToSilentPayment;
final bool hasChange;
final bool isSendAll;

View file

@ -110,6 +110,7 @@ class PendingBitcoinTransaction with PendingTransaction {
direction: TransactionDirection.outgoing,
date: DateTime.now(),
isPending: true,
isReplaced: false,
confirmations: 0,
fee: fee);
}

View file

@ -18,6 +18,7 @@ abstract class TransactionInfo extends Object with Keyable {
String? to;
String? from;
String? evmSignatureName;
bool? isReplaced;
List<String>? inputAddresses;
List<String>? outputAddresses;

View file

@ -411,12 +411,18 @@ class CWBitcoin extends Bitcoin {
}
@override
Future<bool> canReplaceByFee(Object wallet, Object transactionInfo) async {
Future<String?> canReplaceByFee(Object wallet, Object transactionInfo) async {
final bitcoinWallet = wallet as ElectrumWallet;
final tx = transactionInfo as ElectrumTransactionInfo;
return bitcoinWallet.canReplaceByFee(tx);
}
@override
int getTransactionVSize(Object wallet, String transactionHex) {
final bitcoinWallet = wallet as ElectrumWallet;
return bitcoinWallet.transactionVSize(transactionHex);
}
@override
Future<bool> isChangeSufficientForFee(Object wallet, String txId, String newFee) async {
final bitcoinWallet = wallet as ElectrumWallet;

View file

@ -1080,21 +1080,40 @@ Future<void> setup({
param1: derivations,
)));
getIt.registerFactoryParam<TransactionDetailsViewModel, TransactionInfo, void>(
(TransactionInfo transactionInfo, _) {
final wallet = getIt.get<AppStore>().wallet!;
return TransactionDetailsViewModel(
transactionInfo: transactionInfo,
transactionDescriptionBox: _transactionDescriptionBox,
wallet: wallet,
settingsStore: getIt.get<SettingsStore>(),
sendViewModel: getIt.get<SendViewModel>());
});
getIt.registerFactoryParam<TransactionDetailsViewModel, List<dynamic>, void>(
(params, _) {
final transactionInfo = params[0] as TransactionInfo;
final canReplaceByFee = params[1] as bool? ?? false;
final wallet = getIt.get<AppStore>().wallet!;
return TransactionDetailsViewModel(
transactionInfo: transactionInfo,
transactionDescriptionBox: _transactionDescriptionBox,
wallet: wallet,
settingsStore: getIt.get<SettingsStore>(),
sendViewModel: getIt.get<SendViewModel>(),
canReplaceByFee: canReplaceByFee,
);
}
);
getIt.registerFactoryParam<TransactionDetailsPage, TransactionInfo, void>(
(TransactionInfo transactionInfo, _) => TransactionDetailsPage(
transactionDetailsViewModel:
getIt.get<TransactionDetailsViewModel>(param1: transactionInfo)));
(TransactionInfo transactionInfo, _) => TransactionDetailsPage(
transactionDetailsViewModel: getIt.get<TransactionDetailsViewModel>(
param1: [transactionInfo, false])));
getIt.registerFactoryParam<RBFDetailsPage, List<dynamic>, void>(
(params, _) {
final transactionInfo = params[0] as TransactionInfo;
final txHex = params[1] as String;
return RBFDetailsPage(
transactionDetailsViewModel: getIt.get<TransactionDetailsViewModel>(
param1: [transactionInfo, true],
),
rawTransaction: txHex,
);
}
);
getIt.registerFactoryParam<NewWalletTypePage, NewWalletTypeArguments, void>(
(newWalletTypeArguments, _) {
@ -1265,11 +1284,6 @@ Future<void> setup({
getIt.registerFactory(() => CakePayAccountPage(getIt.get<CakePayAccountViewModel>()));
getIt.registerFactoryParam<RBFDetailsPage, TransactionInfo, void>(
(TransactionInfo transactionInfo, _) => RBFDetailsPage(
transactionDetailsViewModel:
getIt.get<TransactionDetailsViewModel>(param1: transactionInfo)));
getIt.registerFactory(() => AnonPayApi(
useTorOnly: getIt.get<SettingsStore>().exchangeStatus == ExchangeApiMode.torOnly,
wallet: getIt.get<AppStore>().wallet!));

View file

@ -375,7 +375,7 @@ Route<dynamic> createRoute(RouteSettings settings) {
case Routes.bumpFeePage:
return CupertinoPageRoute<void>(
fullscreenDialog: true,
builder: (_) => getIt.get<RBFDetailsPage>(param1: settings.arguments as TransactionInfo));
builder: (_) => getIt.get<RBFDetailsPage>(param1: settings.arguments as List<dynamic>));
case Routes.newSubaddress:
return CupertinoPageRoute<void>(

View file

@ -84,10 +84,7 @@ class TransactionsPage extends StatelessWidget {
}
final transaction = item.transaction;
final transactionType = dashboardViewModel.type == WalletType.ethereum &&
transaction.evmSignatureName == 'approval'
? ' (${transaction.evmSignatureName})'
: '';
final transactionType = dashboardViewModel.getTransactionType(transaction);
return Observer(
builder: (_) => TransactionRow(
@ -102,7 +99,7 @@ class TransactionsPage extends StatelessWidget {
: item.formattedFiatAmount,
isPending: transaction.isPending,
title:
item.formattedTitle + item.formattedStatus + ' $transactionType',
item.formattedTitle + item.formattedStatus + transactionType,
isReceivedSilentPayment:
dashboardViewModel.type == WalletType.bitcoin &&
bitcoin!.txIsReceivedSilentPayment(transaction),

View file

@ -24,19 +24,22 @@ import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:mobx/mobx.dart';
class RBFDetailsPage extends BasePage {
RBFDetailsPage({required this.transactionDetailsViewModel});
RBFDetailsPage({required this.transactionDetailsViewModel, required this.rawTransaction}) {
transactionDetailsViewModel.addBumpFeesListItems(
transactionDetailsViewModel.transactionInfo, rawTransaction);
}
@override
String get title => S.current.bump_fee;
final TransactionDetailsViewModel transactionDetailsViewModel;
final String rawTransaction;
bool _effectsInstalled = false;
@override
Widget body(BuildContext context) {
_setEffects(context);
return Column(
children: [
Expanded(
@ -166,7 +169,9 @@ class RBFDetailsPage extends BasePage {
actionRightButton: () async {
Navigator.of(popupContext).pop();
await transactionDetailsViewModel.sendViewModel.commitTransaction();
// transactionStatePopup();
try {
Navigator.of(popupContext).pop();
} catch (_) {}
},
actionLeftButton: () => Navigator.of(popupContext).pop(),
feeFiatAmount:

View file

@ -75,7 +75,7 @@ class TransactionDetailsPage extends BasePage {
text: S.of(context).bump_fee,
onTap: () async {
Navigator.of(context).pushNamed(Routes.bumpFeePage,
arguments: transactionDetailsViewModel.transactionInfo);
arguments: [transactionDetailsViewModel.transactionInfo, transactionDetailsViewModel.rawTransaction]);
},
),
);

View file

@ -764,6 +764,16 @@ abstract class DashboardViewModelBase with Store {
}
}
String getTransactionType(TransactionInfo tx) {
if (wallet.type == WalletType.bitcoin) {
if (tx.isReplaced == true) return ' (replaced)';
}
if (wallet.type == WalletType.ethereum && tx.evmSignatureName == 'approval')
return ' (${tx.evmSignatureName})';
return '';
}
Future<void> refreshDashboard() async {
reconnect();
}

View file

@ -36,7 +36,8 @@ abstract class TransactionDetailsViewModelBase with Store {
required this.transactionDescriptionBox,
required this.wallet,
required this.settingsStore,
required this.sendViewModel})
required this.sendViewModel,
this.canReplaceByFee = false})
: items = [],
RBFListItems = [],
newFee = 0,
@ -51,8 +52,7 @@ abstract class TransactionDetailsViewModelBase with Store {
break;
case WalletType.bitcoin:
_addElectrumListItems(tx, dateFormat);
_addBumpFeesListItems(tx);
_checkForRBF(tx);
if(!canReplaceByFee)_checkForRBF(tx);
break;
case WalletType.litecoin:
case WalletType.bitcoinCash:
@ -139,13 +139,11 @@ abstract class TransactionDetailsViewModelBase with Store {
bool showRecipientAddress;
bool isRecipientAddressShown;
int newFee;
String? rawTransaction;
TransactionPriority? transactionPriority;
@observable
bool _canReplaceByFee = false;
@computed
bool get canReplaceByFee => _canReplaceByFee /*&& transactionInfo.confirmations <= 0*/;
bool canReplaceByFee;
String _explorerUrl(WalletType type, String txId) {
switch (type) {
@ -347,7 +345,7 @@ abstract class TransactionDetailsViewModelBase with Store {
items.addAll(_items);
}
void _addBumpFeesListItems(TransactionInfo tx) {
void addBumpFeesListItems(TransactionInfo tx, String rawTransaction) {
transactionPriority = bitcoin!.getBitcoinTransactionPriorityMedium();
final inputsCount = (transactionInfo.inputAddresses?.isEmpty ?? true)
? 1
@ -361,6 +359,14 @@ abstract class TransactionDetailsViewModelBase with Store {
RBFListItems.add(StandartListItem(title: S.current.old_fee, value: tx.feeFormatted() ?? '0.0'));
if (transactionInfo.fee != null && rawTransaction.isNotEmpty) {
final size = bitcoin!.getTransactionVSize(wallet, rawTransaction);
final recommendedRate = (transactionInfo.fee! / size).round() + 1;
RBFListItems.add(
StandartListItem(title: 'New recommended fee rate', value: '$recommendedRate sat/byte'));
}
final priorities = priorityForWalletType(wallet.type);
final selectedItem = priorities.indexOf(sendViewModel.transactionPriority);
final customItem = priorities
@ -429,8 +435,9 @@ abstract class TransactionDetailsViewModelBase with Store {
Future<void> _checkForRBF(TransactionInfo tx) async {
if (wallet.type == WalletType.bitcoin &&
transactionInfo.direction == TransactionDirection.outgoing) {
if (await bitcoin!.canReplaceByFee(wallet, tx)) {
_canReplaceByFee = true;
rawTransaction = await bitcoin!.canReplaceByFee(wallet, tx);
if (rawTransaction != null) {
canReplaceByFee = true;
}
}
}

View file

@ -205,7 +205,8 @@ abstract class Bitcoin {
bool isTestnet(Object wallet);
Future<PendingTransaction> replaceByFee(Object wallet, String transactionHash, String fee);
Future<bool> canReplaceByFee(Object wallet, Object tx);
Future<String?> canReplaceByFee(Object wallet, Object tx);
int getTransactionVSize(Object wallet, String txHex);
Future<bool> isChangeSufficientForFee(Object wallet, String txId, String newFee);
int getFeeAmountForPriority(Object wallet, TransactionPriority priority, int inputsCount, int outputsCount, {int? size});
int getEstimatedFeeWithFeeRate(Object wallet, int feeRate, int? amount,