mirror of
https://github.com/cake-tech/cake_wallet.git
synced 2024-12-22 11:39:22 +00:00
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:
parent
4e2e5e708c
commit
32e119e24f
13 changed files with 105 additions and 45 deletions
|
@ -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)';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -110,6 +110,7 @@ class PendingBitcoinTransaction with PendingTransaction {
|
|||
direction: TransactionDirection.outgoing,
|
||||
date: DateTime.now(),
|
||||
isPending: true,
|
||||
isReplaced: false,
|
||||
confirmations: 0,
|
||||
fee: fee);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
50
lib/di.dart
50
lib/di.dart
|
@ -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!));
|
||||
|
|
|
@ -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>(
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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]);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue