dev
dart 818 lines 27 KB
Raw
1 import 'dart:async';
2 import 'dart:ffi';
3 import 'dart:io';
4 import 'dart:isolate';
5
6 import 'package:cw_core/account.dart';
7 import 'package:cw_core/amount/money.dart';
8 import 'package:cw_core/crypto_currency.dart';
9 import 'package:cw_core/monero_transaction_priority.dart';
10 import 'package:cw_core/monero_wallet_keys.dart';
11 import 'package:cw_core/monero_wallet_utils.dart';
12 import 'package:cw_core/node.dart';
13 import 'package:cw_core/pathForWallet.dart';
14 import 'package:cw_core/pending_transaction.dart';
15 import 'package:cw_core/sync_status.dart';
16 import 'package:cw_core/transaction_direction.dart';
17 import 'package:cw_core/transaction_priority.dart';
18 import 'package:cw_core/unspent_coins_info.dart';
19 import 'package:cw_core/utils/print_verbose.dart';
20 import 'package:cw_core/utils/proxy_wrapper.dart';
21 import 'package:cw_core/wallet_base.dart';
22 import 'package:cw_core/wallet_info.dart';
23 import 'package:cw_core/wownero_amount_format.dart';
24 import 'package:cw_wownero/api/account_list.dart';
25 import 'package:cw_wownero/api/coins_info.dart';
26 import 'package:cw_wownero/api/structs/pending_transaction.dart';
27 import 'package:cw_wownero/api/transaction_history.dart' as transaction_history;
28 import 'package:cw_wownero/api/wallet.dart' as wownero_wallet;
29 import 'package:cw_wownero/api/wallet_manager.dart';
30 import 'package:cw_wownero/api/wownero_output.dart';
31 import 'package:cw_wownero/exceptions/wownero_transaction_creation_exception.dart';
32 import 'package:cw_wownero/exceptions/wownero_transaction_no_inputs_exception.dart';
33 import 'package:cw_wownero/pending_wownero_transaction.dart';
34 import 'package:cw_wownero/wownero_balance.dart';
35 import 'package:cw_wownero/wownero_transaction_creation_credentials.dart';
36 import 'package:cw_wownero/wownero_transaction_history.dart';
37 import 'package:cw_wownero/wownero_transaction_info.dart';
38 import 'package:cw_wownero/wownero_unspent.dart';
39 import 'package:cw_wownero/wownero_wallet_addresses.dart';
40 import 'package:flutter/foundation.dart';
41 import 'package:hive/hive.dart';
42 import 'package:mobx/mobx.dart';
43 import 'package:monero/wownero.dart' as wownero;
44
45 part 'wownero_wallet.g.dart';
46
47 const wowneroBlockSize = 1000;
48 // not sure if this should just be 0 but setting it higher feels safer / should catch more cases:
49 const MIN_RESTORE_HEIGHT = 1000;
50
51 class WowneroWallet = WowneroWalletBase with _$WowneroWallet;
52
53 abstract class WowneroWalletBase
54 extends WalletBase<WowneroBalance, WowneroTransactionHistory, WowneroTransactionInfo>
55 with Store {
56 WowneroWalletBase(
57 {required WalletInfo walletInfo,
58 required DerivationInfo derivationInfo,
59 required Box<UnspentCoinsInfo> unspentCoinsInfo,
60 required String password})
61 : balance = ObservableMap<CryptoCurrency, WowneroBalance>.of({
62 CryptoCurrency.wow: WowneroBalance(
63 fullBalance:
64 Money.fromInt(wownero_wallet.getFullBalance(accountIndex: 0), CryptoCurrency.wow),
65 unlockedBalance:
66 Money.fromInt(wownero_wallet.getFullBalance(accountIndex: 0), CryptoCurrency.wow),
67 )
68 }),
69 _isTransactionUpdating = false,
70 _hasSyncAfterStartup = false,
71 _password = password,
72 isEnabledAutoGenerateSubaddress = true,
73 syncStatus = NotConnectedSyncStatus(),
74 unspentCoins = [],
75 this.unspentCoinsInfo = unspentCoinsInfo,
76 super(walletInfo, derivationInfo) {
77 transactionHistory = WowneroTransactionHistory();
78 walletAddresses = WowneroWalletAddresses(walletInfo, transactionHistory);
79
80 _onAccountChangeReaction = reaction((_) => walletAddresses.account, (Account? account) {
81 if (account == null) return;
82
83 balance = ObservableMap<CryptoCurrency, WowneroBalance>.of(<CryptoCurrency, WowneroBalance>{
84 currency: WowneroBalance(
85 fullBalance: Money.fromInt(
86 wownero_wallet.getFullBalance(accountIndex: account.id), CryptoCurrency.wow),
87 unlockedBalance: Money.fromInt(
88 wownero_wallet.getUnlockedBalance(accountIndex: account.id), CryptoCurrency.wow),
89 )
90 });
91 _updateSubAddress(isEnabledAutoGenerateSubaddress, account: account);
92 _askForUpdateTransactionHistory();
93 });
94
95 reaction((_) => isEnabledAutoGenerateSubaddress, (bool enabled) {
96 _updateSubAddress(enabled, account: walletAddresses.account);
97 });
98
99 _onTxHistoryChangeReaction = reaction((_) => transactionHistory, (__) {
100 _updateSubAddress(isEnabledAutoGenerateSubaddress, account: walletAddresses.account);
101 });
102 }
103
104 static const int _autoSaveInterval = 30;
105
106 Box<UnspentCoinsInfo> unspentCoinsInfo;
107
108 void Function(FlutterErrorDetails)? onError;
109
110 @override
111 late WowneroWalletAddresses walletAddresses;
112
113 @override
114 @observable
115 bool isEnabledAutoGenerateSubaddress;
116
117 @override
118 @observable
119 SyncStatus syncStatus;
120
121 @override
122 @observable
123 ObservableMap<CryptoCurrency, WowneroBalance> balance;
124
125 @override
126 String get seed => wownero_wallet.getSeed();
127
128 String seedLegacy(String? language) => wownero_wallet.getSeedLegacy(language);
129
130 String get password => _password;
131
132 @override
133 String get passphrase => wownero_wallet.getPassphrase();
134
135 String _password;
136
137 @override
138 bool get hasRescan => true;
139
140 @override
141 MoneroWalletKeys get keys => MoneroWalletKeys(
142 primaryAddress: wownero_wallet.getAddress(accountIndex: 0, addressIndex: 0),
143 privateSpendKey: wownero_wallet.getSecretSpendKey(),
144 privateViewKey: wownero_wallet.getSecretViewKey(),
145 publicSpendKey: wownero_wallet.getPublicSpendKey(),
146 publicViewKey: wownero_wallet.getPublicViewKey(),
147 passphrase: wownero_wallet.getPassphrase());
148
149 int? get restoreHeight =>
150 transactionHistory.transactions.values.firstOrNull?.height ??
151 wownero.Wallet_getRefreshFromBlockHeight(wptr!);
152
153 wownero_wallet.SyncListener? _listener;
154 ReactionDisposer? _onAccountChangeReaction;
155 ReactionDisposer? _onTxHistoryChangeReaction;
156 bool _isTransactionUpdating;
157 bool _hasSyncAfterStartup;
158 Timer? _autoSaveTimer;
159 List<WowneroUnspent> unspentCoins;
160
161 Future<void> init() async {
162 await walletAddresses.init();
163 balance = ObservableMap<CryptoCurrency, WowneroBalance>.of(<CryptoCurrency, WowneroBalance>{
164 currency: WowneroBalance(
165 fullBalance: Money.fromInt(
166 wownero_wallet.getFullBalance(accountIndex: walletAddresses.account!.id),
167 CryptoCurrency.wow),
168 unlockedBalance: Money.fromInt(
169 wownero_wallet.getUnlockedBalance(accountIndex: walletAddresses.account!.id),
170 CryptoCurrency.wow),
171 )
172 });
173 _setListeners();
174 await updateTransactions();
175
176 if (walletInfo.isRecovery) {
177 wownero_wallet.setRecoveringFromSeed(isRecovery: walletInfo.isRecovery);
178
179 if (wownero_wallet.getCurrentHeight() <= 1) {
180 wownero_wallet.setRefreshFromBlockHeight(height: walletInfo.restoreHeight);
181 }
182 }
183
184 _autoSaveTimer =
185 Timer.periodic(Duration(seconds: _autoSaveInterval), (_) async => await save());
186 }
187
188 @override
189 Future<void>? updateBalance() => null;
190
191 @override
192 Future<bool> checkNodeHealth() async {
193 try {
194 // Check if the wallet is currently connected to the daemon
195 final isConnected = wownero_wallet.isConnectedSync();
196
197 if (!isConnected) {
198 return false; // It's not connected to daemon
199 }
200
201 // Check to get current node height to ensure daemon is responsive
202 final nodeHeight = await wownero_wallet.getNodeHeight();
203 return nodeHeight > 0;
204 } catch (e) {
205 return false;
206 }
207 }
208
209 @override
210 Future<void> close({bool shouldCleanup = false}) async {
211 _listener?.stop();
212 _onAccountChangeReaction?.reaction.dispose();
213 _onTxHistoryChangeReaction?.reaction.dispose();
214 _autoSaveTimer?.cancel();
215 }
216
217 @override
218 Future<void> connectToNode({required Node node}) async {
219 String socksProxy = node.socksProxyAddress ?? '';
220 printV("bootstrapped: ${CakeTor.instance!.bootstrapped}");
221 printV(" enabled: ${CakeTor.instance!.enabled}");
222 printV(" port: ${CakeTor.instance!.port}");
223 printV(" started: ${CakeTor.instance!.started}");
224 if (CakeTor.instance!.enabled) {
225 socksProxy = "127.0.0.1:${CakeTor.instance!.port}";
226 }
227 try {
228 syncStatus = ConnectingSyncStatus();
229 await wownero_wallet.setupNode(
230 address: node.uri.toString(),
231 login: node.login,
232 password: node.password,
233 useSSL: node.isSSL,
234 isLightWallet: false,
235 // FIXME: hardcoded value
236 socksProxyAddress: socksProxy);
237
238 wownero_wallet.setTrustedDaemon(node.trusted);
239 syncStatus = ConnectedSyncStatus();
240 } catch (e) {
241 syncStatus = FailedSyncStatus();
242 printV(e);
243 }
244 }
245
246 @override
247 Future<void> startSync() async {
248 try {
249 _assertInitialHeight();
250 } catch (_) {
251 // our restore height wasn't correct, so lets see if using the backup works:
252 try {
253 await resetCache(name);
254 _assertInitialHeight();
255 } catch (e) {
256 // we still couldn't get a valid height from the backup?!:
257 // try to use the date instead:
258 try {
259 _setHeightFromDate();
260 } catch (_) {
261 // we still couldn't get a valid sync height :/
262 }
263 }
264 }
265
266 try {
267 syncStatus = AttemptingSyncStatus();
268 wownero_wallet.startRefresh();
269 _setListeners();
270 _listener?.start();
271 } catch (e) {
272 syncStatus = FailedSyncStatus();
273 printV(e);
274 rethrow;
275 }
276 }
277
278 @override
279 Future<PendingTransaction> createTransaction(Object credentials) async {
280 final _credentials = credentials as WowneroTransactionCreationCredentials;
281 final inputs = <String>[];
282 final outputs = _credentials.outputs;
283 final hasMultiDestination = outputs.length > 1;
284 final unlockedBalance =
285 wownero_wallet.getUnlockedBalance(accountIndex: walletAddresses.account!.id);
286 var allInputsAmount = 0;
287
288 PendingTransactionDescription pendingTransactionDescription;
289
290 if (!(syncStatus is SyncedSyncStatus)) {
291 throw WowneroTransactionCreationException('The wallet is not synced.');
292 }
293
294 if (unspentCoins.isEmpty) {
295 await updateUnspent();
296 }
297
298 for (final utx in unspentCoins) {
299 if (utx.isSending) {
300 allInputsAmount += utx.value;
301 inputs.add(utx.keyImage!);
302 }
303 }
304 final spendAllCoins = inputs.length == unspentCoins.length;
305
306 if (hasMultiDestination) {
307 if (outputs.any((item) => item.sendAll || item.cryptoAmount.amount <= BigInt.zero)) {
308 throw WowneroTransactionCreationException(
309 'You do not have enough WOW to send this amount.');
310 }
311
312 final totalAmount = outputs.fold(0, (acc, value) => acc + value.cryptoAmount.amount.toInt());
313
314 final estimatedFee = calculateEstimatedFee(_credentials.priority, totalAmount);
315 if (unlockedBalance < totalAmount) {
316 throw WowneroTransactionCreationException(
317 'You do not have enough WOW to send this amount.');
318 }
319
320 if (!spendAllCoins && (allInputsAmount < totalAmount + estimatedFee)) {
321 throw WowneroTransactionNoInputsException(inputs.length);
322 }
323
324 final wowneroOutputs = outputs.map((output) {
325 final outputAddress = output.isParsedAddress ? output.extractedAddress : output.address;
326
327 return WowneroOutput(address: outputAddress!, amount: output.cryptoAmount.toString());
328 }).toList();
329
330 pendingTransactionDescription = await transaction_history.createTransactionMultDest(
331 outputs: wowneroOutputs,
332 priorityRaw: _credentials.priority.serialize(),
333 accountIndex: walletAddresses.account!.id,
334 preferredInputs: inputs);
335 } else {
336 final output = outputs.first;
337 final address = output.isParsedAddress ? output.extractedAddress : output.address;
338 final amount = output.sendAll ? null : output.cryptoAmount.toString();
339 final formattedAmount = output.sendAll ? null : output.cryptoAmount.amount.toInt();
340
341 if ((formattedAmount != null && unlockedBalance < formattedAmount) ||
342 (formattedAmount == null && unlockedBalance <= 0)) {
343 final formattedBalance = wowneroAmountToString(amount: unlockedBalance);
344
345 throw WowneroTransactionCreationException(
346 'You do not have enough unlocked balance. Unlocked: $formattedBalance. Transaction amount: ${output.cryptoAmount}.');
347 }
348
349 final estimatedFee = calculateEstimatedFee(_credentials.priority, formattedAmount);
350 if (!spendAllCoins &&
351 ((formattedAmount != null && allInputsAmount < (formattedAmount + estimatedFee)) ||
352 formattedAmount == null)) {
353 throw WowneroTransactionNoInputsException(inputs.length);
354 }
355
356 pendingTransactionDescription = await transaction_history.createTransaction(
357 address: address!,
358 amount: amount,
359 priorityRaw: _credentials.priority.serialize(),
360 accountIndex: walletAddresses.account!.id,
361 preferredInputs: inputs);
362 }
363
364 return PendingWowneroTransaction(pendingTransactionDescription);
365 }
366
367 @override
368 int calculateEstimatedFee(TransactionPriority priority, int? amount) {
369 // FIXME: hardcoded value;
370
371 if (priority is MoneroTransactionPriority) {
372 switch (priority) {
373 case MoneroTransactionPriority.slow:
374 return 24590000;
375 case MoneroTransactionPriority.automatic:
376 return 123050000;
377 case MoneroTransactionPriority.medium:
378 return 245029999;
379 case MoneroTransactionPriority.fast:
380 return 614530000;
381 case MoneroTransactionPriority.fastest:
382 return 26021600000;
383 }
384 }
385
386 return 0;
387 }
388
389 @override
390 Future<void> save() async {
391 await walletAddresses.updateUsedSubaddress();
392
393 if (isEnabledAutoGenerateSubaddress) {
394 walletAddresses.updateUnusedSubaddress(
395 accountIndex: walletAddresses.account?.id ?? 0,
396 defaultLabel: walletAddresses.account?.label ?? '');
397 }
398
399 await walletAddresses.updateAddressesInBox();
400 await wownero_wallet.store();
401 try {
402 await backupWalletFiles(name);
403 } catch (e) {
404 printV(\\_(ツ)_/¯");
405 printV(e);
406 }
407 }
408
409 @override
410 Future<void> renameWalletFiles(String newWalletName) async {
411 final currentWalletDirPath = await pathForWalletDir(name: name, type: type);
412 if (openedWalletsByPath["$currentWalletDirPath/$name"] != null) {
413 // NOTE: this is realistically only required on windows.
414 printV("closing wallet");
415 final wmaddr = wmPtr.address;
416 final waddr = openedWalletsByPath["$currentWalletDirPath/$name"]!.address;
417 await Isolate.run(() {
418 wownero.WalletManager_closeWallet(
419 Pointer.fromAddress(wmaddr), Pointer.fromAddress(waddr), true);
420 });
421 openedWalletsByPath.remove("$currentWalletDirPath/$name");
422 printV("wallet closed");
423 }
424 try {
425 // -- rename the waller folder --
426 final currentWalletDir = Directory(await pathForWalletDir(name: name, type: type));
427 final newWalletDirPath = await pathForWalletDir(name: newWalletName, type: type);
428 await currentWalletDir.rename(newWalletDirPath);
429
430 // -- use new waller folder to rename files with old names still --
431 final renamedWalletPath = newWalletDirPath + '/$name';
432
433 final currentCacheFile = File(renamedWalletPath);
434 final currentKeysFile = File('$renamedWalletPath.keys');
435 final currentAddressListFile = File('$renamedWalletPath.address.txt');
436
437 final newWalletPath = await pathForWallet(name: newWalletName, type: type);
438
439 if (currentCacheFile.existsSync()) {
440 await currentCacheFile.rename(newWalletPath);
441 }
442 if (currentKeysFile.existsSync()) {
443 await currentKeysFile.rename('$newWalletPath.keys');
444 }
445 if (currentAddressListFile.existsSync()) {
446 await currentAddressListFile.rename('$newWalletPath.address.txt');
447 }
448
449 await backupWalletFiles(newWalletName);
450 } catch (e) {
451 final currentWalletPath = await pathForWallet(name: name, type: type);
452
453 final currentCacheFile = File(currentWalletPath);
454 final currentKeysFile = File('$currentWalletPath.keys');
455 final currentAddressListFile = File('$currentWalletPath.address.txt');
456
457 final newWalletPath = await pathForWallet(name: newWalletName, type: type);
458
459 // Copies current wallet files into new wallet name's dir and files
460 if (currentCacheFile.existsSync()) {
461 await currentCacheFile.copy(newWalletPath);
462 }
463 if (currentKeysFile.existsSync()) {
464 await currentKeysFile.copy('$newWalletPath.keys');
465 }
466 if (currentAddressListFile.existsSync()) {
467 await currentAddressListFile.copy('$newWalletPath.address.txt');
468 }
469
470 // Delete old name's dir and files
471 await Directory(currentWalletDirPath).delete(recursive: true);
472 }
473 }
474
475 @override
476 Future<void> changePassword(String password) async => wownero_wallet.setPasswordSync(password);
477
478 Future<int> getNodeHeight() async => wownero_wallet.getNodeHeight();
479
480 Future<bool> isConnected() async => wownero_wallet.isConnected();
481
482 Future<void> setAsRecovered() async {
483 walletInfo.isRecovery = false;
484 await walletInfo.save();
485 }
486
487 @override
488 Future<void> rescan({required int height}) async {
489 walletInfo.restoreHeight = height;
490 walletInfo.isRecovery = true;
491 wownero_wallet.setRefreshFromBlockHeight(height: height);
492 wownero_wallet.rescanBlockchainAsync();
493 await startSync();
494 _askForUpdateBalance();
495 walletAddresses.accountList.update();
496 await _askForUpdateTransactionHistory();
497 await save();
498 await walletInfo.save();
499 }
500
501 Future<void> updateUnspent() async {
502 try {
503 refreshCoins(walletAddresses.account!.id);
504
505 unspentCoins.clear();
506
507 final coinCount = countOfCoins();
508 for (var i = 0; i < coinCount; i++) {
509 final coin = getCoin(i);
510 final coinSpent = wownero.CoinsInfo_spent(coin);
511 if (coinSpent == false) {
512 final unspent = WowneroUnspent(
513 wownero.CoinsInfo_address(coin),
514 wownero.CoinsInfo_hash(coin),
515 wownero.CoinsInfo_keyImage(coin),
516 wownero.CoinsInfo_amount(coin),
517 wownero.CoinsInfo_frozen(coin),
518 wownero.CoinsInfo_unlocked(coin),
519 );
520 if (unspent.hash.isNotEmpty) {
521 unspent.isChange = transaction_history.getTransaction(unspent.hash) == 1;
522 }
523 unspentCoins.add(unspent);
524 }
525 }
526
527 if (unspentCoinsInfo.isEmpty) {
528 unspentCoins.forEach((coin) => _addCoinInfo(coin));
529 return;
530 }
531
532 if (unspentCoins.isNotEmpty) {
533 unspentCoins.forEach((coin) {
534 final coinInfoList = unspentCoinsInfo.values.where((element) =>
535 element.walletId.contains(id) &&
536 element.accountIndex == walletAddresses.account!.id &&
537 element.keyImage!.contains(coin.keyImage!));
538
539 if (coinInfoList.isNotEmpty) {
540 final coinInfo = coinInfoList.first;
541
542 coin.isFrozen = coinInfo.isFrozen;
543 coin.isSending = coinInfo.isSending;
544 coin.note = coinInfo.note;
545 } else {
546 _addCoinInfo(coin);
547 }
548 });
549 }
550
551 await _refreshUnspentCoinsInfo();
552 _askForUpdateBalance();
553 } catch (e, s) {
554 printV(e.toString());
555 onError?.call(FlutterErrorDetails(
556 exception: e,
557 stack: s,
558 library: this.runtimeType.toString(),
559 ));
560 }
561 }
562
563 Future<void> _addCoinInfo(WowneroUnspent coin) async {
564 final newInfo = UnspentCoinsInfo(
565 walletId: id,
566 hash: coin.hash,
567 isFrozen: coin.isFrozen,
568 isSending: coin.isSending,
569 noteRaw: coin.note,
570 address: coin.address,
571 value: coin.value,
572 vout: 0,
573 keyImage: coin.keyImage,
574 isChange: coin.isChange,
575 accountIndex: walletAddresses.account!.id);
576
577 await unspentCoinsInfo.add(newInfo);
578 }
579
580 Future<void> _refreshUnspentCoinsInfo() async {
581 try {
582 final List<dynamic> keys = <dynamic>[];
583 final currentWalletUnspentCoins = unspentCoinsInfo.values.where((element) =>
584 element.walletId.contains(id) && element.accountIndex == walletAddresses.account!.id);
585
586 if (currentWalletUnspentCoins.isNotEmpty) {
587 currentWalletUnspentCoins.forEach((element) {
588 final existUnspentCoins =
589 unspentCoins.where((coin) => element.keyImage!.contains(coin.keyImage!));
590
591 if (existUnspentCoins.isEmpty) {
592 keys.add(element.key);
593 }
594 });
595 }
596
597 if (keys.isNotEmpty) {
598 await unspentCoinsInfo.deleteAll(keys);
599 }
600 } catch (e) {
601 printV(e.toString());
602 }
603 }
604
605 String getTransactionAddress(int accountIndex, int addressIndex) =>
606 wownero_wallet.getAddress(accountIndex: accountIndex, addressIndex: addressIndex);
607
608 @override
609 Future<Map<String, WowneroTransactionInfo>> fetchTransactions() async {
610 transaction_history.refreshTransactions();
611 return (await _getAllTransactionsOfAccount(walletAddresses.account?.id))
612 .fold<Map<String, WowneroTransactionInfo>>(<String, WowneroTransactionInfo>{},
613 (Map<String, WowneroTransactionInfo> acc, WowneroTransactionInfo tx) {
614 acc[tx.id] = tx;
615 return acc;
616 });
617 }
618
619 Future<void> updateTransactions() async {
620 try {
621 if (_isTransactionUpdating) {
622 return;
623 }
624
625 _isTransactionUpdating = true;
626 final transactions = await fetchTransactions();
627 transactionHistory.clear();
628 transactionHistory.addMany(transactions);
629 await transactionHistory.save();
630 _isTransactionUpdating = false;
631 } catch (e) {
632 printV(e);
633 _isTransactionUpdating = false;
634 }
635 }
636
637 String getSubaddressLabel(int accountIndex, int addressIndex) =>
638 wownero_wallet.getSubaddressLabel(accountIndex, addressIndex);
639
640 Future<List<WowneroTransactionInfo>> _getAllTransactionsOfAccount(int? accountIndex) async =>
641 (await transaction_history.getAllTransactions())
642 .map(
643 (row) => WowneroTransactionInfo(
644 row.hash,
645 row.blockheight,
646 row.isSpend ? TransactionDirection.outgoing : TransactionDirection.incoming,
647 row.timeStamp,
648 row.isPending,
649 Money.fromInt(row.amount, CryptoCurrency.wow),
650 row.accountIndex,
651 0,
652 Money.fromInt(row.fee, CryptoCurrency.wow),
653 row.confirmations,
654 )..additionalInfo = <String, dynamic>{
655 'key': row.key,
656 'accountIndex': row.accountIndex,
657 'addressIndex': row.addressIndex
658 },
659 )
660 .where((element) => element.accountIndex == (accountIndex ?? 0))
661 .toList();
662
663 void _setListeners() {
664 _listener?.stop();
665 _listener = wownero_wallet.setListeners(_onNewBlock, _onNewTransaction);
666 }
667
668 /// Asserts the current height to be above [MIN_RESTORE_HEIGHT]
669 void _assertInitialHeight() {
670 if (walletInfo.isRecovery) return;
671
672 final height = wownero_wallet.getCurrentHeight();
673
674 // the restore height is probably correct, so we do nothing:
675 if (height > MIN_RESTORE_HEIGHT) return;
676
677 throw Exception("height isn't > $MIN_RESTORE_HEIGHT!");
678 }
679
680 void _setHeightFromDate() {
681 if (walletInfo.isRecovery) {
682 return;
683 }
684
685 int height = 0;
686 try {
687 height = _getHeightByDate(walletInfo.date);
688 } catch (_) {}
689
690 wownero_wallet.setRecoveringFromSeed(isRecovery: true);
691 wownero_wallet.setRefreshFromBlockHeight(height: height);
692 }
693
694 int _getHeightDistance(DateTime date) {
695 final distance = DateTime.now().millisecondsSinceEpoch - date.millisecondsSinceEpoch;
696 final daysTmp = (distance / 86400).round();
697 final days = daysTmp < 1 ? 1 : daysTmp;
698
699 return days * 1000;
700 }
701
702 int _getHeightByDate(DateTime date) {
703 final nodeHeight = wownero_wallet.getNodeHeightSync();
704 final heightDistance = _getHeightDistance(date);
705
706 if (nodeHeight <= 0) {
707 // the node returned 0 (an error state)
708 throw Exception("nodeHeight is <= 0!");
709 }
710
711 return nodeHeight - heightDistance;
712 }
713
714 void _askForUpdateBalance() {
715 final unlockedBalance = _getUnlockedBalance();
716 final fullBalance = _getFullBalance();
717 final frozenBalance = _getFrozenBalance();
718
719 if (balance[currency]!.fullBalance != fullBalance ||
720 balance[currency]!.available != unlockedBalance ||
721 balance[currency]!.frozen != frozenBalance) {
722 balance[currency] = WowneroBalance(
723 fullBalance: fullBalance, unlockedBalance: unlockedBalance, frozen: frozenBalance);
724 }
725 }
726
727 Future<void> _askForUpdateTransactionHistory() async => await updateTransactions();
728
729 Money _getFullBalance() => Money.fromInt(
730 wownero_wallet.getFullBalance(accountIndex: walletAddresses.account!.id), CryptoCurrency.wow);
731
732 Money _getUnlockedBalance() => Money.fromInt(
733 wownero_wallet.getUnlockedBalance(accountIndex: walletAddresses.account!.id),
734 CryptoCurrency.wow);
735
736 Money _getFrozenBalance() {
737 var frozenBalance = 0;
738
739 for (final coin in unspentCoinsInfo.values.where((element) =>
740 element.walletId == id && element.accountIndex == walletAddresses.account!.id)) {
741 if (coin.isFrozen) frozenBalance += coin.value;
742 }
743
744 return Money.fromInt(frozenBalance, CryptoCurrency.wow);
745 }
746
747 void _onNewBlock(int height, int blocksLeft, double ptc) async {
748 try {
749 if (walletInfo.isRecovery) {
750 await _askForUpdateTransactionHistory();
751 _askForUpdateBalance();
752 walletAddresses.accountList.update();
753 }
754
755 if (blocksLeft < 100) {
756 await _askForUpdateTransactionHistory();
757 _askForUpdateBalance();
758 walletAddresses.accountList.update();
759 syncStatus = SyncedSyncStatus();
760
761 if (!_hasSyncAfterStartup) {
762 _hasSyncAfterStartup = true;
763 await save();
764 }
765
766 if (walletInfo.isRecovery) {
767 await setAsRecovered();
768 }
769 } else {
770 syncStatus = SyncingSyncStatus(blocksLeft, ptc);
771 }
772 } catch (e) {
773 printV(e.toString());
774 }
775 }
776
777 void _onNewTransaction() async {
778 try {
779 await _askForUpdateTransactionHistory();
780 _askForUpdateBalance();
781 await Future<void>.delayed(Duration(seconds: 1));
782 } catch (e) {
783 printV(e.toString());
784 }
785 }
786
787 void _updateSubAddress(bool enableAutoGenerate, {Account? account}) {
788 if (enableAutoGenerate) {
789 walletAddresses.updateUnusedSubaddress(
790 accountIndex: account?.id ?? 0,
791 defaultLabel: account?.label ?? '',
792 );
793 } else {
794 walletAddresses.updateSubaddressList(accountIndex: account?.id ?? 0);
795 }
796 }
797
798 @override
799 void setExceptionHandler(void Function(FlutterErrorDetails) e) => onError = e;
800
801 @override
802 Future<String> signMessage(String message, {String? address}) async {
803 final useAddress = address ?? "";
804 return wownero_wallet.signMessage(message, address: useAddress);
805 }
806
807 @override
808 Future<bool> verifyMessage(String message, String signature, {String? address = null}) async {
809 if (address == null) return false;
810
811 return wownero_wallet.verifyMessage(message, address, signature);
812 }
813
814 @override
815 String formatCryptoAmount(String amount) {
816 return wowneroAmountToString(amount: int.parse(amount));
817 }
818 }