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