diff --git a/cw_bitcoin/lib/electrum_fee_estimate.dart b/cw_bitcoin/lib/electrum_fee_estimate.dart new file mode 100644 index 000000000..dc621d739 --- /dev/null +++ b/cw_bitcoin/lib/electrum_fee_estimate.dart @@ -0,0 +1,20 @@ +import 'package:cw_bitcoin/electrum_wallet.dart'; +import 'package:cw_core/fee_estimate.dart'; +import 'package:cw_core/transaction_priority.dart'; + +class ElectrumFeeEstimate extends FeeEstimate { + ElectrumFeeEstimate(ElectrumWalletBase wallet) + : _wallet = wallet; + + ElectrumWalletBase _wallet; + + int get({TransactionPriority priority, int amount, int outputsCount}) { + // Electrum doesn't require an async call to obtain the estimated fee. + // We don't bother caching and just obtain it directly. + return _wallet.calculateEstimatedFee(priority,amount, outputsCount: outputsCount); + } + + void update({TransactionPriority priority, int amount, int outputsCount}) {} + + void set({TransactionPriority priority, int outputsCount, int fee}) {} +} \ No newline at end of file diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index ebfb09392..ede5022ea 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:math'; import 'dart:typed_data'; +import 'package:cw_core/fee_estimate.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:hive/hive.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; @@ -33,6 +34,7 @@ import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_bitcoin/electrum.dart'; import 'package:hex/hex.dart'; +import 'package:cw_bitcoin/electrum_fee_estimate.dart'; part 'electrum_wallet.g.dart'; @@ -66,6 +68,7 @@ abstract class ElectrumWalletBase extends WalletBase @@ -88,6 +91,10 @@ abstract class ElectrumWalletBase extends WalletBase get scriptHashes => walletAddresses.addresses .map((addr) => scriptHash(addr.address, networkType: networkType)) .toList(); @@ -346,8 +353,7 @@ abstract class ElectrumWalletBase extends WalletBase createTransaction(Object credentials); - int calculateEstimatedFee(TransactionPriority priority, int amount); - // void fetchTransactionsAsync( // void Function(TransactionType transaction) onTransactionLoaded, // {void Function() onFinished}); diff --git a/cw_monero/ios/Classes/monero_api.cpp b/cw_monero/ios/Classes/monero_api.cpp index 9d52bdfe2..9edd2a97e 100644 --- a/cw_monero/ios/Classes/monero_api.cpp +++ b/cw_monero/ios/Classes/monero_api.cpp @@ -766,6 +766,25 @@ extern "C" return strdup(m_wallet->getTxKey(std::string(txId)).c_str()); } + uint64_t estimate_transaction_fee(int outputs, uint8_t priority_raw) + { + // estimateTransactionFee only cares about the number of outputs + std::vector> destinations; + for (int i = 0; i < outputs; i++) { + destinations.push_back({"", 0}); + } + auto priority = static_cast(priority_raw); + + try { + return m_wallet->estimateTransactionFee(destinations, priority); + } + catch (...) { + // estimateTransactionFee can throw an exception if there is a problem with the + // network request. We must catch it here because exceptions don't propagate to Dart. + return 0; + } + } + #ifdef __cplusplus } #endif diff --git a/cw_monero/lib/api/signatures.dart b/cw_monero/lib/api/signatures.dart index e97003dc0..51f4f87d0 100644 --- a/cw_monero/lib/api/signatures.dart +++ b/cw_monero/lib/api/signatures.dart @@ -120,3 +120,7 @@ typedef close_current_wallet = Void Function(); typedef on_startup = Void Function(); typedef rescan_blockchain = Void Function(); + +typedef estimate_transaction_fee = Int64 Function( + Int32 outputs, + Int8 priorityRaw); \ No newline at end of file diff --git a/cw_monero/lib/api/types.dart b/cw_monero/lib/api/types.dart index d65f2d0d7..d0db85f58 100644 --- a/cw_monero/lib/api/types.dart +++ b/cw_monero/lib/api/types.dart @@ -117,4 +117,6 @@ typedef CloseCurrentWallet = void Function(); typedef OnStartup = void Function(); -typedef RescanBlockchainAsync = void Function(); \ No newline at end of file +typedef RescanBlockchainAsync = void Function(); + +typedef EstimateTransactionFee = int Function(int, int); \ No newline at end of file diff --git a/cw_monero/lib/api/wallet.dart b/cw_monero/lib/api/wallet.dart index 97245990e..0ad29c0e4 100644 --- a/cw_monero/lib/api/wallet.dart +++ b/cw_monero/lib/api/wallet.dart @@ -112,6 +112,10 @@ final rescanBlockchainAsyncNative = moneroApi .lookup>('rescan_blockchain') .asFunction(); +final estimateTransactionFeeNative = moneroApi + .lookup>('estimate_transaction_fee') + .asFunction(); + int getSyncingHeight() => getSyncingHeightNative(); bool isNeededToRefresh() => isNeededToRefreshNative() != 0; @@ -327,3 +331,18 @@ Future isConnected() => compute(_isConnected, 0); Future getNodeHeight() => compute(_getNodeHeight, 0); void rescanBlockchainAsync() => rescanBlockchainAsyncNative(); + +int estimateTransactionFeeSync(int outputs, int priorityRaw) { + return estimateTransactionFeeNative(outputs, priorityRaw); +} + +int _estimateTransactionFee(Map args) { + final priorityRaw = args['priorityRaw'] as int; + final outputsCount = args['outputsCount'] as int; + + return estimateTransactionFeeSync(outputsCount, priorityRaw); +} + +Future estimateTransactionFee({int priorityRaw, int outputsCount}) { + return compute(_estimateTransactionFee, {'priorityRaw': priorityRaw, 'outputsCount': outputsCount}); +} \ No newline at end of file diff --git a/cw_monero/lib/monero_fee_estimate.dart b/cw_monero/lib/monero_fee_estimate.dart new file mode 100644 index 000000000..c32ad3bf2 --- /dev/null +++ b/cw_monero/lib/monero_fee_estimate.dart @@ -0,0 +1,38 @@ +import 'package:mobx/mobx.dart'; +import 'package:cw_core/fee_estimate.dart'; +import 'package:cw_core/transaction_priority.dart'; +import 'package:cw_monero/api/wallet.dart' as monero_wallet; + +part 'monero_fee_estimate.g.dart'; + +class MoneroFeeEstimate = _MoneroFeeEstimate with _$MoneroFeeEstimate; + +abstract class _MoneroFeeEstimate extends FeeEstimate with Store { + _MoneroFeeEstimate() + : _estimatedFee = new ObservableMap(); + + @observable + ObservableMap _estimatedFee; + + @override + void update({TransactionPriority priority, int outputsCount}) { + Future(() async { + final fee = await monero_wallet.estimateTransactionFee(priorityRaw: priority.raw, outputsCount: outputsCount); + set(priority: priority, fee: fee, outputsCount: outputsCount); + }); + } + + @override + int get({TransactionPriority priority, int amount, int outputsCount}) { + return _estimatedFee[_key(priority, outputsCount)] ?? 0; + } + + @override + void set({TransactionPriority priority, int outputsCount, int fee}) { + _estimatedFee[_key(priority, outputsCount)] = fee; + } + + String _key(TransactionPriority priority, int outputsCount) { + return "$priority:$outputsCount"; + } +} diff --git a/cw_monero/lib/monero_wallet.dart b/cw_monero/lib/monero_wallet.dart index 79808c11b..51bbeb43c 100644 --- a/cw_monero/lib/monero_wallet.dart +++ b/cw_monero/lib/monero_wallet.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'package:cw_core/transaction_priority.dart'; import 'package:cw_monero/monero_amount_format.dart'; import 'package:cw_monero/monero_transaction_creation_exception.dart'; import 'package:cw_monero/monero_transaction_info.dart'; @@ -18,6 +17,7 @@ import 'package:cw_monero/monero_transaction_creation_credentials.dart'; import 'package:cw_monero/pending_monero_transaction.dart'; import 'package:cw_monero/monero_wallet_keys.dart'; import 'package:cw_monero/monero_balance.dart'; +import 'package:cw_monero/monero_fee_estimate.dart'; import 'package:cw_monero/monero_transaction_history.dart'; import 'package:cw_monero/account.dart'; import 'package:cw_core/pending_transaction.dart'; @@ -25,7 +25,6 @@ import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/sync_status.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/node.dart'; -import 'package:cw_monero/monero_transaction_priority.dart'; part 'monero_wallet.g.dart'; @@ -52,6 +51,7 @@ abstract class MoneroWalletBase extends WalletBase monero_wallet.getSeed(); @@ -221,28 +224,6 @@ abstract class MoneroWalletBase extends WalletBase save() async { await walletAddresses.updateAddressesInBox(); diff --git a/lib/entities/format_amount.dart b/lib/entities/format_amount.dart index 7dd6c0c8a..50cc92d9f 100644 --- a/lib/entities/format_amount.dart +++ b/lib/entities/format_amount.dart @@ -1,3 +1,7 @@ +import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/monero/monero.dart'; +import 'package:cw_core/wallet_type.dart'; + String formatAmount(String amount) { if ((!amount.contains('.'))&&(!amount.contains(','))) { return amount + '.00'; @@ -5,4 +9,16 @@ String formatAmount(String amount) { return amount + '00'; } return amount; +} + +double formatAmountToDouble({WalletType type, int amount}) { + if (type == WalletType.bitcoin || type == WalletType.litecoin) { + return bitcoin.formatterBitcoinAmountToDouble(amount: amount); + } + + if (type == WalletType.monero) { + return monero.formatterMoneroAmountToDouble(amount: amount); + } + + return 0.0; } \ No newline at end of file diff --git a/lib/reactions/on_current_wallet_change.dart b/lib/reactions/on_current_wallet_change.dart index 842bc761f..581d17158 100644 --- a/lib/reactions/on_current_wallet_change.dart +++ b/lib/reactions/on_current_wallet_change.dart @@ -51,7 +51,7 @@ void startCurrentWalletChangeReaction(AppStore appStore, wallet) async { try { final node = settingsStore.getCurrentNode(wallet.type); - startWalletSyncStatusChangeReaction(wallet); + startWalletSyncStatusChangeReaction(wallet, settingsStore); startCheckConnectionReaction(wallet, settingsStore); await getIt .get() diff --git a/lib/reactions/on_wallet_sync_status_change.dart b/lib/reactions/on_wallet_sync_status_change.dart index bb245898c..69ddba09b 100644 --- a/lib/reactions/on_wallet_sync_status_change.dart +++ b/lib/reactions/on_wallet_sync_status_change.dart @@ -1,5 +1,9 @@ import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/entities/wake_lock.dart'; +import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cw_core/transaction_priority.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:cw_monero/monero_transaction_priority.dart'; import 'package:mobx/mobx.dart'; import 'package:cw_core/transaction_history.dart'; import 'package:cw_core/wallet_base.dart'; @@ -10,10 +14,7 @@ import 'package:flutter/services.dart'; ReactionDisposer _onWalletSyncStatusChangeReaction; -void startWalletSyncStatusChangeReaction( - WalletBase, - TransactionInfo> - wallet) { +void startWalletSyncStatusChangeReaction(WalletBase, TransactionInfo> wallet, SettingsStore settingsStore) { final _wakeLock = getIt.get(); _onWalletSyncStatusChangeReaction?.reaction?.dispose(); _onWalletSyncStatusChangeReaction = @@ -27,5 +28,9 @@ void startWalletSyncStatusChangeReaction( if (status is SyncedSyncStatus || status is FailedSyncStatus) { await _wakeLock.disableWake(); } + + if (status is SyncedSyncStatus) { + wallet.feeEstimate.update(priority: settingsStore.priority[wallet.type], outputsCount: 1); + } }); } diff --git a/lib/src/screens/send/send_page.dart b/lib/src/screens/send/send_page.dart index 8340641a4..3eeea2f6e 100644 --- a/lib/src/screens/send/send_page.dart +++ b/lib/src/screens/send/send_page.dart @@ -261,6 +261,7 @@ class SendPage extends BasePage { child: PrimaryButton( onPressed: () { sendViewModel.addOutput(); + sendViewModel.estimateFee(); Future.delayed(const Duration(milliseconds: 250), () { controller.jumpToPage(sendViewModel.outputs.length - 1); }); diff --git a/lib/src/screens/send/widgets/send_card.dart b/lib/src/screens/send/widgets/send_card.dart index 0e145d9b5..ed255eb32 100644 --- a/lib/src/screens/send/widgets/send_card.dart +++ b/lib/src/screens/send/widgets/send_card.dart @@ -386,12 +386,7 @@ class SendCardState extends State crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( - output - .estimatedFee - .toString() + - ' ' + - sendViewModel - .currency.title, + sendViewModel.estimatedFee.toString() + ' ' + sendViewModel.currency.title, style: TextStyle( fontSize: 12, fontWeight: @@ -403,11 +398,7 @@ class SendCardState extends State padding: EdgeInsets.only(top: 5), child: Text( - output - .estimatedFeeFiatAmount - + ' ' + - sendViewModel - .fiat.title, + sendViewModel.estimatedFeeFiatAmount + ' ' + sendViewModel.fiat.title, style: TextStyle( fontSize: 12, fontWeight: @@ -513,10 +504,16 @@ class SendCardState extends State } }); + reaction((_) => sendViewModel.estimatedFee, (double estimatedFee) { + final firstOutput = sendViewModel.outputs[0]; + if (firstOutput != null && firstOutput.sendAll) { + firstOutput.updateFiatAmount(); + } + }); + reaction((_) => output.sendAll, (bool all) { if (all) { cryptoAmountController.text = S.current.all; - fiatAmountController.text = null; } }); diff --git a/lib/view_model/exchange/exchange_view_model.dart b/lib/view_model/exchange/exchange_view_model.dart index 9e4e8d861..b9ed9ecff 100644 --- a/lib/view_model/exchange/exchange_view_model.dart +++ b/lib/view_model/exchange/exchange_view_model.dart @@ -340,7 +340,7 @@ abstract class ExchangeViewModelBase with Store { if (wallet.type == WalletType.bitcoin) { final availableBalance = wallet.balance.available; final priority = _settingsStore.priority[wallet.type]; - final fee = wallet.calculateEstimatedFee(priority, null); + final fee = wallet.feeEstimate.get(priority: priority); if (availableBalance < fee || availableBalance == 0) { return; diff --git a/lib/view_model/send/output.dart b/lib/view_model/send/output.dart index f8161a55f..ba7fb6eef 100644 --- a/lib/view_model/send/output.dart +++ b/lib/view_model/send/output.dart @@ -1,5 +1,6 @@ +import 'dart:math'; import 'package:cake_wallet/di.dart'; -import 'package:cake_wallet/entities/calculate_fiat_amount_raw.dart'; +import 'package:cake_wallet/entities/format_amount.dart'; import 'package:cake_wallet/entities/parse_address_from_domain.dart'; import 'package:cake_wallet/entities/parsed_address.dart'; import 'package:cake_wallet/src/screens/send/widgets/extract_address_from_parsed.dart'; @@ -92,39 +93,6 @@ abstract class OutputBase with Store { return amount; } - @computed - double get estimatedFee { - try { - final fee = _wallet.calculateEstimatedFee( - _settingsStore.priority[_wallet.type], formattedCryptoAmount); - - if (_wallet.type == WalletType.bitcoin || - _wallet.type == WalletType.litecoin) { - return bitcoin.formatterBitcoinAmountToDouble(amount: fee); - } - - if (_wallet.type == WalletType.monero) { - return monero.formatterMoneroAmountToDouble(amount: fee); - } - } catch (e) { - print(e.toString()); - } - - return 0; - } - - @computed - String get estimatedFeeFiatAmount { - try { - final fiat = calculateFiatAmountRaw( - price: _fiatConversationStore.prices[_wallet.currency], - cryptoAmount: estimatedFee); - return fiat; - } catch (_) { - return '0.00'; - } - } - WalletType get walletType => _wallet.type; final WalletBase _wallet; final SettingsStore _settingsStore; @@ -156,7 +124,7 @@ abstract class OutputBase with Store { } cryptoAmount = amount; - _updateFiatAmount(); + updateFiatAmount(); } @action @@ -166,11 +134,11 @@ abstract class OutputBase with Store { } @action - void _updateFiatAmount() { + void updateFiatAmount() { try { final fiat = calculateFiatAmount( price: _fiatConversationStore.prices[_wallet.currency], - cryptoAmount: cryptoAmount.replaceAll(',', '.')); + cryptoAmount: (cryptoAmount.toUpperCase() == S.current.all) ? _cryptoNumberFormat.format(formatAmountToDouble(type: _wallet.type, amount: _estimateAmountAll())) : cryptoAmount.replaceAll(',', '.')); if (fiatAmount != fiat) { fiatAmount = fiat; } @@ -214,6 +182,11 @@ abstract class OutputBase with Store { _cryptoNumberFormat.maximumFractionDigits = maximumFractionDigits; } + int _estimateAmountAll() { + final fee = _wallet.feeEstimate.get(priority: _settingsStore.priority[_wallet.type], outputsCount: 1); + return max(0, _wallet.balance.available - fee); + } + Future fetchParsedAddress(BuildContext context) async { final domain = address; final ticker = _wallet.currency.title.toLowerCase(); diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index b4ef7eaad..9b22c4d7e 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -1,4 +1,5 @@ import 'package:cake_wallet/entities/balance_display_mode.dart'; +import 'package:cake_wallet/entities/format_amount.dart'; import 'package:cake_wallet/entities/transaction_description.dart'; import 'package:cw_core/transaction_priority.dart'; import 'package:cake_wallet/view_model/send/output.dart'; @@ -24,6 +25,7 @@ import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/view_model/send/send_view_model_state.dart'; import 'package:cake_wallet/entities/parsed_address.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/entities/calculate_fiat_amount_raw.dart'; part 'send_view_model.g.dart'; @@ -46,6 +48,10 @@ abstract class SendViewModelBase with Store { outputs = ObservableList() ..add(Output(_wallet, _settingsStore, _fiatConversationStore)); + + _settingsStore.priority.observe((change) async { + _wallet.feeEstimate.update(priority: change.newValue, outputsCount: outputs.length); + }); } @observable @@ -240,4 +246,38 @@ abstract class SendViewModelBase with Store { bool _isEqualCurrency(String currency) => currency.toLowerCase() == _wallet.currency.title.toLowerCase(); + + void estimateFee() { + _wallet.feeEstimate.update(priority: _settingsStore.priority[_wallet.type], outputsCount: outputs.length); + } + + @computed + double get estimatedFee { + try { + var totalFormattedCryptoAmount = 0; + for (final output in outputs) { + totalFormattedCryptoAmount += output.formattedCryptoAmount; + } + + final fee = _wallet.feeEstimate.get(priority: _settingsStore.priority[_wallet.type], amount: totalFormattedCryptoAmount, outputsCount: outputs.length); + + return formatAmountToDouble(type: _wallet.type, amount: fee); + } catch (e) { + print(e.toString()); + } + + return 0; + } + + @computed + String get estimatedFeeFiatAmount { + try { + final fiat = calculateFiatAmountRaw( + price: _fiatConversationStore.prices[_wallet.currency], + cryptoAmount: this.estimatedFee); + return fiat; + } catch (_) { + return '0.00'; + } + } }