CAKE-345 | applied batch sending to monero wallet

OleksandrSobol committed Aug 4, 2021 at 17:38 UTC d4c0fb6fec59b9823927ce6d39945d0028be5f94
10 files changed +230 -35
cw_monero/ios/Classes/monero_api.cpp
+42
@@ -495,6 +495,48 @@ extern "C"
495 return true;
496 }
497
498 + bool transaction_create_mult_dest(char **addresses, char *payment_id, char **amounts, uint32_t size,
499 + uint8_t priority_raw, uint32_t subaddr_account, Utf8Box &error, PendingTransactionRaw &pendingTransaction)
500 + {
501 + nice(19);
502 +
503 + std::vector<std::string> _addresses;
504 + std::vector<uint64_t> _amounts;
505 +
506 + for (int i = 0; i < size; i++) {
507 + _addresses.push_back(std::string(*addresses));
508 + _amounts.push_back(Monero::Wallet::amountFromString(std::string(*amounts)));
509 + addresses++;
510 + amounts++;
511 + }
512 +
513 + auto priority = static_cast<Monero::PendingTransaction::Priority>(priority_raw);
514 + std::string _payment_id;
515 + Monero::PendingTransaction *transaction;
516 +
517 + if (payment_id != nullptr)
518 + {
519 + _payment_id = std::string(payment_id);
520 + }
521 +
522 + transaction = m_wallet->createTransactionMultDest(_addresses, _payment_id, _amounts, m_wallet->defaultMixin(), priority, subaddr_account);
523 +
524 + int status = transaction->status();
525 +
526 + if (status == Monero::PendingTransaction::Status::Status_Error || status == Monero::PendingTransaction::Status::Status_Critical)
527 + {
528 + error = Utf8Box(strdup(transaction->errorString().c_str()));
529 + return false;
530 + }
531 +
532 + if (m_listener != nullptr) {
533 + m_listener->m_new_transaction = true;
534 + }
535 +
536 + pendingTransaction = PendingTransactionRaw(transaction);
537 + return true;
538 + }
539 +
540 bool transaction_commit(PendingTransactionRaw *transaction, Utf8Box &error)
541 {
542 bool committed = transaction->transaction->commit();
cw_monero/lib/signatures.dart
+10
@@ -95,6 +95,16 @@ typedef transaction_create = Int8 Function(
95 Pointer<Utf8Box> error,
96 Pointer<PendingTransactionRaw> pendingTransaction);
97
98 +typedef transaction_create_mult_dest = Int8 Function(
99 + Pointer<Pointer<Utf8>> addresses,
100 + Pointer<Utf8> paymentId,
101 + Pointer<Pointer<Utf8>> amounts,
102 + Int32 size,
103 + Int8 priorityRaw,
104 + Int32 subaddrAccount,
105 + Pointer<Utf8Box> error,
106 + Pointer<PendingTransactionRaw> pendingTransaction);
107 +
108 typedef transaction_commit = Int8 Function(Pointer<PendingTransactionRaw>, Pointer<Utf8Box>);
109
110 typedef secret_view_key = Pointer<Utf8> Function();
cw_monero/lib/transaction_history.dart
+90
@@ -26,6 +26,10 @@ final transactionCreateNative = moneroApi
26 .lookup<NativeFunction<transaction_create>>('transaction_create')
27 .asFunction<TransactionCreate>();
28
29 +final transactionCreateMultDestNative = moneroApi
30 + .lookup<NativeFunction<transaction_create_mult_dest>>('transaction_create_mult_dest')
31 + .asFunction<TransactionCreateMultDest>();
32 +
33 final transactionCommitNative = moneroApi
34 .lookup<NativeFunction<transaction_commit>>('transaction_commit')
35 .asFunction<TransactionCommit>();
@@ -102,6 +106,59 @@ PendingTransactionDescription createTransactionSync(
106 pointerAddress: pendingTransactionRawPointer.address);
107 }
108
109 +PendingTransactionDescription createTransactionMultDestSync(
110 + {List<String> addresses,
111 + String paymentId,
112 + List<String> amounts,
113 + int size,
114 + int priorityRaw,
115 + int accountIndex = 0}) {
116 + final List<Pointer<Utf8>> addressesPointers = addresses.map(Utf8.toUtf8).toList();
117 + final Pointer<Pointer<Utf8>> addressesPointerPointer = allocate(count: size);
118 +
119 + final List<Pointer<Utf8>> amountsPointers = amounts.map(Utf8.toUtf8).toList();
120 + final Pointer<Pointer<Utf8>> amountsPointerPointer = allocate(count: size);
121 +
122 + for (int i = 0; i < size; i++) {
123 + addressesPointerPointer[ i ] = addressesPointers[ i ];
124 + amountsPointerPointer[ i ] = amountsPointers[ i ];
125 + }
126 +
127 + final paymentIdPointer = Utf8.toUtf8(paymentId);
128 + final errorMessagePointer = allocate<Utf8Box>();
129 + final pendingTransactionRawPointer = allocate<PendingTransactionRaw>();
130 + final created = transactionCreateMultDestNative(
131 + addressesPointerPointer,
132 + paymentIdPointer,
133 + amountsPointerPointer,
134 + size,
135 + priorityRaw,
136 + accountIndex,
137 + errorMessagePointer,
138 + pendingTransactionRawPointer) !=
139 + 0;
140 +
141 + free(addressesPointerPointer);
142 + free(amountsPointerPointer);
143 +
144 + addressesPointers.forEach((element) => free(element));
145 + amountsPointers.forEach((element) => free(element));
146 +
147 + free(paymentIdPointer);
148 +
149 + if (!created) {
150 + final message = errorMessagePointer.ref.getValue();
151 + free(errorMessagePointer);
152 + throw CreationTransactionException(message: message);
153 + }
154 +
155 + return PendingTransactionDescription(
156 + amount: pendingTransactionRawPointer.ref.amount,
157 + fee: pendingTransactionRawPointer.ref.fee,
158 + hash: pendingTransactionRawPointer.ref.getHash(),
159 + pointerAddress: pendingTransactionRawPointer.address);
160 +}
161 +
162 void commitTransactionFromPointerAddress({int address}) => commitTransaction(
163 transactionPointer: Pointer<PendingTransactionRaw>.fromAddress(address));
164
@@ -132,6 +189,23 @@ PendingTransactionDescription _createTransactionSync(Map args) {
189 accountIndex: accountIndex);
190 }
191
192 +PendingTransactionDescription _createTransactionMultDestSync(Map args) {
193 + final addresses = args['addresses'] as List<String>;
194 + final paymentId = args['paymentId'] as String;
195 + final amounts = args['amounts'] as List<String>;
196 + final size = args['size'] as int;
197 + final priorityRaw = args['priorityRaw'] as int;
198 + final accountIndex = args['accountIndex'] as int;
199 +
200 + return createTransactionMultDestSync(
201 + addresses: addresses,
202 + paymentId: paymentId,
203 + amounts: amounts,
204 + size: size,
205 + priorityRaw: priorityRaw,
206 + accountIndex: accountIndex);
207 +}
208 +
209 Future<PendingTransactionDescription> createTransaction(
210 {String address,
211 String paymentId,
@@ -145,3 +219,19 @@ Future<PendingTransactionDescription> createTransaction(
219 'priorityRaw': priorityRaw,
220 'accountIndex': accountIndex
221 });
222 +
223 +Future<PendingTransactionDescription> createTransactionMultDest(
224 + {List<String> addresses,
225 + String paymentId,
226 + List<String> amounts,
227 + int size,
228 + int priorityRaw,
229 + int accountIndex = 0}) =>
230 + compute(_createTransactionMultDestSync, {
231 + 'addresses': addresses,
232 + 'paymentId': paymentId,
233 + 'amounts': amounts,
234 + 'size': size,
235 + 'priorityRaw': priorityRaw,
236 + 'accountIndex': accountIndex
237 + });
cw_monero/lib/types.dart
+10
@@ -93,6 +93,16 @@ typedef TransactionCreate = int Function(
93 Pointer<Utf8Box> error,
94 Pointer<PendingTransactionRaw> pendingTransaction);
95
96 +typedef TransactionCreateMultDest = int Function(
97 + Pointer<Pointer<Utf8>> addresses,
98 + Pointer<Utf8> paymentId,
99 + Pointer<Pointer<Utf8>> amounts,
100 + int size,
101 + int priorityRaw,
102 + int subaddrAccount,
103 + Pointer<Utf8Box> error,
104 + Pointer<PendingTransactionRaw> pendingTransaction);
105 +
106 typedef TransactionCommit = int Function(Pointer<PendingTransactionRaw>, Pointer<Utf8Box>);
107
108 typedef SecretViewKey = Pointer<Utf8> Function();
lib/monero/monero_transaction_creation_credentials.dart
+3 -5
@@ -1,13 +1,11 @@
1 import 'package:cake_wallet/entities/transaction_creation_credentials.dart';
2 import 'package:cake_wallet/entities/monero_transaction_priority.dart';
3 +import 'package:cake_wallet/view_model/send/send_item.dart';
4
5 class MoneroTransactionCreationCredentials
6 extends TransactionCreationCredentials {
6 - MoneroTransactionCreationCredentials(
7 - {this.address, this.paymentId, this.priority, this.amount});
7 + MoneroTransactionCreationCredentials({this.sendItemList, this.priority});
8
9 - final String address;
10 - final String paymentId;
11 - final String amount;
9 + final List<SendItem> sendItemList;
10 final MoneroTransactionPriority priority;
11 }
lib/monero/monero_wallet.dart
+68 -18
@@ -5,6 +5,7 @@ import 'package:cake_wallet/monero/monero_transaction_creation_exception.dart';
5 import 'package:cake_wallet/monero/monero_transaction_info.dart';
6 import 'package:cake_wallet/monero/monero_wallet_addresses.dart';
7 import 'package:cake_wallet/monero/monero_wallet_utils.dart';
8 +import 'package:cw_monero/structs/pending_transaction.dart';
9 import 'package:flutter/foundation.dart';
10 import 'package:mobx/mobx.dart';
11 import 'package:cw_monero/transaction_history.dart'
@@ -149,31 +150,80 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
150 @override
151 Future<PendingTransaction> createTransaction(Object credentials) async {
152 final _credentials = credentials as MoneroTransactionCreationCredentials;
152 - final amount = _credentials.amount != null
153 - ? moneroParseAmount(amount: _credentials.amount)
154 - : null;
153 + final sendItemList = _credentials.sendItemList;
154 + final listSize = sendItemList.length;
155 final unlockedBalance =
156 - monero_wallet.getUnlockedBalance(accountIndex: walletAddresses.account.id);
156 + monero_wallet.getUnlockedBalance(accountIndex: walletAddresses.account.id);
157
158 - if ((amount != null && unlockedBalance < amount) ||
159 - (amount == null && unlockedBalance <= 0)) {
160 - final formattedBalance = moneroAmountToString(amount: unlockedBalance);
161 -
162 - throw MoneroTransactionCreationException(
163 - 'Incorrect unlocked balance. Unlocked: $formattedBalance. Transaction amount: ${_credentials.amount}.');
164 - }
158 + PendingTransactionDescription pendingTransactionDescription;
159
160 if (!(syncStatus is SyncedSyncStatus)) {
161 throw MoneroTransactionCreationException('The wallet is not synced.');
162 }
163
170 - final pendingTransactionDescription =
171 - await transaction_history.createTransaction(
172 - address: _credentials.address,
173 - paymentId: _credentials.paymentId,
174 - amount: _credentials.amount,
175 - priorityRaw: _credentials.priority.serialize(),
176 - accountIndex: walletAddresses.account.id);
164 + if (listSize > 1) {
165 + final sendAllItems = sendItemList.where((item) => item.sendAll).toList();
166 +
167 + if (sendAllItems?.isNotEmpty ?? false) {
168 + throw MoneroTransactionCreationException('Wrong balance. Not enough XMR on your balance.');
169 + }
170 +
171 + final nullAmountItems = sendItemList.where((item) =>
172 + moneroParseAmount(amount: item.cryptoAmount.replaceAll(',', '.')) <= 0)
173 + .toList();
174 +
175 + if (nullAmountItems?.isNotEmpty ?? false) {
176 + throw MoneroTransactionCreationException('Wrong balance. Not enough XMR on your balance.');
177 + }
178 +
179 + var credentialsAmount = 0;
180 +
181 + credentialsAmount = sendItemList.fold(0, (previousValue, element) =>
182 + previousValue + moneroParseAmount(
183 + amount: element.cryptoAmount.replaceAll(',', '.')));
184 +
185 + if (unlockedBalance < credentialsAmount) {
186 + throw MoneroTransactionCreationException('Wrong balance. Not enough XMR on your balance.');
187 + }
188 +
189 + final addresses = sendItemList.map((e) => e.address).toList();
190 + final amounts = sendItemList.map((e) =>
191 + e.cryptoAmount.replaceAll(',', '.')).toList();
192 +
193 + pendingTransactionDescription =
194 + await transaction_history.createTransactionMultDest(
195 + addresses: addresses,
196 + paymentId: '',
197 + amounts: amounts,
198 + size: listSize,
199 + priorityRaw: _credentials.priority.serialize(),
200 + accountIndex: walletAddresses.account.id);
201 + } else {
202 + final item = sendItemList.first;
203 + final address = item.address;
204 + final amount = item.sendAll
205 + ? null
206 + : item.cryptoAmount.replaceAll(',', '.');
207 + final formattedAmount = item.sendAll
208 + ? null
209 + : moneroParseAmount(amount: amount);
210 +
211 + if ((formattedAmount != null && unlockedBalance < formattedAmount) ||
212 + (formattedAmount == null && unlockedBalance <= 0)) {
213 + final formattedBalance = moneroAmountToString(amount: unlockedBalance);
214 +
215 + throw MoneroTransactionCreationException(
216 + 'Incorrect unlocked balance. Unlocked: $formattedBalance. Transaction amount: ${item.cryptoAmount}.');
217 + }
218 +
219 + pendingTransactionDescription =
220 + await transaction_history.createTransaction(
221 + address: address,
222 + paymentId: '',
223 + amount: amount,
224 + priorityRaw: _credentials.priority.serialize(),
225 + accountIndex: walletAddresses.account.id);
226 + }
227
228 return PendingMoneroTransaction(pendingTransactionDescription);
229 }
lib/src/screens/send/send_page.dart
+1 -1
@@ -259,7 +259,7 @@ class SendPage extends BasePage {
259 EdgeInsets.only(left: 24, right: 24, bottom: 24),
260 bottomSection: Column(
261 children: [
262 - if (sendViewModel.isElectrumWallet) Padding(
262 + Padding(
263 padding: EdgeInsets.only(bottom: 12),
264 child: PrimaryButton(
265 onPressed: () {
lib/view_model/send/send_item.dart
-1
@@ -1,7 +1,6 @@
1 import 'package:cake_wallet/bitcoin/bitcoin_amount_format.dart';
2 import 'package:cake_wallet/bitcoin/electrum_wallet.dart';
3 import 'package:cake_wallet/entities/calculate_fiat_amount_raw.dart';
4 -import 'package:cake_wallet/entities/openalias_record.dart';
4 import 'package:cake_wallet/entities/parse_address_from_domain.dart';
5 import 'package:cake_wallet/entities/parsed_address.dart';
6 import 'package:cake_wallet/monero/monero_amount_format.dart';
lib/view_model/send/send_view_model.dart
+2 -9
@@ -199,18 +199,11 @@ abstract class SendViewModelBase with Store {
199 return BitcoinTransactionCredentials(
200 sendItemList, priority as BitcoinTransactionPriority);
201 case WalletType.monero:
202 - final _item = sendItemList.first;
203 - final address = _item.address;
204 - final amount = _item.sendAll
205 - ? null
206 - : _item.cryptoAmount.replaceAll(',', '.');
202 final priority = _settingsStore.priority[_wallet.type];
203
204 return MoneroTransactionCreationCredentials(
210 - address: address,
211 - paymentId: '',
212 - priority: priority as MoneroTransactionPriority,
213 - amount: amount);
205 + sendItemList: sendItemList,
206 + priority: priority as MoneroTransactionPriority);
207 default:
208 return null;
209 }
lib/view_model/transaction_details_view_model.dart
+4 -1
@@ -30,6 +30,7 @@ abstract class TransactionDetailsViewModelBase with Store {
30 this.settingsStore})
31 : items = [] {
32 showRecipientAddress = settingsStore?.shouldSaveRecipientAddress ?? false;
33 + isRecipientAddressShown = false;
34
35 final dateFormat = DateFormatter.withCurrentLocal();
36 final tx = transactionInfo;
@@ -64,6 +65,7 @@ abstract class TransactionDetailsViewModelBase with Store {
65 final address =
66 _wallet.getTransactionAddress(accountIndex, addressIndex);
67 if (address?.isNotEmpty ?? false) {
68 + isRecipientAddressShown = true;
69 _items.add(
70 StandartListItem(
71 title: S.current.transaction_details_recipient_address,
@@ -101,7 +103,7 @@ abstract class TransactionDetailsViewModelBase with Store {
103 items.addAll(_items);
104 }
105
104 - if (showRecipientAddress) {
106 + if (showRecipientAddress && !isRecipientAddressShown) {
107 final recipientAddress = transactionDescriptionBox.values
108 .firstWhere((val) => val.id == transactionInfo.id, orElse: () => null)
109 ?.recipientAddress;
@@ -151,6 +153,7 @@ abstract class TransactionDetailsViewModelBase with Store {
153
154 final List<TransactionDetailsListItem> items;
155 bool showRecipientAddress;
156 + bool isRecipientAddressShown;
157
158 String _explorerUrl(WalletType type, String txId) {
159 switch (type) {