diff --git a/cw_monero/ios/Classes/monero_api.cpp b/cw_monero/ios/Classes/monero_api.cpp index d58f4e509..7ab1780c8 100644 --- a/cw_monero/ios/Classes/monero_api.cpp +++ b/cw_monero/ios/Classes/monero_api.cpp @@ -495,6 +495,48 @@ extern "C" return true; } + bool transaction_create_mult_dest(char **addresses, char *payment_id, char **amounts, uint32_t size, + uint8_t priority_raw, uint32_t subaddr_account, Utf8Box &error, PendingTransactionRaw &pendingTransaction) + { + nice(19); + + std::vector _addresses; + std::vector _amounts; + + for (int i = 0; i < size; i++) { + _addresses.push_back(std::string(*addresses)); + _amounts.push_back(Monero::Wallet::amountFromString(std::string(*amounts))); + addresses++; + amounts++; + } + + auto priority = static_cast(priority_raw); + std::string _payment_id; + Monero::PendingTransaction *transaction; + + if (payment_id != nullptr) + { + _payment_id = std::string(payment_id); + } + + transaction = m_wallet->createTransactionMultDest(_addresses, _payment_id, _amounts, m_wallet->defaultMixin(), priority, subaddr_account); + + int status = transaction->status(); + + if (status == Monero::PendingTransaction::Status::Status_Error || status == Monero::PendingTransaction::Status::Status_Critical) + { + error = Utf8Box(strdup(transaction->errorString().c_str())); + return false; + } + + if (m_listener != nullptr) { + m_listener->m_new_transaction = true; + } + + pendingTransaction = PendingTransactionRaw(transaction); + return true; + } + bool transaction_commit(PendingTransactionRaw *transaction, Utf8Box &error) { bool committed = transaction->transaction->commit(); diff --git a/cw_monero/lib/signatures.dart b/cw_monero/lib/signatures.dart index 0f75288e7..1e8ad0fbd 100644 --- a/cw_monero/lib/signatures.dart +++ b/cw_monero/lib/signatures.dart @@ -95,6 +95,16 @@ typedef transaction_create = Int8 Function( Pointer error, Pointer pendingTransaction); +typedef transaction_create_mult_dest = Int8 Function( + Pointer> addresses, + Pointer paymentId, + Pointer> amounts, + Int32 size, + Int8 priorityRaw, + Int32 subaddrAccount, + Pointer error, + Pointer pendingTransaction); + typedef transaction_commit = Int8 Function(Pointer, Pointer); typedef secret_view_key = Pointer Function(); diff --git a/cw_monero/lib/transaction_history.dart b/cw_monero/lib/transaction_history.dart index 7e6f94c23..799d4d512 100644 --- a/cw_monero/lib/transaction_history.dart +++ b/cw_monero/lib/transaction_history.dart @@ -26,6 +26,10 @@ final transactionCreateNative = moneroApi .lookup>('transaction_create') .asFunction(); +final transactionCreateMultDestNative = moneroApi + .lookup>('transaction_create_mult_dest') + .asFunction(); + final transactionCommitNative = moneroApi .lookup>('transaction_commit') .asFunction(); @@ -102,6 +106,59 @@ PendingTransactionDescription createTransactionSync( pointerAddress: pendingTransactionRawPointer.address); } +PendingTransactionDescription createTransactionMultDestSync( + {List addresses, + String paymentId, + List amounts, + int size, + int priorityRaw, + int accountIndex = 0}) { + final List> addressesPointers = addresses.map(Utf8.toUtf8).toList(); + final Pointer> addressesPointerPointer = allocate(count: size); + + final List> amountsPointers = amounts.map(Utf8.toUtf8).toList(); + final Pointer> amountsPointerPointer = allocate(count: size); + + for (int i = 0; i < size; i++) { + addressesPointerPointer[ i ] = addressesPointers[ i ]; + amountsPointerPointer[ i ] = amountsPointers[ i ]; + } + + final paymentIdPointer = Utf8.toUtf8(paymentId); + final errorMessagePointer = allocate(); + final pendingTransactionRawPointer = allocate(); + final created = transactionCreateMultDestNative( + addressesPointerPointer, + paymentIdPointer, + amountsPointerPointer, + size, + priorityRaw, + accountIndex, + errorMessagePointer, + pendingTransactionRawPointer) != + 0; + + free(addressesPointerPointer); + free(amountsPointerPointer); + + addressesPointers.forEach((element) => free(element)); + amountsPointers.forEach((element) => free(element)); + + free(paymentIdPointer); + + if (!created) { + final message = errorMessagePointer.ref.getValue(); + free(errorMessagePointer); + throw CreationTransactionException(message: message); + } + + return PendingTransactionDescription( + amount: pendingTransactionRawPointer.ref.amount, + fee: pendingTransactionRawPointer.ref.fee, + hash: pendingTransactionRawPointer.ref.getHash(), + pointerAddress: pendingTransactionRawPointer.address); +} + void commitTransactionFromPointerAddress({int address}) => commitTransaction( transactionPointer: Pointer.fromAddress(address)); @@ -132,6 +189,23 @@ PendingTransactionDescription _createTransactionSync(Map args) { accountIndex: accountIndex); } +PendingTransactionDescription _createTransactionMultDestSync(Map args) { + final addresses = args['addresses'] as List; + final paymentId = args['paymentId'] as String; + final amounts = args['amounts'] as List; + final size = args['size'] as int; + final priorityRaw = args['priorityRaw'] as int; + final accountIndex = args['accountIndex'] as int; + + return createTransactionMultDestSync( + addresses: addresses, + paymentId: paymentId, + amounts: amounts, + size: size, + priorityRaw: priorityRaw, + accountIndex: accountIndex); +} + Future createTransaction( {String address, String paymentId, @@ -145,3 +219,19 @@ Future createTransaction( 'priorityRaw': priorityRaw, 'accountIndex': accountIndex }); + +Future createTransactionMultDest( + {List addresses, + String paymentId, + List amounts, + int size, + int priorityRaw, + int accountIndex = 0}) => + compute(_createTransactionMultDestSync, { + 'addresses': addresses, + 'paymentId': paymentId, + 'amounts': amounts, + 'size': size, + 'priorityRaw': priorityRaw, + 'accountIndex': accountIndex + }); diff --git a/cw_monero/lib/types.dart b/cw_monero/lib/types.dart index 1cc1a6055..b972231e6 100644 --- a/cw_monero/lib/types.dart +++ b/cw_monero/lib/types.dart @@ -93,6 +93,16 @@ typedef TransactionCreate = int Function( Pointer error, Pointer pendingTransaction); +typedef TransactionCreateMultDest = int Function( + Pointer> addresses, + Pointer paymentId, + Pointer> amounts, + int size, + int priorityRaw, + int subaddrAccount, + Pointer error, + Pointer pendingTransaction); + typedef TransactionCommit = int Function(Pointer, Pointer); typedef SecretViewKey = Pointer Function(); diff --git a/lib/monero/monero_transaction_creation_credentials.dart b/lib/monero/monero_transaction_creation_credentials.dart index 75d3eaa79..aa49eba00 100644 --- a/lib/monero/monero_transaction_creation_credentials.dart +++ b/lib/monero/monero_transaction_creation_credentials.dart @@ -1,13 +1,11 @@ import 'package:cake_wallet/entities/transaction_creation_credentials.dart'; import 'package:cake_wallet/entities/monero_transaction_priority.dart'; +import 'package:cake_wallet/view_model/send/send_item.dart'; class MoneroTransactionCreationCredentials extends TransactionCreationCredentials { - MoneroTransactionCreationCredentials( - {this.address, this.paymentId, this.priority, this.amount}); + MoneroTransactionCreationCredentials({this.sendItemList, this.priority}); - final String address; - final String paymentId; - final String amount; + final List sendItemList; final MoneroTransactionPriority priority; } diff --git a/lib/monero/monero_wallet.dart b/lib/monero/monero_wallet.dart index 36f2e4bc8..6652f27ee 100644 --- a/lib/monero/monero_wallet.dart +++ b/lib/monero/monero_wallet.dart @@ -5,6 +5,7 @@ import 'package:cake_wallet/monero/monero_transaction_creation_exception.dart'; import 'package:cake_wallet/monero/monero_transaction_info.dart'; import 'package:cake_wallet/monero/monero_wallet_addresses.dart'; import 'package:cake_wallet/monero/monero_wallet_utils.dart'; +import 'package:cw_monero/structs/pending_transaction.dart'; import 'package:flutter/foundation.dart'; import 'package:mobx/mobx.dart'; import 'package:cw_monero/transaction_history.dart' @@ -149,31 +150,80 @@ abstract class MoneroWalletBase extends WalletBase createTransaction(Object credentials) async { final _credentials = credentials as MoneroTransactionCreationCredentials; - final amount = _credentials.amount != null - ? moneroParseAmount(amount: _credentials.amount) - : null; + final sendItemList = _credentials.sendItemList; + final listSize = sendItemList.length; final unlockedBalance = - monero_wallet.getUnlockedBalance(accountIndex: walletAddresses.account.id); + monero_wallet.getUnlockedBalance(accountIndex: walletAddresses.account.id); - if ((amount != null && unlockedBalance < amount) || - (amount == null && unlockedBalance <= 0)) { - final formattedBalance = moneroAmountToString(amount: unlockedBalance); - - throw MoneroTransactionCreationException( - 'Incorrect unlocked balance. Unlocked: $formattedBalance. Transaction amount: ${_credentials.amount}.'); - } + PendingTransactionDescription pendingTransactionDescription; if (!(syncStatus is SyncedSyncStatus)) { throw MoneroTransactionCreationException('The wallet is not synced.'); } - final pendingTransactionDescription = - await transaction_history.createTransaction( - address: _credentials.address, - paymentId: _credentials.paymentId, - amount: _credentials.amount, - priorityRaw: _credentials.priority.serialize(), - accountIndex: walletAddresses.account.id); + if (listSize > 1) { + final sendAllItems = sendItemList.where((item) => item.sendAll).toList(); + + if (sendAllItems?.isNotEmpty ?? false) { + throw MoneroTransactionCreationException('Wrong balance. Not enough XMR on your balance.'); + } + + final nullAmountItems = sendItemList.where((item) => + moneroParseAmount(amount: item.cryptoAmount.replaceAll(',', '.')) <= 0) + .toList(); + + if (nullAmountItems?.isNotEmpty ?? false) { + throw MoneroTransactionCreationException('Wrong balance. Not enough XMR on your balance.'); + } + + var credentialsAmount = 0; + + credentialsAmount = sendItemList.fold(0, (previousValue, element) => + previousValue + moneroParseAmount( + amount: element.cryptoAmount.replaceAll(',', '.'))); + + if (unlockedBalance < credentialsAmount) { + throw MoneroTransactionCreationException('Wrong balance. Not enough XMR on your balance.'); + } + + final addresses = sendItemList.map((e) => e.address).toList(); + final amounts = sendItemList.map((e) => + e.cryptoAmount.replaceAll(',', '.')).toList(); + + pendingTransactionDescription = + await transaction_history.createTransactionMultDest( + addresses: addresses, + paymentId: '', + amounts: amounts, + size: listSize, + priorityRaw: _credentials.priority.serialize(), + accountIndex: walletAddresses.account.id); + } else { + final item = sendItemList.first; + final address = item.address; + final amount = item.sendAll + ? null + : item.cryptoAmount.replaceAll(',', '.'); + final formattedAmount = item.sendAll + ? null + : moneroParseAmount(amount: amount); + + if ((formattedAmount != null && unlockedBalance < formattedAmount) || + (formattedAmount == null && unlockedBalance <= 0)) { + final formattedBalance = moneroAmountToString(amount: unlockedBalance); + + throw MoneroTransactionCreationException( + 'Incorrect unlocked balance. Unlocked: $formattedBalance. Transaction amount: ${item.cryptoAmount}.'); + } + + pendingTransactionDescription = + await transaction_history.createTransaction( + address: address, + paymentId: '', + amount: amount, + priorityRaw: _credentials.priority.serialize(), + accountIndex: walletAddresses.account.id); + } return PendingMoneroTransaction(pendingTransactionDescription); } diff --git a/lib/src/screens/send/send_page.dart b/lib/src/screens/send/send_page.dart index a88c01b6e..017cec804 100644 --- a/lib/src/screens/send/send_page.dart +++ b/lib/src/screens/send/send_page.dart @@ -259,7 +259,7 @@ class SendPage extends BasePage { EdgeInsets.only(left: 24, right: 24, bottom: 24), bottomSection: Column( children: [ - if (sendViewModel.isElectrumWallet) Padding( + Padding( padding: EdgeInsets.only(bottom: 12), child: PrimaryButton( onPressed: () { diff --git a/lib/view_model/send/send_item.dart b/lib/view_model/send/send_item.dart index 983646d47..153a6c1d7 100644 --- a/lib/view_model/send/send_item.dart +++ b/lib/view_model/send/send_item.dart @@ -1,7 +1,6 @@ import 'package:cake_wallet/bitcoin/bitcoin_amount_format.dart'; import 'package:cake_wallet/bitcoin/electrum_wallet.dart'; import 'package:cake_wallet/entities/calculate_fiat_amount_raw.dart'; -import 'package:cake_wallet/entities/openalias_record.dart'; import 'package:cake_wallet/entities/parse_address_from_domain.dart'; import 'package:cake_wallet/entities/parsed_address.dart'; import 'package:cake_wallet/monero/monero_amount_format.dart'; diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 6d3ec9109..9fa1c4ee6 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -199,18 +199,11 @@ abstract class SendViewModelBase with Store { return BitcoinTransactionCredentials( sendItemList, priority as BitcoinTransactionPriority); case WalletType.monero: - final _item = sendItemList.first; - final address = _item.address; - final amount = _item.sendAll - ? null - : _item.cryptoAmount.replaceAll(',', '.'); final priority = _settingsStore.priority[_wallet.type]; return MoneroTransactionCreationCredentials( - address: address, - paymentId: '', - priority: priority as MoneroTransactionPriority, - amount: amount); + sendItemList: sendItemList, + priority: priority as MoneroTransactionPriority); default: return null; } diff --git a/lib/view_model/transaction_details_view_model.dart b/lib/view_model/transaction_details_view_model.dart index 49748f7f0..c13881c21 100644 --- a/lib/view_model/transaction_details_view_model.dart +++ b/lib/view_model/transaction_details_view_model.dart @@ -30,6 +30,7 @@ abstract class TransactionDetailsViewModelBase with Store { this.settingsStore}) : items = [] { showRecipientAddress = settingsStore?.shouldSaveRecipientAddress ?? false; + isRecipientAddressShown = false; final dateFormat = DateFormatter.withCurrentLocal(); final tx = transactionInfo; @@ -64,6 +65,7 @@ abstract class TransactionDetailsViewModelBase with Store { final address = _wallet.getTransactionAddress(accountIndex, addressIndex); if (address?.isNotEmpty ?? false) { + isRecipientAddressShown = true; _items.add( StandartListItem( title: S.current.transaction_details_recipient_address, @@ -101,7 +103,7 @@ abstract class TransactionDetailsViewModelBase with Store { items.addAll(_items); } - if (showRecipientAddress) { + if (showRecipientAddress && !isRecipientAddressShown) { final recipientAddress = transactionDescriptionBox.values .firstWhere((val) => val.id == transactionInfo.id, orElse: () => null) ?.recipientAddress; @@ -151,6 +153,7 @@ abstract class TransactionDetailsViewModelBase with Store { final List items; bool showRecipientAddress; + bool isRecipientAddressShown; String _explorerUrl(WalletType type, String txId) { switch (type) {