1
+import 'dart:async';
2
+import 'dart:convert';
3
+import 'dart:io';
4
+
5
+import 'package:cw_core/crypto_currency.dart';
6
+import 'package:cw_core/monero_transaction_priority.dart';
7
+import 'package:cw_core/node.dart';
8
+import 'package:cw_core/pathForWallet.dart';
9
+import 'package:cw_core/pending_transaction.dart';
10
+import 'package:cw_core/sync_status.dart';
11
+import 'package:cw_core/transaction_direction.dart';
12
+import 'package:cw_core/transaction_priority.dart';
13
+import 'package:cw_core/utils/print_verbose.dart';
14
+import 'package:cw_core/wallet_addresses.dart';
15
+import 'package:cw_core/wallet_base.dart';
16
+import 'package:cw_core/wallet_credentials.dart';
17
+import 'package:cw_core/wallet_info.dart';
18
+import 'package:cw_core/wallet_type.dart';
19
+import 'package:bip39/bip39.dart' as bip39;
20
+import 'package:cw_zcash/cw_zcash.dart';
21
+import 'package:cw_zcash/src/util/crc32.dart';
22
+import 'package:cw_zcash/src/zcash_taddress_rotation.dart';
23
+import 'package:cw_zcash/src/zcash_wallet_addresses.dart';
24
+import 'package:mobx/mobx.dart';
25
+import 'package:warp_api/warp_api.dart';
26
+import 'package:warp_api/data_fb_generated.dart';
27
+import 'package:flutter/services.dart';
28
+import 'package:mutex/mutex.dart';
29
+import 'package:path/path.dart' as p;
30
+
31
+part 'zcash_wallet.g.dart';
32
+
33
+class ZcashWallet = ZcashWalletBase with _$ZcashWallet;
34
+
35
+abstract class ZcashWalletBase
36
+ extends WalletBase<ZcashBalance, ZcashTransactionHistory, ZcashTransactionInfo>
37
+ with Store {
38
+ ZcashWalletBase(super.walletInfo, super.derivationInfo, {required this.accountId}) {
39
+ transactionHistory = ZcashTransactionHistory();
40
+ }
41
+
42
+ final int accountId;
43
+ static const int coin = 0; // Zcash mainnet coin ID in warp_api
44
+ @override
45
+ @observable
46
+ SyncStatus syncStatus = NotConnectedSyncStatus();
47
+
48
+ Timer? _syncStatusTimer;
49
+ Timer? _periodicSyncTimer;
50
+ int _initialSyncHeight = 0;
51
+ int _lastKnownBlockHeight = 0;
52
+
53
+ @override
54
+ ObservableMap<CryptoCurrency, ZcashBalance> balance = ObservableMap.of({
55
+ CryptoCurrency.zec: ZcashBalance(confirmed: 0, unconfirmed: 0, frozen: 0),
56
+ });
57
+
58
+ static int internalCalculateEstimatedFee(final TransactionPriority priority, final int? amount) {
59
+ const baseFee = 10000;
60
+ switch (priority) {
61
+ case MoneroTransactionPriority.slow:
62
+ case MoneroTransactionPriority.automatic:
63
+ return baseFee;
64
+ case MoneroTransactionPriority.medium:
65
+ return baseFee * 2;
66
+ case MoneroTransactionPriority.fast:
67
+ return baseFee * 4;
68
+ case MoneroTransactionPriority.fastest:
69
+ return baseFee * 10;
70
+ }
71
+ ;
72
+ return internalCalculateEstimatedFee(MoneroTransactionPriority.automatic, amount);
73
+ }
74
+
75
+ @override
76
+ int calculateEstimatedFee(final TransactionPriority priority, final int? amount) {
77
+ return internalCalculateEstimatedFee(priority, amount);
78
+ }
79
+
80
+ @override
81
+ Future<void> changePassword(final String password) async {
82
+ // throw UnimplementedError();
83
+ }
84
+
85
+ static bool isNodeWorking = true;
86
+
87
+ @override
88
+ Future<bool> checkNodeHealth() {
89
+ return Future.value(isNodeWorking);
90
+ }
91
+
92
+ @override
93
+ Future<void> close({final bool shouldCleanup = false}) async {
94
+ _stopSyncStatusUpdates();
95
+ _stopPeriodicSync();
96
+ await ZcashWalletService.runInDbMutex(() async => WarpApi.cancelSync());
97
+ }
98
+
99
+ @override
100
+ @action
101
+ Future<void> connectToNode({required final Node node}) async {
102
+ printV("connecting to node: ${node.uriRaw}");
103
+ syncStatus = ConnectingSyncStatus();
104
+ try {
105
+ String lwdUrl = node.uriRaw;
106
+ if (!lwdUrl.startsWith('http://') && !lwdUrl.startsWith('https://')) {
107
+ final protocol = node.useSSL == true ? 'https://' : 'http://';
108
+ lwdUrl = '$protocol$lwdUrl';
109
+ }
110
+ printV("Setting LWD URL to: $lwdUrl");
111
+ WarpApi.updateLWD(coin, lwdUrl);
112
+ syncStatus = ConnectedSyncStatus();
113
+ try {
114
+ await updateBalance();
115
+ await updateTransactions();
116
+ } catch (e) {
117
+ printV("Error updating balance/transactions after connect: $e");
118
+ }
119
+ } catch (e) {
120
+ printV("Connection error: $e");
121
+ syncStatus = FailedSyncStatus(error: e.toString());
122
+ rethrow;
123
+ }
124
+ }
125
+
126
+ @override
127
+ Future<PendingTransaction> createTransaction(final Object credentials) async {
128
+ final creds = credentials as ZcashTransactionCredentials;
129
+ await updateBalance();
130
+
131
+ final zcashBalance = balance[CryptoCurrency.zec];
132
+ final availableBalance = zcashBalance?.confirmed ?? 0;
133
+
134
+ final recipients = <Recipient>[];
135
+ int totalAmount = 0;
136
+
137
+ for (final output in creds.outputs) {
138
+ int amount;
139
+ if (output.sendAll) {
140
+ amount = availableBalance;
141
+ } else {
142
+ amount = output.formattedCryptoAmount ?? 0;
143
+
144
+ if (amount == 0 && output.cryptoAmount != null && output.cryptoAmount!.isNotEmpty) {
145
+ try {
146
+ final parsedAmount = CryptoCurrency.zec.parseAmount(
147
+ output.cryptoAmount!.replaceAll(',', '.'),
148
+ );
149
+ amount = parsedAmount.toInt();
150
+ printV("Parsed amount from cryptoAmount '${output.cryptoAmount}': $amount");
151
+ } catch (e) {
152
+ printV("Failed to parse cryptoAmount '${output.cryptoAmount}': $e");
153
+ }
154
+ }
155
+
156
+ if (amount <= 0) {
157
+ throw Exception(
158
+ 'Invalid amount for output. Amount: ${output.cryptoAmount}, Formatted: ${output.formattedCryptoAmount}',
159
+ );
160
+ }
161
+ }
162
+
163
+ totalAmount += amount;
164
+
165
+ var address = (output.isParsedAddress ? output.extractedAddress! : output.address).trim();
166
+
167
+ if (address.isEmpty) {
168
+ throw Exception('Empty address for output');
169
+ }
170
+
171
+ final paymentUri = WarpApi.decodePaymentURI(coin, address);
172
+ String memo = output.note ?? '';
173
+ if (paymentUri != null && paymentUri.address != null) {
174
+ address = paymentUri.address!;
175
+ if (memo.isEmpty && paymentUri.memo != null) {
176
+ memo = paymentUri.memo!;
177
+ }
178
+ }
179
+
180
+ if (!WarpApi.validAddress(coin, address)) {
181
+ throw Exception('Invalid Zcash address: $address');
182
+ }
183
+
184
+ int recipientPools = 7;
185
+
186
+ if (address.startsWith('t1') || address.startsWith('t3')) {
187
+ recipientPools = 1; // Transparent only
188
+ } else if (address.startsWith('zs')) {
189
+ recipientPools = 2; // Sapling only
190
+ }
191
+ // For unified addresses (u1...) and other types, use 7 (all pools)
192
+
193
+ final builder = RecipientObjectBuilder(
194
+ address: address,
195
+ pools: recipientPools,
196
+ amount: amount,
197
+ feeIncluded: output.sendAll,
198
+ replyTo: false,
199
+ memo: memo.isNotEmpty ? memo : null,
200
+ );
201
+
202
+ recipients.add(Recipient(builder.toBytes()));
203
+ }
204
+
205
+ if (totalAmount > availableBalance) {
206
+ throw Exception('Insufficient balance');
207
+ }
208
+
209
+ final fee = FeeT(
210
+ fee: internalCalculateEstimatedFee(creds.priority, null),
211
+ minFee: 0,
212
+ maxFee: 0,
213
+ scheme: 0, // Fixed fee scheme
214
+ );
215
+
216
+ // pools parameter: bitmask for which pools to use for sending
217
+ // 1=Transparent, 2=Sapling, 4=Orchard, 7=All pools
218
+ // Using 7 (all pools) allows spending from any pool type
219
+ final txPlan = await ZcashWalletService.runInDbMutex(
220
+ () => WarpApi.prepareTx(
221
+ coin,
222
+ accountId,
223
+ recipients,
224
+ 7, // pools: All pools (Transparent + Sapling + Orchard) - allows spending from any pool
225
+ 1, // senderUAType: 0 = unified address
226
+ 0, // anchorOffset
227
+ fee,
228
+ ),
229
+ );
230
+
231
+ return PendingZcashTransaction(
232
+ zcashWallet: this as ZcashWallet,
233
+ credentials: creds,
234
+ txPlan: txPlan,
235
+ fee: internalCalculateEstimatedFee(creds.priority, null),
236
+ availableBalance: availableBalance,
237
+ );
238
+ }
239
+
240
+ static const _dispPhrase = "Received to disposable address";
241
+ Future<List<ShieldedTx>> getShieldTxForUi() async {
242
+ final tx = (ZcashTaddressRotation.shieldedAccountsTx[accountId] ?? <ShieldedTx>[])
243
+ .map((final v) {
244
+ final unpacked = v.unpack();
245
+ unpacked.memo ??= "";
246
+ unpacked.memo = "${unpacked.memo}\n$_dispPhrase".trim();
247
+ final List<int> buff = base64.decode(
248
+ ZcashTaddressRotation.flatBuffersPack(unpacked.pack),
249
+ );
250
+ return ShieldedTx(buff);
251
+ })
252
+ .where((final t) => t.value > 0);
253
+
254
+ return tx.toList();
255
+ }
256
+
257
+ static Map<int, List<ShieldedTx>> temporarySentTx = {};
258
+
259
+ static String txChecksumKey(final ShieldedTx tx) {
260
+ final direction = tx.value > 0 ? TransactionDirection.incoming : TransactionDirection.outgoing;
261
+ return 'tx${direction}_${tx.id}_${tx.timestamp}_${CRC32.compute(tx.toString())}';
262
+ }
263
+
264
+ @override
265
+ Future<Map<String, ZcashTransactionInfo>> fetchTransactions() async {
266
+ await ZcashWalletService.loadShieldTxs();
267
+ final txs = (await ZcashWalletService.runInDbMutex(
268
+ () => WarpApi.getTxs(coin, accountId),
269
+ )).toList();
270
+ // ShieldedTx{id: 26, txId: 4d1be06ce2c2debec8d98ce4e9434c8aac27c980488b459017d423fdcab37f93, height: 3195705, shortTxId: 4d1be06c, timestamp: 1767730944, name: null, value: 1000000, address: null, memo: , messages: MemoVec{memos: null}}
271
+
272
+ final shieldTx = await getShieldTxForUi();
273
+
274
+ txs.addAll(shieldTx);
275
+ final txIds = txs.map((final tx) => tx.txId!.replaceAll('"', '')).toSet();
276
+ temporarySentTx[accountId]?.removeWhere(
277
+ (final ttx) => txIds.contains(ttx.txId!.replaceAll('"', '')),
278
+ );
279
+ txs.addAll(temporarySentTx[accountId] ?? []);
280
+
281
+ txs.sort((final a, final b) => a.height.compareTo(b.height));
282
+ final Map<String, ZcashTransactionInfo> result = {};
283
+ int currentHeight = 0;
284
+ try {
285
+ currentHeight = await WarpApi.getLatestHeight(coin);
286
+ } catch (e) {
287
+ printV("Error getting latest height: $e");
288
+ }
289
+
290
+ for (final tx in txs) {
291
+ final direction = tx.value > 0
292
+ ? TransactionDirection.incoming
293
+ : TransactionDirection.outgoing;
294
+
295
+ final confirmations = tx.height > 0 && currentHeight > 0 ? currentHeight - tx.height + 1 : 0;
296
+
297
+ final txChecksum = txChecksumKey(tx);
298
+ final txId = tx.txId ?? tx.shortTxId ?? txChecksum;
299
+
300
+ final txInfo = ZcashTransactionInfo(
301
+ id: txId.trim().replaceAll('"', ''),
302
+ amount: tx.value.abs(),
303
+ fee: 0,
304
+ direction: direction,
305
+ isPending: tx.height == 0,
306
+ date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000),
307
+ height: tx.height,
308
+ confirmations: confirmations,
309
+ to: tx.address ?? '',
310
+ memo: tx.memo,
311
+ );
312
+ // if (txInfo.additionalInfo['autoShield'] == true) {
313
+ // continue;
314
+ // }
315
+ result[txChecksum] = txInfo;
316
+ }
317
+
318
+ return result;
319
+ }
320
+
321
+ @override
322
+ Object get keys => {};
323
+
324
+ @override
325
+ String get password => _password!;
326
+
327
+ @override
328
+ Future<void> renameWalletFiles(final String newWalletName) async {
329
+ final currentWalletPath = await pathForWallet(name: name, type: type);
330
+ final currentCacheFile = File(currentWalletPath);
331
+ final newWalletPath = await pathForWallet(name: newWalletName, type: type);
332
+ if (currentCacheFile.existsSync()) {
333
+ await currentCacheFile.copy(newWalletPath);
334
+ }
335
+ Directory(currentWalletPath).deleteSync(recursive: true);
336
+ }
337
+
338
+ @override
339
+ bool get hasRescan => true;
340
+
341
+ @override
342
+ @action
343
+ Future<void> rescan({required final int height}) async {
344
+ try {
345
+ syncStatus = StartingScanSyncStatus(height);
346
+ printV("rescanning from: $height");
347
+ await ZcashWalletService.runInDbMutex(() async => WarpApi.rescanFrom(coin, height));
348
+ await startSync();
349
+ } catch (e) {
350
+ printV("Rescan error: $e");
351
+ syncStatus = FailedSyncStatus(error: e.toString());
352
+ rethrow;
353
+ }
354
+ }
355
+
356
+ bool _isTransactionUpdating = false;
357
+
358
+ Future<void> updateTransactions() async {
359
+ try {
360
+ if (_isTransactionUpdating) {
361
+ return;
362
+ }
363
+
364
+ _isTransactionUpdating = true;
365
+ final transactions = await fetchTransactions();
366
+
367
+ final currentIds = transactionHistory.transactions.keys.toSet();
368
+ final newIds = transactions.keys.toSet();
369
+
370
+ currentIds
371
+ .difference(newIds)
372
+ .forEach((final id) => transactionHistory.transactions.remove(id));
373
+
374
+ transactions.forEach((final key, final tx) {
375
+ transactionHistory.transactions[key] = tx;
376
+ });
377
+ await transactionHistory.save();
378
+ _isTransactionUpdating = false;
379
+ } catch (e, stackTrace) {
380
+ printV("Update transactions error: $e");
381
+ printV("Stack trace: $stackTrace");
382
+ _isTransactionUpdating = false;
383
+ }
384
+ }
385
+
386
+ Future<void> updateTransactionsHistory() => updateTransactions();
387
+
388
+ @override
389
+ Future<void> save() async {}
390
+
391
+ Future<void> init() async {
392
+ try {
393
+ await walletAddresses.init();
394
+
395
+ await updateBalance();
396
+ await updateTransactions();
397
+ } catch (e) {
398
+ printV("Wallet init error: $e");
399
+ }
400
+ }
401
+
402
+ @override
403
+ String? get seed {
404
+ try {
405
+ final backup = WarpApi.getBackup(coin, accountId);
406
+ final seed = backup.seed!.split(" ");
407
+ if ([13, 25].contains(seed.length)) {
408
+ seed.removeLast();
409
+ }
410
+ return seed.join(" ").trim();
411
+ } catch (e) {
412
+ return null;
413
+ }
414
+ }
415
+
416
+ @override
417
+ String? get passphrase {
418
+ try {
419
+ final backup = WarpApi.getBackup(coin, accountId);
420
+ final seed = backup.seed!.split(" ");
421
+ if ([13, 25].contains(seed.length)) {
422
+ final passphrase = seed.removeLast();
423
+ return passphrase;
424
+ }
425
+ return null;
426
+ } catch (e) {
427
+ return null;
428
+ }
429
+ }
430
+
431
+ @override
432
+ Future<String> signMessage(final String message, {final String? address = null}) {
433
+ throw UnimplementedError();
434
+ }
435
+
436
+ @override
437
+ @action
438
+ Future<void> startSync() async {
439
+ try {
440
+ syncStatus = AttemptingSyncStatus();
441
+
442
+ _initialSyncHeight = 0;
443
+ _lastKnownBlockHeight = 0;
444
+
445
+ _startSyncStatusUpdates();
446
+
447
+ syncStatus = SyncronizingSyncStatus();
448
+
449
+ unawaited(
450
+ _runWarpSync().catchError((final e) {
451
+ isNodeWorking = false;
452
+ printV("WarpSync error in startSync: $e");
453
+ syncStatus = FailedSyncStatus(error: e.toString());
454
+ _stopSyncStatusUpdates();
455
+ }),
456
+ );
457
+ } catch (e) {
458
+ isNodeWorking = false;
459
+ printV("Sync error: $e");
460
+ syncStatus = FailedSyncStatus(error: e.toString());
461
+ _stopSyncStatusUpdates();
462
+ rethrow;
463
+ }
464
+ }
465
+
466
+ static Mutex warpSyncMutex = Mutex();
467
+
468
+ static Future<void> initialSyncCheck() async {
469
+ final zcashDir = await pathForWalletTypeDir(type: WalletType.zcash);
470
+ final zcashInitialSync = File(p.join(zcashDir, ".initial-sync-marker"));
471
+ if (!zcashInitialSync.existsSync()) {
472
+ int chainHeight = 3000000; // fallback if node is offline
473
+ try {
474
+ chainHeight = await WarpApi.getLatestHeight(coin);
475
+ } catch (e) {
476
+ printV("Error getting latest height: $e");
477
+ }
478
+ await ZcashWalletService.runInDbMutex(
479
+ () async => await WarpApi.rescanFrom(coin, chainHeight - 150000),
480
+ );
481
+ zcashInitialSync.writeAsBytesSync([0x00]);
482
+ zcashInitialSync.writeAsStringSync(chainHeight.toString(), mode: FileMode.writeOnlyAppend);
483
+ }
484
+ }
485
+
486
+ @action
487
+ Future<void> _runWarpSync() async {
488
+ Timer? _t;
489
+ try {
490
+ await warpSyncMutex.acquire();
491
+ await initialSyncCheck();
492
+ isNodeWorking = true;
493
+ printV("Starting warpSync for coin $coin, account $accountId");
494
+ int? initialQueue = null;
495
+ void _cancelSyncIfShould(final Timer t) {
496
+ initialQueue ??= ZcashWalletService.dbMutexQueue + 2;
497
+ if (ZcashWalletService.dbMutexQueue <= initialQueue!) {
498
+ initialQueue = ZcashWalletService.dbMutexQueue;
499
+ return;
500
+ }
501
+ printV(
502
+ "Canceling sync! (ZcashWalletService.dbMutexQueue: ${ZcashWalletService.dbMutexQueue} > initialQueue: ${initialQueue})",
503
+ );
504
+ WarpApi.cancelSync();
505
+ t.cancel();
506
+ _t = null;
507
+ }
508
+
509
+ unawaited(
510
+ Future.delayed(Duration(seconds: 2)).then((_) {
511
+ _t = Timer.periodic(Duration(milliseconds: 100), _cancelSyncIfShould);
512
+ }),
513
+ );
514
+ final result = await ZcashWalletService.runInDbMutex(
515
+ () => WarpApi.warpSync(coin, accountId, true, 0, 1000000, 0),
516
+ );
517
+ printV("warpSync completed with result: $result");
518
+
519
+ await _updateSyncStatus();
520
+ } catch (e) {
521
+ syncStatus = FailedSyncStatus(error: e.toString());
522
+ unawaited(Future.delayed(Duration(seconds: 1)).then((_) => unawaited(_runWarpSync())));
523
+ _stopSyncStatusUpdates();
524
+ } finally {
525
+ isNodeWorking = false;
526
+ warpSyncMutex.release();
527
+ _t?.cancel();
528
+ }
529
+ }
530
+
531
+ void _startSyncStatusUpdates() {
532
+ _stopSyncStatusUpdates();
533
+ _updateSyncStatus();
534
+ _syncStatusTimer = Timer.periodic(const Duration(milliseconds: 5000), (_) {
535
+ _updateSyncStatus().catchError((final e) {
536
+ printV("Error in sync status update timer: $e");
537
+ });
538
+ });
539
+ }
540
+
541
+ void _stopSyncStatusUpdates() {
542
+ _syncStatusTimer?.cancel();
543
+ _syncStatusTimer = null;
544
+ }
545
+
546
+ void _startPeriodicSync() {
547
+ printV("_startPeriodicSync");
548
+ _stopPeriodicSync();
549
+ _periodicSyncTimer = Timer.periodic(const Duration(seconds: 5), (_) async {
550
+ try {
551
+ final chainHeight = await WarpApi.getLatestHeight(coin);
552
+ final dbHeight = WarpApi.getDbHeight(coin);
553
+ final height = dbHeight.unpack();
554
+ final syncHeight = height.height;
555
+
556
+ if (syncHeight < chainHeight) {
557
+ printV("Periodic sync: chainHeight=$chainHeight, syncHeight=$syncHeight, starting sync");
558
+ await _runWarpSync();
559
+ } else {
560
+ await updateBalance();
561
+ await updateTransactions();
562
+ }
563
+ } catch (e) {
564
+ printV("Periodic sync error: $e");
565
+ }
566
+ });
567
+ }
568
+
569
+ void _stopPeriodicSync() {
570
+ _periodicSyncTimer?.cancel();
571
+ _periodicSyncTimer = null;
572
+ }
573
+
574
+ @action
575
+ Future<void> _updateSyncStatus() async {
576
+ try {
577
+ final dbHeight = WarpApi.getDbHeight(coin);
578
+ final height = dbHeight.unpack();
579
+ final syncHeight = height.height;
580
+
581
+ final chainHeight = await WarpApi.getLatestHeight(coin);
582
+
583
+ if (_initialSyncHeight <= 0 && syncHeight > 0) {
584
+ _initialSyncHeight = syncHeight;
585
+ printV("Initialized sync height to: $_initialSyncHeight");
586
+ if (syncHeight - 10 > dbHeight.height) {}
587
+ }
588
+
589
+ if (chainHeight <= 0) {
590
+ if (syncStatus is! ConnectedSyncStatus && syncStatus is! ConnectingSyncStatus) {
591
+ syncStatus = ConnectedSyncStatus();
592
+ }
593
+ try {
594
+ await updateBalance();
595
+ await updateTransactions();
596
+ } catch (e) {
597
+ printV("Error updating balance/transactions: $e");
598
+ }
599
+ return;
600
+ }
601
+
602
+ if (syncHeight <= 0) {
603
+ if (syncStatus is! ConnectedSyncStatus &&
604
+ syncStatus is! ConnectingSyncStatus &&
605
+ syncStatus is! AttemptingSyncStatus) {
606
+ syncStatus = ConnectedSyncStatus();
607
+ }
608
+ try {
609
+ await updateBalance();
610
+ await updateTransactions();
611
+ } catch (e) {
612
+ printV("Error updating balance/transactions: $e");
613
+ }
614
+ return;
615
+ }
616
+
617
+ if (syncHeight >= chainHeight && syncHeight > 0) {
618
+ syncStatus = SyncedSyncStatus();
619
+ _stopSyncStatusUpdates();
620
+ await updateBalance();
621
+ await updateTransactions();
622
+ _startPeriodicSync();
623
+ return;
624
+ }
625
+
626
+ if (_lastKnownBlockHeight != syncHeight) {
627
+ _lastKnownBlockHeight = syncHeight;
628
+ }
629
+
630
+ if (syncHeight < 0 || chainHeight < syncHeight) {
631
+ return;
632
+ }
633
+
634
+ final blocksLeft = chainHeight - syncHeight;
635
+ if (blocksLeft <= 0) {
636
+ syncStatus = SyncedSyncStatus();
637
+ _stopSyncStatusUpdates();
638
+ await updateBalance();
639
+ await updateTransactions();
640
+ _startPeriodicSync();
641
+ return;
642
+ }
643
+
644
+ double ptc = 0.0;
645
+ if (_initialSyncHeight > 0) {
646
+ final track = chainHeight - _initialSyncHeight;
647
+ final diff = track > 0 ? track - (chainHeight - syncHeight) : 0;
648
+ ptc = track > 0 && diff >= 0 ? diff / track : 0.0;
649
+ } else {
650
+ ptc = syncHeight / chainHeight;
651
+ }
652
+
653
+ syncStatus = SyncingSyncStatus(blocksLeft, ptc.clamp(0.0, 1.0));
654
+
655
+ await updateBalance();
656
+ await updateTransactions();
657
+ } catch (e) {
658
+ printV("Sync status update error: $e");
659
+ }
660
+ }
661
+
662
+ String getDiversifiedAddress(final int uaType, {final DateTime? time}) {
663
+ try {
664
+ final timestamp = (time ?? DateTime.now()).millisecondsSinceEpoch ~/ 1000;
665
+ return WarpApi.getDiversifiedAddress(coin, accountId, uaType, timestamp);
666
+ } catch (e) {
667
+ printV("Error getting diversified address: $e");
668
+ return "";
669
+ }
670
+ }
671
+
672
+ static final autoShieldMutex = Mutex();
673
+ Future<void> _autoShield() async {
674
+ try {
675
+ await autoShieldMutex.acquire();
676
+ await _$autoShield();
677
+ } catch (e) {
678
+ printV("shielding failed: $e");
679
+ await Future.delayed(Duration(seconds: 30));
680
+ } finally {
681
+ autoShieldMutex.release();
682
+ }
683
+ }
684
+
685
+ Future<void> _$autoShield() async {
686
+ final chainHeight = await WarpApi.getLatestHeight(coin);
687
+ final dbHeight = WarpApi.getDbHeight(coin);
688
+ final height = dbHeight.unpack();
689
+ final syncHeight = height.height;
690
+ if (chainHeight != syncHeight) {
691
+ printV("Not autoshielding: chainHeight(${chainHeight}) != syncHeight(${syncHeight})");
692
+ return;
693
+ }
694
+ final bpConfirmed = WarpApi.getPoolBalances(coin, accountId, 0, false);
695
+ if (bpConfirmed.transparent + bpConfirmed.sapling <= 20000) {
696
+ return;
697
+ }
698
+
699
+ final recipientBuilder = RecipientObjectBuilder(
700
+ address: (walletAddresses as ZcashWalletAddresses).orchardAddress,
701
+ pools: 4,
702
+ feeIncluded: true,
703
+ amount: bpConfirmed.transparent + bpConfirmed.sapling,
704
+ );
705
+
706
+ final recipient = Recipient(recipientBuilder.toBytes());
707
+ final fee = FeeT(fee: 10000, minFee: 0, maxFee: 0, scheme: 0);
708
+ final txPlan = await ZcashWalletService.runInDbMutex(
709
+ () => WarpApi.prepareTx(
710
+ coin,
711
+ accountId,
712
+ [recipient],
713
+ 3, // pools: (Transparent + Sapling)
714
+ 1,
715
+ 0, // anchorOffset
716
+ fee,
717
+ ),
718
+ );
719
+ final _txId = await ZcashWalletService.runInDbMutex(
720
+ () => WarpApi.signAndBroadcast(ZcashWalletBase.coin, accountId, txPlan),
721
+ );
722
+ await ZcashWalletService.addShieldedTx(_txId);
723
+ printV("shielded: $_txId");
724
+ await updateTransactions();
725
+ await updateBalance();
726
+ await Future.delayed(Duration(seconds: 75 * 5)); // do not re-try doing that
727
+ }
728
+
729
+ @override
730
+ @action
731
+ Future<void> updateBalance() async {
732
+ try {
733
+ final poolBalances = WarpApi.getPoolBalances(coin, accountId, 0, true);
734
+ final balances = poolBalances.unpack();
735
+ // final notes = WarpApi.getNotesSync(coin, accountId);
736
+ // int frozenBalance = 0;
737
+ // for (final note in notes) {
738
+ // if (note.excluded) {
739
+ // frozenBalance += note.value;
740
+ // }
741
+ // }
742
+ final total = balances.orchard + balances.sapling + balances.transparent;
743
+ final spendable = total - balances.transparent;
744
+
745
+ final confirmedPoolBalances = WarpApi.getPoolBalances(coin, accountId, 3, true);
746
+ final confirmedBalances = confirmedPoolBalances.unpack();
747
+ final confirmedTotal =
748
+ confirmedBalances.orchard + confirmedBalances.sapling + confirmedBalances.transparent;
749
+
750
+ int knownOutPending = 0;
751
+ ZcashWalletBase.temporarySentTx[accountId]?.forEach((final sTx) {
752
+ knownOutPending += sTx.value; // it's negative
753
+ });
754
+ final confirmedSpendable = confirmedTotal - balances.transparent + knownOutPending;
755
+
756
+ unawaited(_autoShield());
757
+
758
+ balance[CryptoCurrency.zec] = ZcashBalance(
759
+ confirmed: confirmedSpendable,
760
+ unconfirmed: spendable - confirmedSpendable,
761
+ frozen: 0,
762
+ );
763
+ } catch (e, stackTrace) {
764
+ printV("Balance update error: $e");
765
+ printV("Stack trace: $stackTrace");
766
+ }
767
+ }
768
+
769
+ @override
770
+ Future<bool> verifyMessage(
771
+ final String message,
772
+ final String signature, {
773
+ final String? address = null,
774
+ }) {
775
+ throw UnimplementedError();
776
+ }
777
+
778
+ @observable
779
+ late WalletAddresses walletAddresses = ZcashWalletAddresses(accountId, walletInfo);
780
+
781
+ static Future<ZcashWallet> create(final WalletCredentials credentials) async {
782
+ await _init();
783
+ final newWalletCredentials = credentials as ZcashNewWalletCredentials;
784
+
785
+ String mnemonic;
786
+ if (newWalletCredentials.mnemonic != null && newWalletCredentials.mnemonic!.isNotEmpty) {
787
+ mnemonic = newWalletCredentials.mnemonic!;
788
+ } else {
789
+ final strength = (newWalletCredentials.seedPhraseLength == 24) ? 256 : 128;
790
+ mnemonic = bip39.generateMnemonic(strength: strength);
791
+ }
792
+
793
+ final accountId = await _restoreZcashWalletFromSeed(
794
+ name: credentials.name,
795
+ seed: mnemonic,
796
+ passphrase: newWalletCredentials.passphrase,
797
+ );
798
+ await _saveAccountId(credentials.name, accountId);
799
+ final wallet = await open(
800
+ name: credentials.name,
801
+ password: credentials.password!,
802
+ walletInfo: credentials.walletInfo!,
803
+ );
804
+ await wallet.walletAddresses.saveAddressesInBox();
805
+ return wallet;
806
+ }
807
+
808
+ static Future<ZcashWallet> restore(final WalletCredentials credentials) async {
809
+ await _init();
810
+ final fromSeedCredentials = credentials as ZcashFromSeedWalletCredentials;
811
+ final String? seed = fromSeedCredentials.seed;
812
+ if (seed == null || seed.isEmpty) {
813
+ throw Exception('Seed phrase is required for wallet restoration');
814
+ }
815
+
816
+ final accountId = await _restoreZcashWalletFromSeed(
817
+ name: credentials.name,
818
+ seed: seed,
819
+ passphrase: fromSeedCredentials.passphrase,
820
+ );
821
+ await _saveAccountId(credentials.name, accountId);
822
+ final wallet = await open(
823
+ name: credentials.name,
824
+ password: credentials.password!,
825
+ walletInfo: credentials.walletInfo!,
826
+ );
827
+ await wallet.walletAddresses.saveAddressesInBox();
828
+ printV("height: ${credentials.height}");
829
+ if (credentials.height != null) {
830
+ final zcashDir = await pathForWalletTypeDir(type: WalletType.zcash);
831
+ final zcashInitialSync = File(p.join(zcashDir, ".initial-sync-marker"));
832
+ zcashInitialSync.writeAsBytesSync([0x00]);
833
+ zcashInitialSync.writeAsStringSync(
834
+ credentials.height.toString(),
835
+ mode: FileMode.writeOnlyAppend,
836
+ );
837
+ unawaited(
838
+ Future.delayed(Duration(seconds: 8)).then(
839
+ (_) => ZcashWalletService.runInDbMutex(
840
+ () async => await WarpApi.rescanFrom(coin, credentials.height ?? 0),
841
+ ),
842
+ ),
843
+ );
844
+ }
845
+ return wallet;
846
+ }
847
+
848
+ static Future<ZcashWallet> open({
849
+ required final String name,
850
+ required final String password,
851
+ required final WalletInfo walletInfo,
852
+ }) async {
853
+ await _init();
854
+ if (password.isNotEmpty) {
855
+ WarpApi.setDbPasswd(coin, password);
856
+ }
857
+ final accountId = await getZcashAccountIdForName(name);
858
+ if (accountId == null) {
859
+ throw Exception("Wallet account not found for name: $name");
860
+ }
861
+ final wallet = ZcashWallet(
862
+ walletInfo,
863
+ await walletInfo.getDerivationInfo(),
864
+ accountId: accountId,
865
+ );
866
+ await wallet.walletAddresses.init();
867
+ return wallet;
868
+ }
869
+
870
+ static Future<int> _restoreZcashWalletFromSeed({
871
+ required final String name,
872
+ required String seed,
873
+ required String? passphrase,
874
+ }) async {
875
+ if (passphrase?.isNotEmpty == true) {
876
+ passphrase = passphrase!.replaceAll(" ", "_");
877
+ seed = "${seed} ${passphrase}";
878
+ }
879
+ final accountId = await ZcashWalletService.runInDbMutex(
880
+ () => WarpApi.newAccount(coin, name, seed, 0),
881
+ );
882
+ return accountId;
883
+ }
884
+
885
+ static Future<int?> getZcashAccountIdForName(final String name) async {
886
+ final wPath = await pathForWallet(name: name, type: _type);
887
+ final f = File(wPath);
888
+ if (!f.existsSync()) {
889
+ final accounts = WarpApi.getAccountList(coin);
890
+ for (final account in accounts) {
891
+ if (account.name == name) {
892
+ return account.id;
893
+ }
894
+ }
895
+ return null;
896
+ }
897
+ final content = f.readAsStringSync();
898
+ return int.tryParse(content.trim());
899
+ }
900
+
901
+ static Future<void> _saveAccountId(final String name, final int accountId) async {
902
+ final wPath = await pathForWallet(name: name, type: _type);
903
+ final f = File(wPath);
904
+ f.writeAsStringSync(accountId.toString());
905
+ }
906
+
907
+ static WalletType get _type => WalletType.zcash;
908
+
909
+ static Future<String> getDbDataPath() async {
910
+ final pathForWalletType = await pathForWalletTypeDir(type: _type);
911
+ final dbDataPath = "${pathForWalletType}/zec.db";
912
+ if (!Directory(pathForWalletType).existsSync()) {
913
+ Directory(pathForWalletType).createSync(recursive: true);
914
+ }
915
+ return dbDataPath;
916
+ }
917
+
918
+ static Future<String> getTorDir() async {
919
+ final pathForWalletType = await pathForWalletTypeDir(type: _type);
920
+ final torPath = "${pathForWalletType}/tor";
921
+ if (!Directory(torPath).existsSync()) {
922
+ Directory(torPath).createSync(recursive: true);
923
+ }
924
+ return torPath;
925
+ }
926
+
927
+ static Future<String> getFsBlockCacheDir() async {
928
+ final pathForWalletType = await pathForWalletTypeDir(type: _type);
929
+ final fsBlockCacheDir = "${pathForWalletType}/blockCache";
930
+ if (!Directory(pathForWalletType).existsSync()) {
931
+ Directory(pathForWalletType).createSync(recursive: true);
932
+ }
933
+ return fsBlockCacheDir;
934
+ }
935
+
936
+ static String? dbDataPath;
937
+ static bool _initialized = false;
938
+
939
+ static void unlockDatabase(final String password) {
940
+ _password = password;
941
+ }
942
+
943
+ static String? _password;
944
+ static Future<void> _init() async {
945
+ if (_initialized) return;
946
+ dbDataPath = await getDbDataPath();
947
+ printV("WarpApi.initWallet");
948
+ if (_password == null) {
949
+ throw Exception("Zcash wallet locked! Please contact support");
950
+ }
951
+ if (!File(dbDataPath!).existsSync()) {
952
+ //TODO(mrcyjanek): copy-encrypt
953
+ }
954
+ // coin+1 = ycash
955
+ WarpApi.setDbPasswd(coin, '');
956
+ WarpApi.setDbPasswd(coin + 1, '');
957
+ WarpApi.initWallet(coin, dbDataPath!);
958
+ WarpApi.initWallet(coin + 1, dbDataPath!);
959
+ try {
960
+ WarpApi.migrateData(coin);
961
+ WarpApi.migrateData(coin + 1);
962
+ } catch (e) {
963
+ printV("zec init failed: $e");
964
+ } // do not fail on network exception
965
+ final spend = await rootBundle.load('scripts/zcash_lib/assets/sapling-spend.params');
966
+ final output = await rootBundle.load('scripts/zcash_lib/assets/sapling-output.params');
967
+ WarpApi.initProver(spend.buffer.asUint8List(), output.buffer.asUint8List());
968
+ await ZcashTaddressRotation.init();
969
+ await ZcashTransactionInfo.init();
970
+ _initialized = true;
971
+ }
972
+
973
+ static Future<int> getHeightByDate(final DateTime date) {
974
+ return WarpApi.getBlockHeightByTime(coin, date);
975
+ }
976
+}