dev
dart 396 lines 12.4 KB
Raw
1 import 'dart:async';
2 import 'dart:ffi';
3 import 'dart:isolate';
4
5 import 'package:cw_core/utils/print_verbose.dart';
6 import 'package:cw_monero/api/account_list.dart';
7 import 'package:cw_monero/api/exceptions/creation_transaction_exception.dart';
8 import 'package:cw_monero/api/monero_output.dart';
9 import 'package:cw_monero/api/structs/pending_transaction.dart';
10 import 'package:cw_monero/api/wallet.dart';
11 import 'package:cw_monero/exceptions/monero_transaction_creation_exception.dart';
12 import 'package:ffi/ffi.dart';
13 import 'package:monero/src/monero.dart';
14 import 'package:monero/monero.dart' as monero;
15 import 'package:monero/src/wallet2.dart';
16 import 'package:monero/src/generated_bindings_monero.g.dart' as monero_gen;
17 import 'package:mutex/mutex.dart';
18
19 String _formatTransactionError(String error) {
20 final message = error.replaceAll(
21 RegExp(
22 r'(?:[A-Za-z]:)?[\\/][^\s:]*\.(?:c|cc|cpp|cxx|h|hpp|hxx):\d+:(?:[A-Za-z0-9_]+:)?\s*',
23 ),
24 '',
25 );
26 if (message.contains("RPC error")) {
27 return "Invalid node response, please try again or switch node\n\ntrace: $message";
28 }
29 return message;
30 }
31
32 Map<int, Map<String, String>> txKeys = {};
33 String getTxKey(String txId) {
34 txKeys[currentWallet!.ffiAddress()] ??= {};
35 if (txKeys[currentWallet!.ffiAddress()]![txId] != null) {
36 return txKeys[currentWallet!.ffiAddress()]![txId]!;
37 }
38 final txKey = currentWallet!.getTxKey(txid: txId);
39 final status = currentWallet!.status();
40 if (status != 0) {
41 currentWallet!.errorString();
42 txKeys[currentWallet!.ffiAddress()]![txId] = "";
43 return "";
44 }
45 txKeys[currentWallet!.ffiAddress()]![txId] = txKey;
46 return txKey;
47 }
48
49 final txHistoryMutex = Mutex();
50 Wallet2TransactionHistory? txhistory;
51 bool isRefreshingTx = false;
52 Future<void> refreshTransactions() async {
53 if (isRefreshingTx == true) return;
54 isRefreshingTx = true;
55 txhistory ??= currentWallet!.history();
56 final ptr = txhistory!.ffiAddress();
57 await txHistoryMutex.acquire();
58 await Isolate.run(() {
59 monero.TransactionHistory_refresh(Pointer.fromAddress(ptr));
60 });
61 await Future.delayed(Duration.zero);
62 txHistoryMutex.release();
63 isRefreshingTx = false;
64 }
65
66 int countOfTransactions() => txhistory!.count();
67
68 Future<List<Transaction>> getAllTransactions() async {
69 List<Transaction> dummyTxs = [];
70
71 await txHistoryMutex.acquire();
72 txhistory ??= currentWallet!.history();
73 final startAddress = txhistory!.ffiAddress() * currentWallet!.ffiAddress();
74 int size = countOfTransactions();
75 final list = <Transaction>[];
76 for (int index = 0; index < size; index++) {
77 if (index % 25 == 0) {
78 // Give main thread a chance to do other things.
79 await Future.delayed(Duration.zero);
80 }
81 if (txhistory!.ffiAddress() * currentWallet!.ffiAddress() != startAddress) {
82 printV("Loop broken because txhistory!.address * wptr!.address != startAddress");
83 break;
84 }
85 final txInfo = txhistory!.transaction(index);
86 final txHash = txInfo.hash();
87 txCache[currentWallet!.ffiAddress()] ??= {};
88 txCache[currentWallet!.ffiAddress()]![txHash] = Transaction(txInfo: txInfo);
89 list.add(txCache[currentWallet!.ffiAddress()]![txHash]!);
90 }
91 txHistoryMutex.release();
92 final accts = currentWallet!.numSubaddressAccounts();
93 for (var i = 0; i < accts; i++) {
94 final fullBalance = currentWallet!.balance(accountIndex: i);
95 final availBalance = currentWallet!.unlockedBalance(accountIndex: i);
96 if (fullBalance > availBalance) {
97 if (list
98 .where((element) => element.accountIndex == i && element.isConfirmed == false)
99 .isEmpty) {
100 dummyTxs.add(Transaction.dummy(
101 displayLabel: "",
102 description: "",
103 fee: 0,
104 confirmations: 0,
105 blockheight: 0,
106 accountIndex: i,
107 addressIndex: 0,
108 addressIndexList: [0],
109 paymentId: "",
110 amount: fullBalance - availBalance,
111 isSpend: false,
112 hash: "pending",
113 key: "",
114 txInfo: DummyTransaction(),
115 )..timeStamp = DateTime.now());
116 }
117 }
118 }
119 list.addAll(dummyTxs);
120 return list;
121 }
122
123 class DummyTransaction implements Wallet2TransactionInfo {
124 @override
125 dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
126 }
127
128 Map<int, Map<String, Transaction>> txCache = {};
129 Future<Transaction> getTransaction(String txId) async {
130 if (txCache[currentWallet!.ffiAddress()] != null &&
131 txCache[currentWallet!.ffiAddress()]![txId] != null) {
132 return txCache[currentWallet!.ffiAddress()]![txId]!;
133 }
134 await txHistoryMutex.acquire();
135 final tx = txhistory!.transactionById(txId);
136 final txDart = Transaction(txInfo: tx);
137 txCache[currentWallet!.ffiAddress()] ??= {};
138 txCache[currentWallet!.ffiAddress()]![txId] = txDart;
139 txHistoryMutex.release();
140 return txDart;
141 }
142
143 Future<PendingTransactionDescription> createTransactionSync(
144 {required String address,
145 required String paymentId,
146 required int priorityRaw,
147 String? amount,
148 int accountIndex = 0,
149 List<String> preferredInputs = const []}) async {
150 final amt = amount == null ? 0 : currentWallet!.amountFromString(amount);
151
152 final waddr = currentWallet!.ffiAddress();
153
154 // force reconnection in case the os killed the connection?
155 // fixes failed to get block height error.
156 Isolate.run(() async {
157 monero.Wallet_synchronized(Pointer.fromAddress(waddr));
158 });
159
160 final address_ = address.toNativeUtf8();
161 final paymentId_ = paymentId.toNativeUtf8();
162 if (preferredInputs.isEmpty) {
163 throw MoneroTransactionCreationException(
164 "No inputs provided, transaction cannot be constructed");
165 }
166
167 final preferredInputs_ = preferredInputs.join(monero.defaultSeparatorStr).toNativeUtf8();
168
169 final addraddr = address_.address;
170 final paymentIdAddr = paymentId_.address;
171 final preferredInputsAddr = preferredInputs_.address;
172 final spaddr = monero.defaultSeparator.address;
173 final pendingTxPtr = Pointer<Void>.fromAddress(await Isolate.run(() {
174 final tx =
175 monero_gen.MoneroC(DynamicLibrary.open(monero.libPath)).MONERO_Wallet_createTransaction(
176 Pointer.fromAddress(waddr),
177 Pointer.fromAddress(addraddr).cast(),
178 Pointer.fromAddress(paymentIdAddr).cast(),
179 amt,
180 1,
181 priorityRaw,
182 accountIndex,
183 Pointer.fromAddress(preferredInputsAddr).cast(),
184 Pointer.fromAddress(spaddr),
185 );
186 return tx.address;
187 }));
188 final Wallet2PendingTransaction pendingTx = MoneroPendingTransaction(pendingTxPtr);
189 calloc.free(address_);
190 calloc.free(paymentId_);
191 calloc.free(preferredInputs_);
192 final String? error = (() {
193 final status = pendingTx.status();
194 if (status == 0) {
195 return null;
196 }
197 return pendingTx.errorString();
198 })();
199
200 if (error != null) {
201 throw CreationTransactionException(message: _formatTransactionError(error));
202 }
203
204 final rAmt = pendingTx.amount();
205 final rFee = pendingTx.fee();
206 final rHash = pendingTx.txid('');
207 final rHex = pendingTx.hex('');
208
209 return PendingTransactionDescription(
210 amount: rAmt,
211 fee: rFee,
212 hash: rHash,
213 hex: rHex,
214 pointerAddress: pendingTx.ffiAddress(),
215 );
216 }
217
218 Future<PendingTransactionDescription> createTransactionMultDest(
219 {required List<MoneroOutput> outputs,
220 required String paymentId,
221 required int priorityRaw,
222 int accountIndex = 0,
223 List<String> preferredInputs = const []}) async {
224 final dstAddrs = outputs.map((e) => e.address).toList();
225 final amounts = outputs.map((e) => currentWallet!.amountFromString(e.amount)).toList();
226
227 final waddr = currentWallet!.ffiAddress();
228
229 // force reconnection in case the os killed the connection
230 Isolate.run(() async {
231 monero.Wallet_synchronized(Pointer.fromAddress(waddr));
232 });
233
234 final txptr = Pointer<Void>.fromAddress(await Isolate.run(() {
235 return monero.Wallet_createTransactionMultDest(
236 Pointer.fromAddress(waddr),
237 dstAddr: dstAddrs,
238 isSweepAll: false,
239 amounts: amounts,
240 mixinCount: 0,
241 pendingTransactionPriority: priorityRaw,
242 subaddr_account: accountIndex,
243 ).address;
244 }));
245
246 final Wallet2PendingTransaction tx = MoneroPendingTransaction(txptr);
247
248 if (tx.status() != 0) {
249 throw CreationTransactionException(message: _formatTransactionError(tx.errorString()));
250 }
251
252 return PendingTransactionDescription(
253 amount: tx.amount(),
254 fee: tx.fee(),
255 hash: tx.txid(''),
256 hex: tx.hex(''),
257 pointerAddress: tx.ffiAddress(),
258 );
259 }
260
261 Future<String?> commitTransactionFromPointerAddress({required int address, required bool useUR}) =>
262 commitTransaction(tx: MoneroPendingTransaction(Pointer.fromAddress(address)), useUR: useUR);
263
264 Future<String?> commitTransaction(
265 {required Wallet2PendingTransaction tx, required bool useUR}) async {
266 final txCommit = useUR
267 ? tx.commitUR(120)
268 : await Isolate.run(() {
269 monero.PendingTransaction_commit(
270 Pointer.fromAddress(tx.ffiAddress()),
271 filename: '',
272 overwrite: false,
273 );
274 return null;
275 });
276
277 String? error = (() {
278 final status = tx.status();
279 if (status == 0) {
280 return null;
281 }
282 return tx.errorString();
283 })();
284 if (error == null) {
285 error = (() {
286 final status = currentWallet!.status();
287 if (status == 0) {
288 return null;
289 }
290 return currentWallet!.errorString();
291 })();
292 }
293 if (error != null && error != "no tx keys found for this txid") {
294 throw CreationTransactionException(message: _formatTransactionError(error));
295 }
296 unawaited(() async {
297 storeSync(force: true);
298 await Future.delayed(Duration(seconds: 5));
299 storeSync(force: true);
300 }());
301 return Future.value(txCommit);
302 }
303
304 class Transaction {
305 final String displayLabel;
306 late final String subaddressLabel = currentWallet!.getSubaddressLabel(
307 accountIndex: accountIndex,
308 addressIndex: addressIndex,
309 );
310 late final String address = getAddress(
311 accountIndex: accountIndex,
312 addressIndex: addressIndex,
313 );
314 late final List<String> addressList = List.generate(
315 addressIndexList.length,
316 (index) => getAddress(
317 accountIndex: accountIndex,
318 addressIndex: addressIndexList[index],
319 ));
320 final String description;
321 final int fee;
322 final int confirmations;
323 late final bool isPending = confirmations < 10;
324 final int blockheight;
325 final int addressIndex;
326 final int accountIndex;
327 final List<int> addressIndexList;
328 final String paymentId;
329 final int amount;
330 final bool isSpend;
331 late DateTime timeStamp;
332 late final bool isConfirmed = !isPending;
333 final String hash;
334 final String key;
335
336 Map<String, dynamic> toJson() {
337 return {
338 "displayLabel": displayLabel,
339 "subaddressLabel": subaddressLabel,
340 "address": address,
341 "description": description,
342 "fee": fee,
343 "confirmations": confirmations,
344 "isPending": isPending,
345 "blockheight": blockheight,
346 "accountIndex": accountIndex,
347 "addressIndex": addressIndex,
348 "paymentId": paymentId,
349 "amount": amount,
350 "isSpend": isSpend,
351 "timeStamp": timeStamp.toIso8601String(),
352 "isConfirmed": isConfirmed,
353 "hash": hash,
354 };
355 }
356
357 // final SubAddress? subAddress;
358 // List<Transfer> transfers = [];
359 // final int txIndex;
360 final Wallet2TransactionInfo txInfo;
361 Transaction({
362 required this.txInfo,
363 }) : displayLabel = txInfo.label(),
364 hash = txInfo.hash(),
365 timeStamp = DateTime.fromMillisecondsSinceEpoch(
366 txInfo.timestamp() * 1000,
367 ),
368 isSpend = txInfo.direction() == monero.TransactionInfo_Direction.Out.index,
369 amount = txInfo.amount(),
370 paymentId = txInfo.paymentId(),
371 accountIndex = txInfo.subaddrAccount(),
372 addressIndex = int.tryParse(txInfo.subaddrIndex().split(", ")[0]) ?? 0,
373 addressIndexList =
374 txInfo.subaddrIndex().split(", ").map((e) => int.tryParse(e) ?? 0).toList(),
375 blockheight = txInfo.blockHeight(),
376 confirmations = txInfo.confirmations(),
377 fee = txInfo.fee(),
378 description = txInfo.description(),
379 key = getTxKey(txInfo.hash());
380
381 Transaction.dummy(
382 {required this.displayLabel,
383 required this.description,
384 required this.fee,
385 required this.confirmations,
386 required this.blockheight,
387 required this.accountIndex,
388 required this.addressIndexList,
389 required this.addressIndex,
390 required this.paymentId,
391 required this.amount,
392 required this.isSpend,
393 required this.hash,
394 required this.key,
395 required this.txInfo});
396 }