cw-62: Sending ALL should fill in approximate USD value

This commit is contained in:
clear-xmr 2022-03-16 11:50:12 +01:00
parent 987e6f21ee
commit 64898dba01
18 changed files with 216 additions and 84 deletions

View file

@ -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}) {}
}

View file

@ -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<ElectrumBalance,
ElectrumTransactionHistory(walletInfo: walletInfo, password: password);
unspentCoins = [];
_scripthashesUpdateSubject = {};
feeEstimate = ElectrumFeeEstimate(this);
}
static int estimatedTransactionSize(int inputsCount, int outputsCounts) =>
@ -88,6 +91,10 @@ abstract class ElectrumWalletBase extends WalletBase<ElectrumBalance,
@observable
SyncStatus syncStatus;
@override
@observable
ElectrumFeeEstimate feeEstimate;
List<String> get scriptHashes => walletAddresses.addresses
.map((addr) => scriptHash(addr.address, networkType: networkType))
.toList();
@ -346,8 +353,7 @@ abstract class ElectrumWalletBase extends WalletBase<ElectrumBalance,
feeRate(priority) * estimatedTransactionSize(inputsCount, outputsCount);
@override
int calculateEstimatedFee(TransactionPriority priority, int amount,
{int outputsCount}) {
int calculateEstimatedFee(TransactionPriority priority, int amount, {int outputsCount = 1}) {
if (priority is BitcoinTransactionPriority) {
int inputsCount = 0;

View file

@ -0,0 +1,10 @@
import 'package:cw_core/transaction_priority.dart';
abstract class FeeEstimate
{
void update({TransactionPriority priority, int outputsCount});
int get({TransactionPriority priority, int amount, int outputsCount});
void set({TransactionPriority priority, int outputsCount, int fee});
}

View file

@ -1,4 +1,5 @@
import 'package:cw_core/balance.dart';
import 'package:cw_core/fee_estimate.dart';
import 'package:cw_core/transaction_info.dart';
import 'package:cw_core/transaction_priority.dart';
import 'package:cw_core/wallet_addresses.dart';
@ -39,6 +40,8 @@ abstract class WalletBase<
SyncStatus get syncStatus;
FeeEstimate get feeEstimate;
set syncStatus(SyncStatus status);
String get seed;
@ -55,8 +58,6 @@ abstract class WalletBase<
Future<PendingTransaction> createTransaction(Object credentials);
int calculateEstimatedFee(TransactionPriority priority, int amount);
// void fetchTransactionsAsync(
// void Function(TransactionType transaction) onTransactionLoaded,
// {void Function() onFinished});

View file

@ -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<std::pair<std::string, uint64_t>> destinations;
for (int i = 0; i < outputs; i++) {
destinations.push_back({"", 0});
}
auto priority = static_cast<Monero::PendingTransaction::Priority>(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

View file

@ -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);

View file

@ -118,3 +118,5 @@ typedef CloseCurrentWallet = void Function();
typedef OnStartup = void Function();
typedef RescanBlockchainAsync = void Function();
typedef EstimateTransactionFee = int Function(int, int);

View file

@ -112,6 +112,10 @@ final rescanBlockchainAsyncNative = moneroApi
.lookup<NativeFunction<rescan_blockchain>>('rescan_blockchain')
.asFunction<RescanBlockchainAsync>();
final estimateTransactionFeeNative = moneroApi
.lookup<NativeFunction<estimate_transaction_fee>>('estimate_transaction_fee')
.asFunction<EstimateTransactionFee>();
int getSyncingHeight() => getSyncingHeightNative();
bool isNeededToRefresh() => isNeededToRefreshNative() != 0;
@ -327,3 +331,18 @@ Future<bool> isConnected() => compute(_isConnected, 0);
Future<int> 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<int> estimateTransactionFee({int priorityRaw, int outputsCount}) {
return compute(_estimateTransactionFee, {'priorityRaw': priorityRaw, 'outputsCount': outputsCount});
}

View file

@ -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<String, int>();
@observable
ObservableMap<String, int> _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";
}
}

View file

@ -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<MoneroBalance,
monero_wallet.getUnlockedBalance(accountIndex: account.id));
walletAddresses.updateSubaddressList(accountIndex: account.id);
});
feeEstimate = MoneroFeeEstimate();
}
static const int _autoSaveInterval = 30;
@ -67,6 +67,9 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
@observable
MoneroBalance balance;
@override
MoneroFeeEstimate feeEstimate;
@override
String get seed => monero_wallet.getSeed();
@ -221,28 +224,6 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
return PendingMoneroTransaction(pendingTransactionDescription);
}
@override
int calculateEstimatedFee(TransactionPriority priority, int amount) {
// FIXME: hardcoded value;
if (priority is MoneroTransactionPriority) {
switch (priority) {
case MoneroTransactionPriority.slow:
return 24590000;
case MoneroTransactionPriority.regular:
return 123050000;
case MoneroTransactionPriority.medium:
return 245029999;
case MoneroTransactionPriority.fast:
return 614530000;
case MoneroTransactionPriority.fastest:
return 26021600000;
}
}
return 0;
}
@override
Future<void> save() async {
await walletAddresses.updateAddressesInBox();

View file

@ -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';
@ -6,3 +10,15 @@ String formatAmount(String amount) {
}
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;
}

View file

@ -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<SharedPreferences>()

View file

@ -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<Balance, TransactionHistoryBase<TransactionInfo>,
TransactionInfo>
wallet) {
void startWalletSyncStatusChangeReaction(WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo> wallet, SettingsStore settingsStore) {
final _wakeLock = getIt.get<WakeLock>();
_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);
}
});
}

View file

@ -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);
});

View file

@ -386,12 +386,7 @@ class SendCardState extends State<SendCard>
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<SendCard>
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<SendCard>
}
});
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;
}
});

View file

@ -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;

View file

@ -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<void> fetchParsedAddress(BuildContext context) async {
final domain = address;
final ticker = _wallet.currency.title.toLowerCase();

View file

@ -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<Output>()
..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';
}
}
}