dev
dart 790 lines 23.7 KB
Raw
1 import 'dart:async';
2 import 'dart:convert';
3 import 'dart:io';
4 import 'package:cw_core/amount/money.dart';
5 import 'package:path/path.dart' as p;
6 import 'package:cw_core/exceptions.dart';
7 import 'package:cw_core/transaction_direction.dart';
8 import 'package:cw_core/utils/print_verbose.dart';
9 import 'package:cw_core/pathForWallet.dart';
10 import 'package:cw_core/wallet_type.dart';
11 import 'package:cw_decred/pending_transaction.dart';
12 import 'package:cw_decred/transaction_credentials.dart';
13 import 'package:flutter/foundation.dart';
14 import 'package:mobx/mobx.dart';
15 import 'package:hive/hive.dart';
16
17 import 'package:cw_decred/api/libdcrwallet.dart';
18 import 'package:cw_decred/transaction_history.dart';
19 import 'package:cw_decred/wallet_addresses.dart';
20 import 'package:cw_decred/transaction_priority.dart';
21 import 'package:cw_decred/wallet_service.dart';
22 import 'package:cw_decred/balance.dart';
23 import 'package:cw_decred/transaction_info.dart';
24 import 'package:cw_core/crypto_currency.dart';
25 import 'package:cw_core/encryption_file_utils.dart';
26 import 'package:cw_core/wallet_info.dart';
27 import 'package:cw_core/wallet_base.dart';
28 import 'package:cw_core/wallet_keys_file.dart';
29 import 'package:cw_core/transaction_priority.dart';
30 import 'package:cw_core/pending_transaction.dart';
31 import 'package:cw_core/sync_status.dart';
32 import 'package:cw_core/node.dart';
33 import 'package:cw_core/unspent_coins_info.dart';
34 import 'package:cw_core/unspent_transaction_output.dart';
35
36 part 'wallet.g.dart';
37
38 class DecredWallet = DecredWalletBase with _$DecredWallet;
39
40 abstract class DecredWalletBase
41 extends WalletBase<DecredBalance, DecredTransactionHistory, DecredTransactionInfo>
42 with Store, WalletKeysFile {
43 DecredWalletBase(WalletInfo walletInfo, DerivationInfo derivationInfo, String password,
44 Box<UnspentCoinsInfo> unspentCoinsInfo, Libwallet libwallet, Function() closeLibwallet,
45 {this.passphrase, required this.encryptionFileUtils})
46 : _password = password,
47 _libwallet = libwallet,
48 _closeLibwallet = closeLibwallet,
49 this.syncStatus = NotConnectedSyncStatus(),
50 this.unspentCoinsInfo = unspentCoinsInfo,
51 this.watchingOnly =
52 derivationInfo.derivationPath == DecredWalletService.pubkeyRestorePath ||
53 derivationInfo.derivationPath == DecredWalletService.pubkeyRestorePathTestnet,
54 this.balance = ObservableMap.of({CryptoCurrency.dcr: DecredBalance.zero()}),
55 this.isTestnet =
56 derivationInfo.derivationPath == DecredWalletService.seedRestorePathTestnet ||
57 derivationInfo.derivationPath == DecredWalletService.pubkeyRestorePathTestnet,
58 super(walletInfo, derivationInfo) {
59 walletAddresses = DecredWalletAddresses(walletInfo, libwallet, isTestnet);
60 transactionHistory = DecredTransactionHistory();
61
62 reaction((_) => isEnabledAutoGenerateSubaddress, (bool enabled) {
63 this.walletAddresses.isEnabledAutoGenerateSubaddress = enabled;
64 });
65 }
66
67 // NOTE: Hitting this max fee would be unexpected with current on chain use
68 // but this may need to be updated in the future.
69 final maxFeeRate = 100000;
70
71 // syncIntervalSyncing is used up until synced, then transactions are checked
72 // every syncIntervalSynced.
73 final syncIntervalSyncing = 5; // seconds
74 final syncIntervalSynced = 30; // seconds
75 static final defaultFeeRate = 10000;
76 final String _password;
77 final Libwallet _libwallet;
78 final Function() _closeLibwallet;
79 final idPrefix = "decred_";
80
81 // TODO: Encrypt this.
82 var _seed = "";
83 var _pubkey = "";
84 var _unspents = <Unspent>[];
85
86 // synced is used to set the syncTimer interval.
87 bool synced = false;
88 bool watchingOnly;
89 bool connecting = false;
90 String persistantPeer = "default-spv-nodes";
91 FeeCache feeRateFast = FeeCache(defaultFeeRate);
92 FeeCache feeRateMedium = FeeCache(defaultFeeRate);
93 FeeCache feeRateSlow = FeeCache(defaultFeeRate);
94 Timer? syncTimer;
95 Box<UnspentCoinsInfo> unspentCoinsInfo;
96
97 @override
98 @observable
99 bool isEnabledAutoGenerateSubaddress = true;
100
101 @override
102 @observable
103 SyncStatus syncStatus;
104
105 @override
106 @observable
107 late ObservableMap<CryptoCurrency, DecredBalance> balance;
108
109 @override
110 late DecredWalletAddresses walletAddresses;
111
112 @override
113 String? get seed {
114 if (watchingOnly) {
115 return null;
116 }
117 return _seed;
118 }
119
120 @override
121 final String? passphrase;
122
123 final EncryptionFileUtils encryptionFileUtils;
124
125 @override
126 WalletKeysData get walletKeysData => WalletKeysData(
127 mnemonic: seed,
128 passphrase: passphrase,
129 );
130
131 @override
132 Object get keys => {};
133
134 @override
135 bool isTestnet;
136
137 String get pubkey => _pubkey;
138
139 Future<void> init() async {
140 final getSeed = () async {
141 if (!watchingOnly) {
142 _seed = await _libwallet.walletSeed(walletInfo.name, _password) ?? "";
143 }
144 _pubkey = await _libwallet.defaultPubkey(walletInfo.name);
145 };
146 await Future.wait([
147 updateBalance(),
148 updateTransactionHistory(),
149 walletAddresses.init(),
150 fetchTransactions(),
151 updateFees(),
152 fetchUnspents(),
153 getSeed(),
154 ]);
155 }
156
157 Future<void> performBackgroundTasks() async {
158 if (!await checkSync()) {
159 if (synced == true) {
160 synced = false;
161 if (syncTimer != null) {
162 syncTimer!.cancel();
163 }
164 syncTimer = Timer.periodic(
165 Duration(seconds: syncIntervalSyncing), (Timer t) => performBackgroundTasks());
166 }
167 return;
168 }
169 // Set sync check interval lower since we are synced.
170 if (synced == false) {
171 synced = true;
172 if (syncTimer != null) {
173 syncTimer!.cancel();
174 }
175 syncTimer = Timer.periodic(
176 Duration(seconds: syncIntervalSynced), (Timer t) => performBackgroundTasks());
177 }
178 await Future.wait([
179 updateTransactionHistory(),
180 updateFees(),
181 fetchUnspents(),
182 updateBalance(),
183 walletAddresses.updateAddressesInBox(),
184 ]);
185 }
186
187 Future<void> updateFees() async {
188 final feeForNb = (int nb) async {
189 try {
190 final feeStr = await _libwallet.estimateFee(walletInfo.name, nb);
191 var fee = int.parse(feeStr);
192 if (fee > maxFeeRate) {
193 throw "dcr fee returned from estimate fee was over max";
194 } else if (fee <= 0) {
195 throw "dcr fee returned from estimate fee was zero";
196 }
197 return fee;
198 } catch (e) {
199 printV(e);
200 return defaultFeeRate;
201 }
202 };
203 if (feeRateSlow.isOld()) {
204 feeRateSlow.update(await feeForNb(4));
205 }
206 if (feeRateMedium.isOld()) {
207 feeRateMedium.update(await feeForNb(2));
208 }
209 if (feeRateFast.isOld()) {
210 feeRateFast.update(await feeForNb(1));
211 }
212 }
213
214 Future<void> updateTransactionHistory() async {
215 // from is the number of transactions skipped from most recent, not block
216 // height.
217 var from = 0;
218 while (true) {
219 // Transactions are returned from newest to oldest. Loop fetching 5 txn
220 // at a time until we find a batch with txn that no longer need to be
221 // updated.
222 final txs = await this.fetchFiveTransactions(from);
223 if (txs.length == 0) {
224 return;
225 }
226 if (this.transactionHistory.update(txs)) {
227 return;
228 }
229 from += 5;
230 }
231 }
232
233 Future<bool> checkSync() async {
234 final syncStatusJSON = await _libwallet.syncStatus(walletInfo.name);
235 final decoded = json.decode(syncStatusJSON.isEmpty ? "{}" : syncStatusJSON);
236
237 final syncStatusCode = decoded["syncstatuscode"] ?? 0;
238 // final syncStatusStr = decoded["syncstatus"] ?? "";
239 final targetHeight = decoded["targetheight"] ?? 1;
240 final numPeers = decoded["numpeers"] ?? 0;
241 // final cFiltersHeight = decoded["cfiltersheight"] ?? 0;
242 final headersHeight = decoded["headersheight"] ?? 0;
243 final rescanHeight = decoded["rescanheight"] ?? 0;
244
245 if (numPeers == 0) {
246 syncStatus = NotConnectedSyncStatus();
247 return false;
248 }
249
250 // Sync codes:
251 // NotStarted = 0
252 // FetchingCFilters = 1
253 // FetchingHeaders = 2
254 // DiscoveringAddrs = 3
255 // Rescanning = 4
256 // Complete = 5
257
258 if (syncStatusCode > 4) {
259 syncStatus = SyncedSyncStatus();
260 return true;
261 }
262
263 if (syncStatusCode == 0) {
264 syncStatus = ConnectedSyncStatus();
265 return false;
266 }
267
268 if (syncStatusCode == 1) {
269 syncStatus = SyncingSyncStatus(targetHeight, 0.0);
270 return false;
271 }
272
273 if (syncStatusCode == 2) {
274 final headersProg = headersHeight / targetHeight;
275 // Only allow headers progress to go up half way.
276 syncStatus = SyncingSyncStatus(targetHeight - headersHeight, headersProg);
277 return false;
278 }
279
280 // TODO: This step takes a while so should really get more info to the UI
281 // that we are discovering addresses.
282 if (syncStatusCode == 3) {
283 // Hover at half.
284 syncStatus = ProcessingSyncStatus();
285 return false;
286 }
287
288 if (syncStatusCode == 4) {
289 // Start at 75%.
290 final rescanProg = rescanHeight / targetHeight / 4;
291 syncStatus = SyncingSyncStatus(targetHeight - rescanHeight, .75 + rescanProg);
292 return false;
293 }
294 return false;
295 }
296
297 @action
298 @override
299 Future<void> connectToNode({required Node node}) async {
300 if (connecting) {
301 return;
302 }
303 connecting = true;
304 String addr = "default-spv-nodes";
305 if (node.uri.host != addr) {
306 addr = node.uri.host;
307 if (node.uri.port != "") {
308 addr += ":" + node.uri.port.toString();
309 }
310 }
311 if (addr != persistantPeer) {
312 if (syncTimer != null) {
313 syncTimer!.cancel();
314 syncTimer = null;
315 }
316 persistantPeer = addr;
317 await _libwallet.closeWallet(walletInfo.name);
318 final network = isTestnet ? "testnet" : "mainnet";
319 final dirPath = await pathForWalletDir(name: walletInfo.name, type: WalletType.decred);
320 final config = {
321 "name": walletInfo.name,
322 "datadir": dirPath,
323 "net": network,
324 "unsyncedaddrs": true,
325 };
326 await _libwallet.loadWallet(jsonEncode(config));
327 }
328 await this._startSync();
329 connecting = false;
330 }
331
332 @action
333 @override
334 Future<void> startSync() async {
335 if (connecting) {
336 return;
337 }
338 connecting = true;
339 await this._startSync();
340 connecting = false;
341 }
342
343 Future<void> _startSync() async {
344 if (syncTimer != null) {
345 return;
346 }
347 try {
348 syncStatus = ConnectingSyncStatus();
349 await _libwallet.startSync(
350 walletInfo.name,
351 persistantPeer == "default-spv-nodes" ? "" : persistantPeer,
352 );
353 syncTimer = Timer.periodic(
354 Duration(seconds: syncIntervalSyncing), (Timer t) => performBackgroundTasks());
355 } catch (e) {
356 printV(e.toString());
357 syncStatus = FailedSyncStatus();
358 }
359 }
360
361 @override
362 Future<PendingTransaction> createTransaction(Object credentials) async {
363 if (watchingOnly) {
364 return DecredPendingTransaction(
365 txId: "",
366 amount: Money.zero(currency),
367 fee: Money.zero(currency),
368 rawHex: "",
369 send: () async => throw "unable to send with watching only wallet",
370 );
371 }
372 var totalIn = 0;
373 final ignoreInputs = [];
374 this.unspentCoinsInfo.values.forEach((unspent) {
375 if (unspent.isFrozen || !unspent.isSending) {
376 final input = {"txid": unspent.hash, "vout": unspent.vout};
377 ignoreInputs.add(input);
378 return;
379 }
380 totalIn += unspent.value;
381 });
382
383 final creds = credentials as DecredTransactionCredentials;
384 var totalAmt = 0;
385 var sendAll = false;
386 final outputs = [];
387 for (final out in creds.outputs) {
388 var amt = 0;
389 if (out.sendAll) {
390 if (creds.outputs.length != 1) throw "can only send all to one output";
391
392 sendAll = true;
393 totalAmt = totalIn;
394 } else {
395 amt = out.cryptoAmount.amount.toInt();
396 }
397 totalAmt += amt;
398 final o = {
399 "address": out.isParsedAddress ? out.extractedAddress! : out.address,
400 "amount": amt
401 };
402 outputs.add(o);
403 }
404
405 // throw exception if no selected coins under coin control
406 // or if the total coins selected, is less than the amount the user wants to spend
407 if (ignoreInputs.length == unspentCoinsInfo.values.length || totalIn < totalAmt) {
408 throw TransactionNoInputsException();
409 }
410
411 // The inputs are always used. Currently we don't have use for this
412 // argument. sendall ingores output value and sends everything.
413 final signReq = {
414 // "inputs": inputs,
415 "ignoreInputs": ignoreInputs,
416 "outputs": outputs,
417 "feerate": creds.feeRate ?? defaultFeeRate,
418 "password": _password,
419 "sendall": sendAll,
420 "sign": true,
421 };
422 final res = await _libwallet.createSignedTransaction(walletInfo.name, jsonEncode(signReq));
423 final decoded = json.decode(res);
424 final signedHex = decoded["hex"];
425 final send = () async {
426 await _libwallet.sendRawTransaction(walletInfo.name, signedHex);
427 await updateBalance();
428 };
429 final fee = decoded["fee"] ?? 0;
430 if (sendAll) {
431 totalAmt = (totalAmt - fee).round();
432 }
433
434 return DecredPendingTransaction(
435 txId: decoded["txid"] ?? "",
436 amount: Money.fromInt(totalAmt, currency),
437 fee: Money.fromInt(fee, currency),
438 rawHex: signedHex,
439 send: send,
440 );
441 }
442
443 int feeRate(TransactionPriority priority) {
444 if (!(priority is DecredTransactionPriority)) {
445 return defaultFeeRate;
446 }
447 final p = priority;
448 switch (p) {
449 case DecredTransactionPriority.slow:
450 return feeRateSlow.feeRate();
451 case DecredTransactionPriority.medium:
452 return feeRateMedium.feeRate();
453 case DecredTransactionPriority.fast:
454 return feeRateFast.feeRate();
455 }
456 return defaultFeeRate;
457 }
458
459 @override
460 int calculateEstimatedFee(TransactionPriority priority, int? amount) {
461 if (priority is DecredTransactionPriority) {
462 final P2PKHOutputSize =
463 36; // 8 bytes value + 2 bytes version + at least 1 byte varint script size + P2PKHPkScriptSize
464 // MsgTxOverhead is 4 bytes version (lower 2 bytes for the real transaction
465 // version and upper 2 bytes for the serialization type) + 4 bytes locktime
466 // + 4 bytes expiry + 3 bytes of varints for the number of transaction
467 // inputs (x2 for witness and prefix) and outputs
468 final MsgTxOverhead = 15;
469 // TxInOverhead is the overhead for a wire.TxIn with a scriptSig length <
470 // 254. prefix (41 bytes) + ValueIn (8 bytes) + BlockHeight (4 bytes) +
471 // BlockIndex (4 bytes) + sig script var int (at least 1 byte)
472 final TxInOverhead = 57;
473 final P2PKHInputSize =
474 TxInOverhead + 109; // TxInOverhead (57) + var int (1) + P2PKHSigScriptSize (108)
475
476 int inputsCount = 1;
477 if (amount != null) {
478 inputsCount += _unspents.where((e) {
479 amount = (amount!) - e.value;
480 return (amount!) > 0;
481 }).length;
482 }
483
484 // Estimate using a transaction consuming inoutsCount and paying to one address with change.
485 return (this.feeRate(priority) / 1000).round() *
486 (MsgTxOverhead + P2PKHInputSize * inputsCount + P2PKHOutputSize * 2);
487 }
488 return 0;
489 }
490
491 @override
492 Future<Map<String, DecredTransactionInfo>> fetchTransactions() => fetchFiveTransactions(0);
493
494 Future<Map<String, DecredTransactionInfo>> fetchFiveTransactions(int from) async {
495 try {
496 final res = await _libwallet.listTransactions(walletInfo.name, from.toString(), "5");
497 final decoded = json.decode(res);
498 final txs = <String, DecredTransactionInfo>{};
499
500 for (final d in decoded) {
501 final txid = uniqueTxID(d["txid"] ?? "", d["vout"] ?? 0);
502 var direction = TransactionDirection.outgoing;
503 if (d["category"] == "receive") {
504 direction = TransactionDirection.incoming;
505 }
506 final amountDouble = d["amount"] ?? 0.0;
507 final amount = (amountDouble * 1e8).round().abs();
508 final feeDouble = d["fee"] ?? 0.0;
509 final fee = (feeDouble * 1e8).round().abs();
510 final confs = d["confirmations"] ?? 0;
511 final sendTime = d["time"] ?? 0;
512
513 txs[txid] = DecredTransactionInfo(
514 id: txid,
515 amount: Money.fromInt(amount, currency),
516 fee: Money.fromInt(fee, currency),
517 direction: direction,
518 isPending: confs == 0,
519 date: DateTime.fromMillisecondsSinceEpoch(sendTime * 1000, isUtc: false),
520 height: d["height"] ?? 0,
521 confirmations: confs,
522 to: d["address"] ?? "",
523 );
524 }
525 return txs;
526 } catch (e) {
527 printV(e);
528 return {};
529 }
530 }
531
532 // uniqueTxID combines the tx id and vout to create a unique id.
533 String uniqueTxID(String id, int vout) => "$id:$vout";
534
535 @override
536 Future<void> save() async {
537 if (watchingOnly) {
538 return;
539 }
540 await saveKeysFile(_password, encryptionFileUtils);
541 }
542
543 @override
544 bool get hasRescan => walletBirthdayBlockHeight() != -1;
545
546 @override
547 Future<void> rescan({required int height}) async {
548 // The required height is not used. A birthday time is recorded in the
549 // mnemonic. As long as not private data is imported into the wallet, we
550 // can always rescan from there.
551 var rescanHeight = 0;
552 if (!watchingOnly) {
553 rescanHeight = await walletBirthdayBlockHeight();
554 // Sync has not yet reached the birthday block.
555 if (rescanHeight == -1) {
556 return;
557 }
558 }
559 await _libwallet.rescanFromHeight(walletInfo.name, rescanHeight.toString());
560 }
561
562 @override
563 Future<void> close({bool shouldCleanup = false}) async {
564 if (syncTimer != null) {
565 syncTimer!.cancel();
566 syncTimer = null;
567 }
568 await _libwallet.closeWallet(walletInfo.name);
569 if (shouldCleanup) {
570 await _libwallet.shutdown();
571 _closeLibwallet();
572 }
573 }
574
575 @override
576 Future<void> changePassword(String password) async {
577 if (watchingOnly) {
578 return;
579 }
580 return () async {
581 await _libwallet.changeWalletPassword(walletInfo.name, _password, password);
582 }();
583 }
584
585 @override
586 Future<void> updateBalance() async {
587 final balanceMap = await _libwallet.balance(walletInfo.name);
588
589 var totalFrozen = 0;
590
591 unspentCoinsInfo.values.forEach((info) {
592 _unspents.forEach((element) {
593 if (element.hash == info.hash &&
594 element.vout == info.vout &&
595 info.isFrozen &&
596 element.value == info.value) {
597 totalFrozen += element.value;
598 }
599 });
600 });
601
602 balance[CryptoCurrency.dcr] = DecredBalance(
603 confirmed: Money.fromInt(balanceMap["confirmed"] ?? 0, currency),
604 unconfirmed: Money.fromInt(balanceMap["unconfirmed"] ?? 0, currency),
605 frozen: Money.fromInt(totalFrozen, currency),
606 );
607 }
608
609 @override
610 Future<bool> checkNodeHealth() async => await checkSync();
611
612 @override
613 void setExceptionHandler(void Function(FlutterErrorDetails) onError) => onError;
614
615 Future<void> renameWalletFiles(String newWalletName) async {
616 final currentDirPath = await pathForWalletDir(name: walletInfo.name, type: type);
617
618 final newDirPath = await pathForWalletDir(name: newWalletName, type: type);
619
620 if (File(newDirPath).existsSync()) {
621 throw "wallet already exists at $newDirPath";
622 }
623
624 final sourceDir = Directory(currentDirPath);
625 final targetDir = Directory(newDirPath);
626
627 if (!targetDir.existsSync()) {
628 await targetDir.create(recursive: true);
629 }
630
631 await for (final entity in sourceDir.list(recursive: true)) {
632 final relativePath = entity.path.substring(sourceDir.path.length + 1);
633 final targetPath = p.join(targetDir.path, relativePath);
634
635 if (entity is File) {
636 await entity.rename(targetPath);
637 } else if (entity is Directory) {
638 await Directory(targetPath).create(recursive: true);
639 }
640 }
641
642 await sourceDir.delete(recursive: true);
643
644 for (final suffix in const ['.keys', '.keys.backup']) {
645 final file = File(p.join(newDirPath, '${walletInfo.name}$suffix'));
646 if (file.existsSync()) {
647 await file.rename(p.join(newDirPath, '$newWalletName$suffix'));
648 }
649 }
650 }
651
652 @override
653 Future<String> signMessage(String message, {String? address = null}) async {
654 if (watchingOnly) {
655 throw "a watching only wallet cannot sign";
656 }
657 var addr = address;
658 if (addr == null) {
659 addr = walletAddresses.address;
660 }
661 if (addr == "") {
662 throw "unable to get an address from unsynced wallet";
663 }
664 return await _libwallet.signMessage(walletInfo.name, message, addr, _password);
665 }
666
667 Future<void> fetchUnspents() async {
668 try {
669 final res = await _libwallet.listUnspents(walletInfo.name);
670 final decoded = json.decode(res);
671 final unspents = <Unspent>[];
672 for (final d in decoded) {
673 final spendable = d["spendable"] ?? false;
674 if (!spendable) {
675 continue;
676 }
677 final amountDouble = d["amount"] ?? 0.0;
678 final amount = (amountDouble * 1e8).round().abs();
679 final utxo = Unspent(d["address"] ?? "", d["txid"] ?? "", amount, d["vout"] ?? 0, null);
680 utxo.isChange = d["ischange"] ?? false;
681 unspents.add(utxo);
682 }
683 _unspents = unspents;
684 } catch (e) {
685 printV(e);
686 }
687 }
688
689 List<Unspent> unspents() {
690 this.updateUnspents(_unspents);
691 return _unspents;
692 }
693
694 void updateUnspents(List<Unspent> unspentCoins) {
695 if (this.unspentCoinsInfo.isEmpty) {
696 unspentCoins.forEach((coin) => this.addCoinInfo(coin));
697 return;
698 }
699
700 if (unspentCoins.isEmpty) {
701 this.unspentCoinsInfo.clear();
702 return;
703 }
704
705 final walletID = idPrefix + walletInfo.name;
706 if (unspentCoins.isNotEmpty) {
707 unspentCoins.forEach((coin) {
708 final coinInfoList = this.unspentCoinsInfo.values.where((element) =>
709 element.walletId == walletID && element.hash == coin.hash && element.vout == coin.vout);
710
711 if (coinInfoList.isEmpty) {
712 this.addCoinInfo(coin);
713 } else {
714 final coinInfo = coinInfoList.first;
715
716 coin.isFrozen = coinInfo.isFrozen;
717 coin.isSending = coinInfo.isSending;
718 coin.note = coinInfo.note;
719 }
720 });
721 }
722
723 final List<dynamic> keys = <dynamic>[];
724 this.unspentCoinsInfo.values.forEach((element) {
725 final existUnspentCoins = unspentCoins.where((coin) => element.hash.contains(coin.hash));
726
727 if (existUnspentCoins.isEmpty) {
728 keys.add(element.key);
729 }
730 });
731
732 if (keys.isNotEmpty) {
733 unspentCoinsInfo.deleteAll(keys);
734 }
735 }
736
737 void addCoinInfo(Unspent coin) {
738 final newInfo = UnspentCoinsInfo(
739 walletId: idPrefix + walletInfo.name,
740 hash: coin.hash,
741 isFrozen: false,
742 isSending: coin.isSending,
743 noteRaw: "",
744 address: coin.address,
745 value: coin.value,
746 vout: coin.vout,
747 isChange: coin.isChange,
748 keyImage: coin.keyImage,
749 );
750
751 unspentCoinsInfo.add(newInfo);
752 }
753
754 // walletBirthdayBlockHeight checks if the wallet birthday is set and returns
755 // it. Returns -1 if not.
756 Future<int> walletBirthdayBlockHeight() async {
757 try {
758 final res = await _libwallet.birthState(walletInfo.name);
759 final decoded = json.decode(res);
760 // Having these values set indicates that sync has not reached the birthday
761 // yet, so no birthday is set.
762 if (decoded["setfromheight"] == true || decoded["setfromtime"] == true) {
763 return -1;
764 }
765 return decoded["height"] ?? 0;
766 } on FormatException catch (_) {
767 return 0;
768 }
769 }
770
771 Future<bool> verifyMessage(String message, String signature, {String? address = null}) async {
772 var addr = address;
773 if (addr == null) {
774 throw "an address is required to verify message";
775 }
776 return () async {
777 final verified = await _libwallet.verifyMessage(walletInfo.name, message, addr, signature);
778 if (verified == "true") {
779 return true;
780 }
781 return false;
782 }();
783 }
784
785 @override
786 String get password => _password;
787
788 @override
789 bool canSend() => seed != null;
790 }