Skip to content

Commit

Permalink
rbf-fixes-recomended-fee-rate (#1684)
Browse files Browse the repository at this point in the history
* rbf fixes

* Revert "rbf fixes"

* fix replaced transactions

* re-format electrum_wallet.dart [skip ci]

* minor fixes [skip ci]

---------

Co-authored-by: OmarHatem <[email protected]>
  • Loading branch information
Serhii-Borodenko and OmarHatem28 authored Sep 20, 2024
1 parent 4e2e5e7 commit 32e119e
Show file tree
Hide file tree
Showing 13 changed files with 105 additions and 45 deletions.
9 changes: 8 additions & 1 deletion cw_bitcoin/lib/electrum_transaction_info.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
}
Expand Down Expand Up @@ -98,6 +100,7 @@ class ElectrumTransactionInfo extends TransactionInfo {
id: id,
height: height,
isPending: false,
isReplaced: false,
fee: fee,
direction: direction,
amount: amount,
Expand Down Expand Up @@ -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,
Expand All @@ -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(),
Expand Down Expand Up @@ -238,6 +243,7 @@ class ElectrumTransactionInfo extends TransactionInfo {
direction: direction,
date: date,
isPending: isPending,
isReplaced: isReplaced ?? false,
inputAddresses: inputAddresses,
outputAddresses: outputAddresses,
confirmations: info.confirmations);
Expand All @@ -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;
Expand All @@ -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)';
}
}
19 changes: 15 additions & 4 deletions cw_bitcoin/lib/electrum_wallet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}

Expand Down Expand Up @@ -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();
});
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -2424,6 +2434,7 @@ class EstimatedTxResult {
final int fee;
final int amount;
final bool spendsSilentPayment;

// final bool sendsToSilentPayment;
final bool hasChange;
final bool isSendAll;
Expand Down
1 change: 1 addition & 0 deletions cw_bitcoin/lib/pending_bitcoin_transaction.dart
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ class PendingBitcoinTransaction with PendingTransaction {
direction: TransactionDirection.outgoing,
date: DateTime.now(),
isPending: true,
isReplaced: false,
confirmations: 0,
fee: fee);
}
1 change: 1 addition & 0 deletions cw_core/lib/transaction_info.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
8 changes: 7 additions & 1 deletion lib/bitcoin/cw_bitcoin.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
50 changes: 32 additions & 18 deletions lib/di.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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, _) {
Expand Down Expand Up @@ -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!));
Expand Down
2 changes: 1 addition & 1 deletion lib/router.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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>(
Expand Down
7 changes: 2 additions & 5 deletions lib/src/screens/dashboard/pages/transactions_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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),
Expand Down
11 changes: 8 additions & 3 deletions lib/src/screens/transaction_details/rbf_details_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
},
),
);
Expand Down
10 changes: 10 additions & 0 deletions lib/view_model/dashboard/dashboard_view_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
27 changes: 17 additions & 10 deletions lib/view_model/transaction_details_view_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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:
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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;
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion tool/configure.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down

0 comments on commit 32e119e

Please sign in to comment.