dev
dart 640 lines 24 KB
Raw
1 import "package:cake_wallet/bitcoin/bitcoin.dart";
2 import "package:cake_wallet/core/address_validator.dart";
3 import "package:cake_wallet/entities/priority_for_wallet_type.dart";
4 import "package:cake_wallet/entities/transaction_description.dart";
5 import "package:cake_wallet/evm/evm.dart";
6 import "package:cake_wallet/generated/i18n.dart";
7 import "package:cake_wallet/monero/monero.dart";
8 import "package:cake_wallet/reactions/wallet_connect.dart";
9 import "package:cake_wallet/solana/solana.dart";
10 import "package:cake_wallet/src/screens/transaction_details/address_list_item.dart";
11 import "package:cake_wallet/src/screens/transaction_details/confirmations_list_item.dart";
12 import "package:cake_wallet/src/screens/transaction_details/rbf_details_list_fee_picker_item.dart";
13 import "package:cake_wallet/src/screens/transaction_details/standart_list_item.dart";
14 import "package:cake_wallet/src/screens/transaction_details/transaction_details_list_item.dart";
15 import "package:cake_wallet/src/screens/transaction_details/transaction_expandable_list_item.dart";
16 import "package:cake_wallet/store/app_store.dart";
17 import "package:cake_wallet/tron/tron.dart";
18 import "package:cake_wallet/view_model/send/send_view_model.dart";
19 import "package:cake_wallet/zano/zano.dart";
20 import "package:collection/collection.dart";
21 import "package:cw_core/crypto_currency.dart";
22 import "package:cw_core/currency_for_wallet_type.dart";
23 import "package:cw_core/transaction_direction.dart";
24 import "package:cw_core/transaction_info.dart";
25 import "package:cw_core/transaction_priority.dart";
26 import "package:cw_core/wallet_base.dart";
27 import "package:cw_core/wallet_type.dart";
28 import "package:flutter/foundation.dart";
29 import "package:hive/hive.dart";
30 import "package:intl/intl.dart";
31 import "package:mobx/mobx.dart";
32 import "package:url_launcher/url_launcher.dart";
33
34 part "transaction_details_view_model.g.dart";
35
36 bool _trueFunc(_) => true;
37
38 /// We're adding a regex here so we can remove any already saved address that has the account in it.
39 /// In the refactor, we will make another separate variable for accounts and the UI would handle it as needed.
40 String _moneroRecipientAddressForDisplay(String raw, WalletType walletType) {
41 if (walletType != WalletType.monero || raw.isEmpty) {
42 return raw;
43 }
44
45 final compact = raw.replaceAll(RegExp(r"\s"), "");
46 final match = RegExp(
47 r"4[0-9a-zA-Z]{94}|8[0-9a-zA-Z]{94}|[0-9a-zA-Z]{106}",
48 caseSensitive: false,
49 ).firstMatch(compact);
50 return match?.group(0) ?? raw.trim();
51 }
52
53 bool isLightning(TransactionInfo tx) => (tx.additionalInfo["isLightning"] as bool?) ?? false;
54
55 bool hasLightningPreimage(TransactionInfo tx) => (tx.additionalInfo["preimage"] as String?) != null;
56
57 class TxDetailRowDefinition {
58 TxDetailRowDefinition({
59 required this.keyString,
60 required this.title,
61 required this.valueGetter,
62 this.applicable = _trueFunc,
63 this.listItemBuilder = StandartListItem.new,
64 });
65
66 final String keyString;
67 final String title;
68 final String Function(TransactionDetailsViewModelBase) valueGetter;
69 final bool Function(TransactionDetailsViewModelBase) applicable;
70 final dynamic Function({
71 required String title,
72 required String value,
73 required Key key,
74 }) listItemBuilder;
75
76 static final List<TxDetailRowDefinition> defs = [
77 TxDetailRowDefinition(
78 keyString: "standard_list_item_transaction_details_date_key",
79 title: S.current.transaction_details_date,
80 valueGetter: (vm) => DateFormat("d MMMM yyyy, HH:mm", vm._appStore.settingsStore.languageCode)
81 .format(vm.transactionInfo.date),
82 ),
83 TxDetailRowDefinition(
84 keyString: "standard_list_item_transaction_details_height_key",
85 title: S.current.transaction_details_height,
86 valueGetter: (vm) => vm.transactionInfo.height?.toString() ?? "",
87 applicable: (vm) =>
88 ![WalletType.solana, WalletType.tron].contains(vm.wallet.type) ||
89 !isLightning(vm.transactionInfo),
90 ),
91 TxDetailRowDefinition(
92 keyString: "standard_list_item_transaction_details_fee_key",
93 title: S.current.transaction_details_fee,
94 valueGetter: (vm) => vm.feeAmount,
95 applicable: (vm) =>
96 vm.wallet.type != WalletType.nano &&
97 (vm.transactionInfo.fee?.toStringWithSymbol() ?? "").isNotEmpty,
98 ),
99 TxDetailRowDefinition(
100 keyString: "standard_list_item_transaction_confirmations_key",
101 title: S.current.confirmations,
102 valueGetter: (vm) => "${vm.transactionInfo.confirmations}/${vm.neededConfirmations}",
103 applicable: (vm) =>
104 [...electrumWalletTypes, ...evmWalletTypes, WalletType.zcash, WalletType.monero]
105 .contains(vm.wallet.type) &&
106 !isLightning(vm.transactionInfo),
107 listItemBuilder: ConfirmationsListItem.new,
108 ),
109 TxDetailRowDefinition(
110 keyString: "standard_list_item_transaction_details_recipient_address_key",
111 title: S.current.transaction_details_recipient_address,
112 valueGetter: (vm) {
113 String? ret;
114
115 switch (vm.wallet.type) {
116 case WalletType.monero:
117 if (vm.transactionInfo.direction == TransactionDirection.incoming) {
118 ret = monero!.getTransactionAddress(
119 vm.wallet,
120 vm.transactionInfo.additionalInfo["accountIndex"] as int,
121 vm.transactionInfo.additionalInfo["addressIndex"] as int,
122 );
123 }
124 case WalletType.bitcoin:
125 ret = (bitcoin!.getTransactionAddresses(vm.wallet, vm.transactionInfo) ?? [])
126 .firstOrNull ??
127 "";
128 case WalletType.tron:
129 if (vm.transactionInfo.to != null) {
130 ret = tron!.getTronBase58Address(vm.transactionInfo.to!, vm.wallet);
131 }
132 default:
133 break;
134 }
135 ret ??= vm.transactionInfo.to ?? "";
136
137 final resolvedAddress = _moneroRecipientAddressForDisplay(ret, vm.wallet.type);
138 vm.isRecipientAddressShown = resolvedAddress.isNotEmpty;
139 return resolvedAddress;
140 },
141 applicable: (vm) =>
142 vm.showRecipientAddress &&
143 (vm.transactionInfo.to != null ||
144 [WalletType.monero, WalletType.tron].contains(vm.wallet.type) ||
145 vm.wallet.type == WalletType.bitcoin &&
146 vm.transactionInfo.direction == TransactionDirection.incoming),
147 listItemBuilder: AddressListItem.new,
148 ),
149 TxDetailRowDefinition(
150 keyString: "standard_list_item_transaction_details_source_address_key",
151 title: S.current.transaction_details_source_address,
152 valueGetter: (vm) {
153 switch (vm.wallet.type) {
154 case WalletType.tron:
155 return tron!.getTronBase58Address(vm.transactionInfo.from!, vm.wallet);
156 default:
157 return vm.transactionInfo.from!;
158 }
159 },
160 applicable: (vm) => vm.transactionInfo.from != null,
161 listItemBuilder: AddressListItem.new,
162 ),
163 TxDetailRowDefinition(
164 keyString: "standard_list_item_address_label_key",
165 title: S.current.address_label,
166 valueGetter: (vm) => monero!.getSubaddressLabel(
167 vm.wallet,
168 vm.transactionInfo.additionalInfo["accountIndex"] as int,
169 vm.transactionInfo.additionalInfo["addressIndex"] as int,
170 ),
171 applicable: (vm) => vm.wallet.type == WalletType.monero,
172 ),
173 TxDetailRowDefinition(
174 keyString: "standard_list_item_transaction_key",
175 title: S.current.transaction_key,
176 valueGetter: (vm) {
177 final descriptionKey =
178 "${vm.transactionInfo.txHash}_${vm.wallet.walletAddresses.primaryAddress}";
179
180 final description = vm.transactionDescriptionBox.values.firstWhere(
181 (val) => val.id == descriptionKey || val.id == vm.transactionInfo.txHash,
182 orElse: () => TransactionDescription(id: descriptionKey),
183 );
184 return vm.transactionInfo.additionalInfo["key"] as String? ??
185 description.transactionKey ??
186 "";
187 },
188 applicable: (vm) => vm.wallet.type == WalletType.monero,
189 ),
190 TxDetailRowDefinition(
191 keyString: "standard_list_item_lightning_preimage",
192 title: S.current.transaction_preimage,
193 valueGetter: (vm) => vm.transactionInfo.additionalInfo["preimage"] as String? ?? "",
194 applicable: (vm) =>
195 hasLightningPreimage(vm.transactionInfo) && isLightning(vm.transactionInfo),
196 ),
197 TxDetailRowDefinition(
198 keyString: "standard_list_item_transaction_confirmed_key",
199 title: S.current.confirmed_tx,
200 valueGetter: (vm) => (vm.transactionInfo.confirmations > 0).toString(),
201 applicable: (vm) => vm.wallet.type == WalletType.nano,
202 ),
203 TxDetailRowDefinition(
204 keyString: "standard_list_item_transaction_details_memo_key",
205 title: S.current.memo,
206 valueGetter: (vm) => vm.transactionInfo.additionalInfo["memo"] as String,
207 applicable: (vm) =>
208 vm.wallet.type == WalletType.zcash && vm.transactionInfo.additionalInfo["memo"] != null,
209 ),
210 TxDetailRowDefinition(
211 keyString: "standard_list_item_transaction_details_asset_id_key",
212 title: "Asset ID",
213 valueGetter: (vm) =>
214 vm.transactionInfo.additionalInfo["assetId"] as String? ?? "Unknown asset id",
215 applicable: (vm) => vm.wallet.type == WalletType.zano,
216 ),
217 TxDetailRowDefinition(
218 keyString: "standard_list_item_transaction_details_comment_key",
219 title: S.current.transaction_details_title,
220 valueGetter: (vm) => vm.transactionInfo.additionalInfo["comment"] as String? ?? "",
221 applicable: (vm) => vm.wallet.type == WalletType.zano,
222 ),
223 TxDetailRowDefinition(
224 keyString: "standard_list_item_transaction_details_id_key",
225 title: S.current.transaction_details_transaction_id,
226 valueGetter: (vm) => vm.transactionInfo.txHash,
227 ),
228 ];
229 }
230
231 class TransactionDetailsViewModel = TransactionDetailsViewModelBase
232 with _$TransactionDetailsViewModel;
233
234 abstract class TransactionDetailsViewModelBase with Store {
235 TransactionDetailsViewModelBase({
236 required this.transactionInfo,
237 required this.transactionDescriptionBox,
238 required this.wallet,
239 required AppStore appStore,
240 required this.sendViewModel,
241 this.canReplaceByFee = false,
242 }) : items = [],
243 rbfListItems = [],
244 newFee = 0,
245 isRecipientAddressShown = false,
246 _appStore = appStore,
247 showRecipientAddress = appStore.settingsStore.shouldSaveRecipientAddress {
248 final tx = transactionInfo;
249
250 for (final def in TxDetailRowDefinition.defs) {
251 if (def.applicable(this)) {
252 items.add(
253 def.listItemBuilder(
254 title: def.title,
255 value: def.valueGetter(this),
256 key: ValueKey(def.keyString),
257 ) as TransactionDetailsListItem,
258 );
259 }
260 }
261
262 _checkForRBF(tx);
263
264 final descriptionKey = "${transactionInfo.txHash}_${wallet.walletAddresses.primaryAddress}";
265 final description = transactionDescriptionBox.values.firstWhere(
266 (val) => val.id == descriptionKey || val.id == transactionInfo.txHash,
267 orElse: () => TransactionDescription(id: descriptionKey),
268 );
269
270 if (showRecipientAddress && !isRecipientAddressShown) {
271 final recipientAddress = description.recipientAddress;
272
273 if (recipientAddress?.isNotEmpty ?? false) {
274 final recipientAddressForDisplay =
275 _moneroRecipientAddressForDisplay(recipientAddress!, wallet.type);
276 items.add(
277 AddressListItem(
278 title: S.current.transaction_details_recipient_address,
279 value: recipientAddressForDisplay,
280 key: ValueKey("standard_list_item_${recipientAddressForDisplay}_key"),
281 ),
282 );
283 }
284 }
285 }
286
287 void updateNote(String note) {
288 final descriptionKey = "${transactionInfo.txHash}_${wallet.walletAddresses.primaryAddress}";
289 final description = transactionDescriptionBox.values.firstWhere(
290 (val) => val.id == descriptionKey || val.id == transactionInfo.txHash,
291 orElse: () => TransactionDescription(id: descriptionKey),
292 );
293
294 description.transactionNote = note;
295
296 if (description.isInBox) {
297 description.save();
298 } else {
299 transactionDescriptionBox.add(description);
300 }
301 }
302
303 String get note {
304 final descriptionKey = "${transactionInfo.txHash}_${wallet.walletAddresses.primaryAddress}";
305 final description = transactionDescriptionBox.values
306 .firstWhereOrNull((val) => val.id == descriptionKey || val.id == transactionInfo.txHash,
307 );
308 return description?.transactionNote ?? "";
309 }
310
311 final TransactionInfo transactionInfo;
312 final Box<TransactionDescription> transactionDescriptionBox;
313 final WalletBase wallet;
314 final SendViewModel sendViewModel;
315 final AppStore _appStore;
316
317 final List<TransactionDetailsListItem> items;
318 final List<TransactionDetailsListItem> rbfListItems;
319 bool showRecipientAddress;
320 bool isRecipientAddressShown;
321 int newFee;
322 String? rawTransaction;
323 TransactionPriority? transactionPriority;
324
325 CryptoCurrency get transactionAsset {
326 if (isEVMCompatibleChain(wallet.type)) {
327 return evm!.assetOfTransaction(wallet, transactionInfo);
328 }
329
330 if (isLightning(transactionInfo)) {
331 return CryptoCurrency.btcln;
332 }
333
334 return switch (wallet.type) {
335 WalletType.solana => solana!.assetOfTransaction(wallet, transactionInfo),
336 WalletType.tron => tron!.assetOfTransaction(wallet, transactionInfo),
337 WalletType.zano => zano!.assetOfTransaction(wallet, transactionInfo) ?? CryptoCurrency.zano,
338 _ => walletTypeToCryptoCurrency(wallet.type)
339 };
340 }
341
342 @computed
343 String get transactionAmount =>
344 _appStore.amountParsingProxy.asDisplayStringWithSymbol(transactionInfo.amount);
345
346 @computed
347 String get feeAmount =>
348 _appStore.amountParsingProxy.asDisplayStringWithSymbol(transactionInfo.fee!);
349
350 @computed
351 String get transactionCopyAmount =>
352 _appStore.amountParsingProxy.asDisplayString(transactionInfo.amount);
353
354 // TODO(malik1004x): integrate these getters with the TransactionInfo object
355 String get formattedPendingStatus {
356 switch (wallet.type) {
357 case WalletType.monero:
358 case WalletType.haven:
359 case WalletType.zano:
360 if (transactionInfo.confirmations >= 0 && transactionInfo.confirmations < 10) {
361 return " (${transactionInfo.confirmations}/10)";
362 }
363 break;
364 case WalletType.wownero:
365 if (transactionInfo.confirmations >= 0 && transactionInfo.confirmations < 3) {
366 return " (${transactionInfo.confirmations}/3)";
367 }
368 break;
369 case WalletType.litecoin:
370 final isPegIn = (transactionInfo.additionalInfo["isPegIn"] as bool?) ?? false;
371 final isPegOut = (transactionInfo.additionalInfo["isPegOut"] as bool?) ?? false;
372 final fromPegOut = (transactionInfo.additionalInfo["fromPegOut"] as bool?) ?? false;
373 String str = "";
374 if (transactionInfo.confirmations <= 0) {
375 str = S.current.pending;
376 }
377 if ((isPegOut || fromPegOut) &&
378 transactionInfo.confirmations >= 0 &&
379 transactionInfo.confirmations < 6) {
380 str = " (${transactionInfo.confirmations}/6)";
381 }
382 if (isPegIn) {
383 str += " (Peg In)";
384 }
385 if (isPegOut) {
386 str += " (Peg Out)";
387 }
388 return str;
389 default:
390 return "";
391 }
392
393 return "";
394 }
395
396 String get formattedStatus {
397 if ([
398 WalletType.monero,
399 WalletType.haven,
400 WalletType.wownero,
401 WalletType.litecoin,
402 WalletType.zano,
403 ].contains(wallet.type)) {
404 return formattedPendingStatus;
405 }
406
407 return transactionInfo.isPending ? S.current.pending : "";
408 }
409
410 int get neededConfirmations {
411 switch (wallet.type) {
412 case WalletType.monero:
413 case WalletType.haven:
414 case WalletType.zano:
415 return 10;
416 case WalletType.wownero:
417 return 3;
418 case WalletType.litecoin:
419 final isPegOut = (transactionInfo.additionalInfo["isPegOut"] as bool?) ?? false;
420 final fromPegOut = (transactionInfo.additionalInfo["fromPegOut"] as bool?) ?? false;
421 if (isPegOut || fromPegOut) {
422 return 6;
423 }
424 default:
425 return 0;
426 }
427 return 0;
428 }
429
430 String get formattedTitle {
431 if (transactionInfo.additionalInfo['isIronwoodMigration'] == true) {
432 return 'Migration';
433 }
434 if (transactionInfo.additionalInfo['isAutoShield'] == true) {
435 return S.current.shielding;
436 }
437 if (transactionInfo.direction == TransactionDirection.incoming) {
438 return S.current.received;
439 }
440
441 return S.current.sent;
442 }
443
444 @observable
445 bool canReplaceByFee;
446
447 String get _explorerUrl {
448 final txId = transactionInfo.txHash;
449 if (wallet.chainId != null) {
450 final explorerUrl = evm!.getExplorerUrlForChainId(wallet.chainId!);
451 if (explorerUrl != null) {
452 return "$explorerUrl/tx/${txId}";
453 }
454 }
455
456 switch (wallet.type) {
457 case WalletType.monero:
458 return "https://monero.com/tx/${txId}";
459 case WalletType.bitcoin:
460 return isLightning(transactionInfo)
461 ? "https://sparkscan.io/tx/${txId}"
462 : 'https://mempool.cakewallet.com/${wallet.isTestnet ? "testnet/" : ""}tx/${txId}';
463 case WalletType.litecoin:
464 return bitcoin!.txIsMweb(transactionInfo)
465 ? "https://www.mwebexplorer.com/blocks/block/${transactionInfo.height}"
466 : "https://blockchair.com/litecoin/transaction/${txId}";
467 case WalletType.bitcoinCash:
468 return "https://blockchair.com/bitcoin-cash/transaction/${txId}";
469 case WalletType.haven:
470 return "https://explorer.havenprotocol.org/search?value=${txId}";
471 case WalletType.ethereum:
472 return "https://etherscan.io/tx/${txId}";
473 case WalletType.base:
474 return "https://basescan.org/tx/${txId}";
475 case WalletType.arbitrum:
476 return "https://arbiscan.io/tx/${txId}";
477 case WalletType.bsc:
478 return "https://bscscan.com/tx/${txId}";
479 case WalletType.polygon:
480 return "https://polygonscan.com/tx/${txId}";
481 case WalletType.nano:
482 return "https://nanexplorer.com/nano/block/${txId}";
483 case WalletType.banano:
484 return "https://nanexplorer.com/banano/block/${txId}";
485 case WalletType.solana:
486 return "https://solscan.io/tx/${txId}";
487 case WalletType.tron:
488 return "https://tronscan.org/#/transaction/${txId}";
489 case WalletType.wownero:
490 return "https://explore.wownero.com/tx/${txId}";
491 case WalletType.zano:
492 return "https://explorer.zano.org/transaction/${txId}";
493 case WalletType.decred:
494 return 'https://${wallet.isTestnet ? "testnet" : "dcrdata"}.decred.org/tx/${txId.split(':')[0]}';
495 case WalletType.dogecoin:
496 return "https://blockchair.com/dogecoin/transaction/${txId}";
497 case WalletType.zcash:
498 return "https://blockchair.com/zcash/transaction/${txId}";
499 case WalletType.none:
500 return "";
501 }
502 }
503
504 String get explorerDescription => S.current.view_transaction_on + Uri.parse(_explorerUrl).host;
505
506 void launchExplorer() {
507 launchUrl(Uri.parse(_explorerUrl));
508 }
509
510 void addBumpFeesListItems(TransactionInfo tx, String rawTransaction) {
511 transactionPriority = bitcoin!.getBitcoinTransactionPriorityMedium();
512 final inputsCount = (transactionInfo.inputAddresses?.isEmpty ?? true)
513 ? 1
514 : transactionInfo.inputAddresses!.length;
515 final outputsCount = (transactionInfo.outputAddresses?.isEmpty ?? true)
516 ? 1
517 : transactionInfo.outputAddresses!.length;
518
519 newFee = bitcoin!.getFeeAmountForPriority(
520 wallet,
521 bitcoin!.getBitcoinTransactionPriorityMedium(),
522 inputsCount,
523 outputsCount,
524 );
525
526 rbfListItems.add(
527 StandartListItem(
528 title: S.current.old_fee,
529 value: tx.fee?.toStringWithSymbol() ?? "0.0",
530 key: const ValueKey("standard_list_item_rbf_old_fee_key"),
531 ),
532 );
533
534 if (transactionInfo.fee != null && rawTransaction.isNotEmpty) {
535 final size = bitcoin!.getTransactionVSize(wallet, rawTransaction);
536 final recommendedRate = (transactionInfo.fee! / BigInt.from(size)) +
537 transactionInfo.fee!.copyWith(amount: BigInt.one);
538
539 rbfListItems.add(
540 StandartListItem(title: "New recommended fee rate", value: "$recommendedRate sat/byte"),
541 );
542 }
543
544 final priorities = priorityForWalletType(wallet.type);
545 final selectedItem = priorities.indexOf(sendViewModel.feesViewModel.transactionPriority);
546 final customItem = priorities.firstWhereOrNull(
547 (element) => element == sendViewModel.feesViewModel.bitcoinTransactionPriorityCustom,
548 );
549 final customItemIndex = customItem != null ? priorities.indexOf(customItem) : null;
550 final maxCustomFeeRate = sendViewModel.feesViewModel.maxCustomFeeRate?.toDouble();
551
552 rbfListItems.add(
553 StandardPickerListItem(
554 key: const ValueKey("standard_picker_list_item_transaction_priorities_key"),
555 title: S.current.estimated_new_fee,
556 value: "${bitcoin!.formatterBitcoinAmountToString(amount: newFee)} ${wallet.currency}",
557 items: priorityForWalletType(wallet.type),
558 customValue: _appStore.settingsStore.customBitcoinFeeRate.toDouble(),
559 maxValue: maxCustomFeeRate,
560 selectedIdx: selectedItem,
561 customItemIndex: customItemIndex ?? 0,
562 displayItem: (dynamic priority, sliderValue) =>
563 sendViewModel.feesViewModel.displayFeeRate(priority, sliderValue.round()),
564 onSliderChanged: (newValue) => setNewFee(value: newValue, priority: transactionPriority!),
565 onItemSelected: (dynamic item, sliderValue) {
566 transactionPriority = item as TransactionPriority;
567 return setNewFee(value: sliderValue, priority: transactionPriority!);
568 },
569 ),
570 );
571
572 if (transactionInfo.inputAddresses != null && transactionInfo.inputAddresses!.isNotEmpty) {
573 rbfListItems.add(
574 StandardExpandableListItem(
575 key: const ValueKey("standard_expandable_list_item_transaction_input_addresses_key"),
576 title: S.current.inputs,
577 expandableItems: transactionInfo.inputAddresses!,
578 ),
579 );
580 }
581
582 if (transactionInfo.outputAddresses != null && transactionInfo.outputAddresses!.isNotEmpty) {
583 final outputAddresses = transactionInfo.outputAddresses!.map((element) {
584 if (element.contains("OP_RETURN:") && element.length > 40) {
585 return "${element.substring(0, 40)}...";
586 }
587 return element;
588 }).toList();
589
590 rbfListItems.add(
591 StandardExpandableListItem(
592 title: S.current.outputs,
593 expandableItems: outputAddresses,
594 key: const ValueKey("standard_expandable_list_item_transaction_output_addresses_key"),
595 ),
596 );
597 }
598 }
599
600 @action
601 Future<void> _checkForRBF(TransactionInfo tx) async {
602 if (wallet.type == WalletType.bitcoin &&
603 transactionInfo.direction == TransactionDirection.outgoing) {
604 final descriptionKey = "${transactionInfo.txHash}_${wallet.walletAddresses.primaryAddress}";
605 final description = transactionDescriptionBox.values
606 .firstWhereOrNull((val) => val.id == descriptionKey || val.id == transactionInfo.txHash);
607
608 if (RegExp(AddressValidator.silentPaymentAddressPatternMainnet)
609 .hasMatch(description?.recipientAddress ?? "")) {
610 canReplaceByFee = false;
611 return;
612 }
613
614 rawTransaction = await bitcoin!.canReplaceByFee(wallet, tx);
615 if (rawTransaction != null) {
616 canReplaceByFee = true;
617 }
618 }
619 }
620
621 String setNewFee({required TransactionPriority priority, double? value}) {
622 newFee = priority == bitcoin!.getBitcoinTransactionPriorityCustom() && value != null
623 ? bitcoin!.feeAmountWithFeeRate(
624 wallet,
625 value.round(),
626 transactionInfo.inputAddresses?.length ?? 1,
627 transactionInfo.outputAddresses?.length ?? 1,
628 )
629 : bitcoin!.getFeeAmountForPriority(
630 wallet,
631 priority,
632 transactionInfo.inputAddresses?.length ?? 1,
633 transactionInfo.outputAddresses?.length ?? 1,
634 );
635
636 return bitcoin!.formatterBitcoinAmountToString(amount: newFee);
637 }
638
639 void replaceByFee(String newFee) => sendViewModel.replaceByFee(transactionInfo, newFee);
640 }