diff --git a/lib/bitcoin/bitcoin_transaction_credentials.dart b/lib/bitcoin/bitcoin_transaction_credentials.dart index 0c977fef8..49a6bebc9 100644 --- a/lib/bitcoin/bitcoin_transaction_credentials.dart +++ b/lib/bitcoin/bitcoin_transaction_credentials.dart @@ -1,9 +1,9 @@ import 'package:cake_wallet/bitcoin/bitcoin_transaction_priority.dart'; +import 'package:cake_wallet/view_model/send/send_item.dart'; class BitcoinTransactionCredentials { - BitcoinTransactionCredentials(this.address, this.amount, this.priority); + BitcoinTransactionCredentials(this.sendItemList, this.priority); - final String address; - final String amount; + final List sendItemList; BitcoinTransactionPriority priority; } diff --git a/lib/bitcoin/electrum_wallet.dart b/lib/bitcoin/electrum_wallet.dart index bf3dfd542..13913c41b 100644 --- a/lib/bitcoin/electrum_wallet.dart +++ b/lib/bitcoin/electrum_wallet.dart @@ -218,20 +218,71 @@ abstract class ElectrumWalletBase extends WalletBase[]; + final sendItemList = transactionCredentials.sendItemList; final allAmountFee = calculateEstimatedFee(transactionCredentials.priority, null); final allAmount = balance.confirmed - allAmountFee; + var credentialsAmount = 0; + var amount = 0; var fee = 0; - final credentialsAmount = transactionCredentials.amount != null - ? stringDoubleToBitcoinAmount(transactionCredentials.amount) - : 0; - final amount = transactionCredentials.amount == null || - allAmount - credentialsAmount < minAmount - ? allAmount - : credentialsAmount; + + if (sendItemList.length > 1) { + final sendAllItems = sendItemList.where((item) => item.sendAll).toList(); + + if (sendAllItems?.isNotEmpty ?? false) { + throw BitcoinTransactionWrongBalanceException(); + } + + final nullAmountItems = sendItemList.where((item) => + stringDoubleToBitcoinAmount(item.cryptoAmount.replaceAll(',', '.')) <= 0) + .toList(); + + if (nullAmountItems?.isNotEmpty ?? false) { + throw BitcoinTransactionWrongBalanceException(); + } + + credentialsAmount = sendItemList.fold(0, (previousValue, element) => + previousValue + stringDoubleToBitcoinAmount( + element.cryptoAmount.replaceAll(',', '.'))); + + amount = allAmount - credentialsAmount < minAmount + ? allAmount + : credentialsAmount; + + fee = amount == allAmount + ? allAmountFee + : calculateEstimatedFee(transactionCredentials.priority, amount, + outputsCount: sendItemList.length + 1); + } else { + final sendItem = sendItemList.first; + + credentialsAmount = !sendItem.sendAll + ? stringDoubleToBitcoinAmount( + sendItem.cryptoAmount.replaceAll(',', '.')) + : 0; + + amount = sendItem.sendAll || allAmount - credentialsAmount < minAmount + ? allAmount + : credentialsAmount; + + fee = sendItem.sendAll || amount == allAmount + ? allAmountFee + : calculateEstimatedFee(transactionCredentials.priority, amount); + } + + if (fee == 0) { + throw BitcoinTransactionWrongBalanceException(); + } + + final totalAmount = amount + fee; + + if (totalAmount > balance.confirmed) { + throw BitcoinTransactionWrongBalanceException(); + } + final txb = bitcoin.TransactionBuilder(network: networkType); final changeAddress = address; - var leftAmount = amount; + var leftAmount = totalAmount; var totalInputAmount = 0; if (_unspent.isEmpty) { @@ -252,16 +303,6 @@ abstract class ElectrumWalletBase extends WalletBase balance.confirmed) { - throw BitcoinTransactionWrongBalanceException(); - } - if (amount <= 0 || totalInputAmount < amount) { throw BitcoinTransactionWrongBalanceException(); } @@ -282,11 +323,18 @@ abstract class ElectrumWalletBase extends WalletBase { if (state is ExecutedSuccessfullyState) { WidgetsBinding.instance.addPostFrameCallback((_) { - final item = widget.exchangeTradeViewModel.sendViewModel - .sendItemList.first; - showPopUp( context: context, builder: (BuildContext context) { @@ -388,8 +385,8 @@ class ExchangeTradeState extends State { .pendingTransactionFiatAmount + ' ' + widget.exchangeTradeViewModel.sendViewModel.fiat.title, - recipientTitle: S.of(context).recipient_address, - recipientAddress: item.address); + sendItemList: widget.exchangeTradeViewModel.sendViewModel + .sendItemList); }); }); } diff --git a/lib/src/screens/send/send_page.dart b/lib/src/screens/send/send_page.dart index 85c5e6856..42eddec1a 100644 --- a/lib/src/screens/send/send_page.dart +++ b/lib/src/screens/send/send_page.dart @@ -266,56 +266,49 @@ class SendPage extends BasePage { EdgeInsets.only(left: 24, right: 24, bottom: 24), bottomSection: Column( children: [ - PrimaryButton( - onPressed: () { - sendViewModel.addSendItem(); - }, - text: 'Add receiver', - color: Colors.green, - textColor: Colors.white, - ), - Padding( - padding: EdgeInsets.only(top: 12), - child: Observer(builder: (_) { - return LoadingPrimaryButton( - onPressed: () async { - if (_formKey.currentState.validate()) { - //await sendViewModel.createTransaction(); - // FIXME: for test only - sendViewModel.clearSendItemList(); - await showPopUp( - context: context, - builder: (BuildContext context) { - return AlertWithOneAction( - alertTitle: S.of(context).send, - alertContent: S.of(context).send_success( - sendViewModel.currency - .toString()), - buttonText: S.of(context).ok, - buttonAction: () => Navigator.of(context).pop()); - }); - } else { - await showPopUp( - context: context, - builder: (BuildContext context) { - return AlertWithOneAction( - alertTitle: S.of(context).error, - alertContent: 'Please, check your receivers forms', - buttonText: S.of(context).ok, - buttonAction: () => - Navigator.of(context).pop()); - }); - } - }, - text: S.of(context).send, - color: Theme.of(context).accentTextTheme.body2.color, - textColor: Colors.white, - isLoading: sendViewModel.state is IsExecutingState || - sendViewModel.state is TransactionCommitting, - isDisabled: !sendViewModel.isReadyForSend, - ); + if (sendViewModel.isAddReceiverButtonEnabled) Padding( + padding: EdgeInsets.only(bottom: 12), + child: PrimaryButton( + onPressed: () { + sendViewModel.addSendItem(); }, - )) + text: S.of(context).add_receiver, + color: Colors.green, + textColor: Colors.white, + ) + ), + Observer(builder: (_) { + return LoadingPrimaryButton( + onPressed: () async { + if (!_formKey.currentState.validate()) { + if (sendViewModel.sendItemList.length > 1) { + showErrorValidationAlert(context); + } + + return; + } + + final notValidItems = sendViewModel.sendItemList + .where((item) => + item.address.isEmpty || item.cryptoAmount.isEmpty) + .toList(); + + if (notValidItems?.isNotEmpty ?? false) { + showErrorValidationAlert(context); + return; + } + + await sendViewModel.createTransaction(); + }, + text: S.of(context).send, + color: Theme.of(context).accentTextTheme.body2.color, + textColor: Colors.white, + isLoading: sendViewModel.state is IsExecutingState || + sendViewModel.state is TransactionCommitting, + isDisabled: !sendViewModel.isReadyForSend, + ); + }, + ) ], )), ); @@ -357,8 +350,7 @@ class SendPage extends BasePage { feeValue: sendViewModel.pendingTransaction.feeFormatted, feeFiatAmount: sendViewModel.pendingTransactionFeeFiatAmount + ' ' + sendViewModel.fiat.title, - recipientTitle: S.of(context).recipient_address, - recipientAddress: '', // FIXME: sendViewModel.address, + sendItemList: sendViewModel.sendItemList, rightButtonText: S.of(context).ok, leftButtonText: S.of(context).cancel, actionRightButton: () { @@ -408,4 +400,17 @@ class SendPage extends BasePage { final itemCount = controller.page.round(); return sendViewModel.sendItemList[itemCount]; } + + void showErrorValidationAlert(BuildContext context) async { + await showPopUp( + context: context, + builder: (BuildContext context) { + return AlertWithOneAction( + alertTitle: S.of(context).error, + alertContent: 'Please, check receiver forms', + buttonText: S.of(context).ok, + buttonAction: () => + Navigator.of(context).pop()); + }); + } } diff --git a/lib/src/screens/send/widgets/confirm_sending_alert.dart b/lib/src/screens/send/widgets/confirm_sending_alert.dart index d1c916fc3..cfe44f80d 100644 --- a/lib/src/screens/send/widgets/confirm_sending_alert.dart +++ b/lib/src/screens/send/widgets/confirm_sending_alert.dart @@ -1,6 +1,8 @@ import 'package:cake_wallet/palette.dart'; +import 'package:cake_wallet/view_model/send/send_item.dart'; import 'package:flutter/material.dart'; import 'package:cake_wallet/src/widgets/base_alert_dialog.dart'; +import 'package:cake_wallet/generated/i18n.dart'; class ConfirmSendingAlert extends BaseAlertDialog { ConfirmSendingAlert({ @@ -11,14 +13,18 @@ class ConfirmSendingAlert extends BaseAlertDialog { @required this.fee, @required this.feeValue, @required this.feeFiatAmount, - @required this.recipientTitle, - @required this.recipientAddress, + @required this.sendItemList, @required this.leftButtonText, @required this.rightButtonText, @required this.actionLeftButton, @required this.actionRightButton, this.alertBarrierDismissible = true - }); + }) { + itemCount = sendItemList.length; + recipientTitle = itemCount > 1 + ? S.current.transaction_details_recipient_address + : S.current.recipient_address; + } final String alertTitle; final String amount; @@ -27,14 +33,16 @@ class ConfirmSendingAlert extends BaseAlertDialog { final String fee; final String feeValue; final String feeFiatAmount; - final String recipientTitle; - final String recipientAddress; + final List sendItemList; final String leftButtonText; final String rightButtonText; final VoidCallback actionLeftButton; final VoidCallback actionRightButton; final bool alertBarrierDismissible; + String recipientTitle; + int itemCount; + @override String get titleText => alertTitle; @@ -58,127 +66,181 @@ class ConfirmSendingAlert extends BaseAlertDialog { @override Widget content(BuildContext context) { - return Column( - children: [ - Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, + return Container( + height: 200, + child: SingleChildScrollView( + child: Column( children: [ - Text( - amount, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.normal, - fontFamily: 'Lato', - color: Theme.of(context).primaryTextTheme.title.color, - decoration: TextDecoration.none, - ), - ), - Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ + Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ Text( - amountValue, + amount, style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, + fontSize: 16, + fontWeight: FontWeight.normal, fontFamily: 'Lato', color: Theme.of(context).primaryTextTheme.title.color, decoration: TextDecoration.none, ), ), - Text( - fiatAmountValue, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, - fontFamily: 'Lato', - color: PaletteDark.pigeonBlue, - decoration: TextDecoration.none, - ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + amountValue, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + fontFamily: 'Lato', + color: Theme.of(context).primaryTextTheme.title.color, + decoration: TextDecoration.none, + ), + ), + Text( + fiatAmountValue, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + fontFamily: 'Lato', + color: PaletteDark.pigeonBlue, + decoration: TextDecoration.none, + ), + ) + ], ) ], - ) - ], - ), - Padding( - padding: EdgeInsets.only(top: 16), - child: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - fee, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.normal, - fontFamily: 'Lato', - color: Theme.of(context).primaryTextTheme.title.color, - decoration: TextDecoration.none, - ), - ), - Column( - crossAxisAlignment: CrossAxisAlignment.end, + ), + Padding( + padding: EdgeInsets.only(top: 16), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + fee, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.normal, + fontFamily: 'Lato', + color: Theme.of(context).primaryTextTheme.title.color, + decoration: TextDecoration.none, + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + feeValue, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + fontFamily: 'Lato', + color: Theme.of(context).primaryTextTheme.title.color, + decoration: TextDecoration.none, + ), + ), + Text( + feeFiatAmount, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + fontFamily: 'Lato', + color: PaletteDark.pigeonBlue, + decoration: TextDecoration.none, + ), + ) + ], + ) + ], + ) + ), + Padding( + padding: EdgeInsets.only(top: 16), + child: Column( children: [ Text( - feeValue, + '$recipientTitle:', style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, + fontSize: 16, + fontWeight: FontWeight.normal, fontFamily: 'Lato', color: Theme.of(context).primaryTextTheme.title.color, decoration: TextDecoration.none, ), ), - Text( - feeFiatAmount, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, - fontFamily: 'Lato', - color: PaletteDark.pigeonBlue, - decoration: TextDecoration.none, + itemCount > 1 + ? ListView.builder( + padding: EdgeInsets.only(top: 0), + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemCount: itemCount, + itemBuilder: (context, index) { + final item = sendItemList[index]; + final _address = item.address; + final _amount = + item.cryptoAmount.replaceAll(',', '.'); + + return Column( + children: [ + Padding( + padding: EdgeInsets.only(top: 8), + child: Text( + _address, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + fontFamily: 'Lato', + color: PaletteDark.pigeonBlue, + decoration: TextDecoration.none, + ), + ) + ), + Padding( + padding: EdgeInsets.only(top: 8), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Text( + _amount, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + fontFamily: 'Lato', + color: PaletteDark.pigeonBlue, + decoration: TextDecoration.none, + ), + ) + ], + ) + ) + ], + ); + } + ) + : Padding( + padding: EdgeInsets.only(top: 8), + child: Text( + sendItemList.first.address, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + fontFamily: 'Lato', + color: PaletteDark.pigeonBlue, + decoration: TextDecoration.none, + ), ), ) ], - ) - ], - ) - ), - Padding( - padding: EdgeInsets.fromLTRB(0, 16, 0, 0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - '$recipientTitle:', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.normal, - fontFamily: 'Lato', - color: Theme.of(context).primaryTextTheme.title.color, - decoration: TextDecoration.none, - ), ), - Padding( - padding: EdgeInsets.only(top: 8), - child: Text( - recipientAddress, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, - fontFamily: 'Lato', - color: PaletteDark.pigeonBlue, - decoration: TextDecoration.none, - ), - ) - ) - ], - ), + ) + ], ) - ], + ) ); } } \ No newline at end of file diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 3e7621c19..6c04980ae 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -1,30 +1,20 @@ -import 'package:cake_wallet/bitcoin/bitcoin_amount_format.dart'; import 'package:cake_wallet/bitcoin/bitcoin_transaction_priority.dart'; import 'package:cake_wallet/bitcoin/electrum_wallet.dart'; -import 'package:cake_wallet/entities/balance_display_mode.dart'; -import 'package:cake_wallet/entities/calculate_fiat_amount_raw.dart'; import 'package:cake_wallet/entities/transaction_description.dart'; import 'package:cake_wallet/entities/transaction_priority.dart'; -import 'package:cake_wallet/monero/monero_amount_format.dart'; import 'package:cake_wallet/view_model/send/send_item.dart'; import 'package:cake_wallet/view_model/send/send_template_view_model.dart'; import 'package:cake_wallet/view_model/settings/settings_view_model.dart'; import 'package:hive/hive.dart'; -import 'package:intl/intl.dart'; import 'package:mobx/mobx.dart'; -import 'package:cake_wallet/entities/openalias_record.dart'; import 'package:cake_wallet/entities/template.dart'; -import 'package:cake_wallet/store/templates/send_template_store.dart'; -import 'package:cake_wallet/core/template_validator.dart'; import 'package:cake_wallet/core/address_validator.dart'; import 'package:cake_wallet/core/amount_validator.dart'; import 'package:cake_wallet/core/pending_transaction.dart'; import 'package:cake_wallet/core/validator.dart'; import 'package:cake_wallet/core/wallet_base.dart'; import 'package:cake_wallet/core/execution_state.dart'; -import 'package:cake_wallet/bitcoin/bitcoin_wallet.dart'; import 'package:cake_wallet/bitcoin/bitcoin_transaction_credentials.dart'; -import 'package:cake_wallet/monero/monero_wallet.dart'; import 'package:cake_wallet/monero/monero_transaction_creation_credentials.dart'; import 'package:cake_wallet/entities/sync_status.dart'; import 'package:cake_wallet/entities/crypto_currency.dart'; @@ -35,7 +25,6 @@ import 'package:cake_wallet/entities/wallet_type.dart'; import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/view_model/send/send_view_model_state.dart'; -import 'package:cake_wallet/generated/i18n.dart'; part 'send_view_model.g.dart'; @@ -138,6 +127,9 @@ abstract class SendViewModelBase with Store { @computed ObservableList