1
import 'dart:async';
2
-import 'dart:convert';
2
import 'dart:io';
3
+import 'dart:math';
4
5
import 'package:cw_core/amount/money.dart';
6
import 'package:cw_core/crypto_currency.dart';
7
+import 'package:cw_core/get_height_by_date_zec.dart';
8
import 'package:cw_core/monero_transaction_priority.dart';
9
import 'package:cw_core/node.dart';
10
import 'package:cw_core/pathForWallet.dart';
13
import 'package:cw_core/transaction_direction.dart';
14
import 'package:cw_core/transaction_priority.dart';
15
import 'package:cw_core/utils/print_verbose.dart';
15
-import 'package:cw_core/utils/proxy_wrapper.dart';
16
-import 'package:cw_core/wallet_addresses.dart';
16
import 'package:cw_core/wallet_base.dart';
17
import 'package:cw_core/wallet_credentials.dart';
18
import 'package:cw_core/wallet_info.dart';
19
import 'package:cw_core/wallet_type.dart';
20
import 'package:bip39/bip39.dart' as bip39;
21
import 'package:cw_zcash/cw_zcash.dart';
23
-import 'package:cw_zcash/src/legacy/zkool_sweep.dart';
22
import 'package:cw_zcash/src/util/crc32.dart';
23
+import 'package:cw_zcash/src/zcash_mempool.dart';
24
import 'package:cw_zcash/src/zcash_taddress_rotation.dart';
25
import 'package:cw_zcash/src/zcash_wallet_addresses.dart';
27
-import 'package:flutter/foundation.dart';
26
+import 'package:cw_zcash/src/zkool_compat.dart';
27
+import 'package:cw_zcash/src/zkooltx.dart';
28
import 'package:mobx/mobx.dart';
29
-import 'package:warp_api/warp_api.dart';
30
-import 'package:warp_api/data_fb_generated.dart';
31
-import 'package:flutter/services.dart';
29
import 'package:mutex/mutex.dart';
33
-import 'package:path/path.dart' as p;
34
-import 'package:path_provider/path_provider.dart';
30
+import 'package:zkool/src/rust/api/account.dart' as zkool_account;
31
+import 'package:zkool/src/rust/api/coin.dart' as zkool_coin;
32
+import 'package:zkool/src/rust/api/mempool.dart' as zkool_mempool;
33
+import 'package:zkool/src/rust/api/sync.dart' as zkool_sync;
34
+import 'package:zkool/src/rust/api/pay.dart' as zkool_pay;
35
+import 'package:zkool/src/rust/api/network.dart' as zkool_network;
36
+import 'package:zkool/src/rust/pay.dart' as zkool_paydart;
37
+import 'package:zkool/src/rust/frb_generated.dart' as zkool_frb;
38
39
part 'zcash_wallet.g.dart';
40
45
with Store {
46
ZcashWalletBase(super.walletInfo, super.derivationInfo, {required this.accountId}) {
47
transactionHistory = ZcashTransactionHistory();
48
+ walletsByAccountId[accountId] = this;
49
}
50
47
- final int accountId;
48
- static const int coin = 0; // Zcash mainnet coin ID in warp_api
51
+ static final Map<int, ZcashWalletBase> walletsByAccountId = {};
52
+
53
+ static Future<void> refreshWalletForAccount(final int accountId) async {
54
+ final wallet = walletsByAccountId[accountId];
55
+ if (wallet == null) {
56
+ return;
57
+ }
58
+ await wallet.updateTransactions();
59
+ await wallet.updateBalance();
60
+ }
61
+
62
+ int accountId;
63
+
64
@override
65
@observable
66
SyncStatus syncStatus = NotConnectedSyncStatus();
67
53
- Timer? _syncStatusTimer;
54
- Timer? _periodicSyncTimer;
55
- int _initialSyncHeight = 0;
56
- int _lastKnownBlockHeight = 0;
57
-
68
@override
69
ObservableMap<CryptoCurrency, ZcashBalance> balance = ObservableMap.of({
70
CryptoCurrency.zec: ZcashBalance.zero(),
71
});
72
73
+ static const int _autoShieldMinSweep = 30000;
74
+
75
+ Money _feeFromTxPlan(
76
+ final zkool_pay.PcztPackage txPlan,
77
+ final TransactionPriority priority,
78
+ final int tryReduceFeeAmount, {
79
+ final zkool_coin.Coin? coin,
80
+ }) {
81
+ try {
82
+ return Money(zkool_pay.toPlan(package: txPlan, c: coin ?? c).fee, currency);
83
+ } catch (_) {
84
+ return Money.fromInt(
85
+ tryReduceFeeAmount != 0
86
+ ? tryReduceFeeAmount
87
+ : internalCalculateEstimatedFee(priority, null),
88
+ currency,
89
+ );
90
+ }
91
+ }
92
+
93
static int internalCalculateEstimatedFee(final TransactionPriority priority, final int? amount) {
94
const baseFee = 10000;
95
switch (priority) {
126
127
@override
128
Future<void> close({final bool shouldCleanup = false}) async {
99
- _stopSyncStatusUpdates();
100
- _stopPeriodicSync();
101
- await ZcashWalletService.runInDbMutex(() async => WarpApi.cancelSync());
129
+ _syncLoopRunning = false;
130
+ walletsByAccountId.remove(accountId);
131
}
132
133
Node? lastNode;
144
lwdUrl = '$protocol$lwdUrl';
145
}
146
printV("Setting LWD URL to: $lwdUrl");
118
- WarpApi.updateLWD(coin, lwdUrl);
147
+ c = c.setLwd(url: lwdUrl, serverType: 0);
148
syncStatus = ConnectedSyncStatus();
120
- try {
121
- await updateBalance();
122
- await updateTransactions();
123
- } catch (e) {
124
- printV("Error updating balance/transactions after connect: $e");
125
- }
149
+ unawaited(ZcashMempoolService.instance.ensureRunning(c));
150
+ _ensureSyncLoopRunning();
151
+ unawaited(_refreshSyncStatus());
152
+ unawaited(_oneshotSync());
153
} catch (e) {
154
printV("Connection error: $e");
155
syncStatus = FailedSyncStatus(error: e.toString());
157
}
158
}
159
133
- @override
134
- Future<PendingTransaction> createTransaction(final Object credentials) async {
135
- final creds = credentials as ZcashTransactionCredentials;
136
- await updateBalance();
137
-
138
- final zcashBalance = balance[CryptoCurrency.zec];
139
- final availableBalance = zcashBalance?.available ?? Money.zero(currency);
160
+ bool _syncLoopRunning = false;
161
141
- final recipients = <Recipient>[];
142
- var totalAmount = Money.zero(currency);
162
+ void _ensureSyncLoopRunning() {
163
+ if (_syncLoopRunning) {
164
+ return;
165
+ }
166
+ _syncLoopRunning = true;
167
+ unawaited(_runSyncLoop());
168
+ }
169
144
- for (final output in creds.outputs) {
145
- Money amount;
146
- if (output.sendAll) {
147
- amount = availableBalance;
148
- } else {
149
- amount = output.cryptoAmount;
170
+ static const _syncedPollInterval = Duration(seconds: 5);
171
+ static const _activePollInterval = Duration(seconds: 1);
172
151
- if (amount <= Money.zero(currency)) {
152
- throw Exception(
153
- 'Invalid amount for output. Amount: ${output.cryptoAmount}, Formatted: ${output.cryptoAmount}',
154
- );
155
- }
173
+ Future<void> _runSyncLoop() async {
174
+ var pollInterval = _activePollInterval;
175
+ while (_syncLoopRunning) {
176
+ await Future.delayed(pollInterval);
177
+ try {
178
+ final alreadySynced = await _oneshotSync();
179
+ pollInterval = alreadySynced ? _syncedPollInterval : _activePollInterval;
180
+ } catch (e) {
181
+ printV("zcash sync failed: $e");
182
+ pollInterval = _activePollInterval;
183
}
184
+ }
185
+ }
186
158
- totalAmount += amount;
187
+ int _syncCheckpointHeight = 0;
188
+
189
+ bool get isSyncing {
190
+ return _isSyncing;
191
+ }
192
+
193
+ set isSyncing(final bool value) {
194
+ _isSyncing = value;
195
+ }
196
+
197
+ bool _isSyncing = false;
198
+
199
+ static int oneshotSyncCount = 0;
200
+
201
+ Future<int> _getLowestSyncHeight() async {
202
+ final accounts = await zkool_account.listAccounts(c: c);
203
+ var lowest = await _getWalletDbHeight();
204
+ for (final acc in accounts) {
205
+ if (acc.id == accountId) continue;
206
+ c = await c.setAccount(account: acc.id);
207
+ lowest = min(lowest, await _getWalletDbHeight());
208
+ }
209
+ c = await c.setAccount(account: accountId);
210
+ return lowest;
211
+ }
212
160
- var address = (output.isParsedAddress ? output.extractedAddress! : output.address).trim();
213
+ Future<bool> _anyAccountNeedsSync(final int currentHeight) =>
214
+ withSharedCoinLock(() => _anyAccountNeedsSyncUnlocked(currentHeight));
215
162
- if (address.isEmpty) {
163
- throw Exception('Empty address for output');
216
+ Future<bool> _anyAccountNeedsSyncUnlocked(final int currentHeight) async {
217
+ final accounts = await zkool_account.listAccounts(c: c);
218
+ for (final acc in accounts) {
219
+ c = await c.setAccount(account: acc.id);
220
+ if (currentHeight > await _getWalletDbHeight()) {
221
+ c = await c.setAccount(account: accountId);
222
+ return true;
223
}
224
+ }
225
+ c = await c.setAccount(account: accountId);
226
+ return false;
227
+ }
228
166
- final paymentUri = WarpApi.decodePaymentURI(coin, address);
167
- String memo = output.memo ?? '';
168
- // String memo = '';
169
- if (paymentUri != null && paymentUri.address != null) {
170
- address = paymentUri.address!;
171
- if (memo.isEmpty && paymentUri.memo != null) {
172
- memo = paymentUri.memo!;
173
- }
229
+ @action
230
+ void _applySyncProgress(final int currentHeight, final int walletHeight) {
231
+ final blocksLeft = (currentHeight - walletHeight).clamp(0, currentHeight);
232
+ _syncCheckpointHeight = walletHeight;
233
+ if (blocksLeft <= 0) {
234
+ syncStatus = _isSyncing ? SyncingSyncStatus(1, 0.999) : SyncedSyncStatus();
235
+ return;
236
+ }
237
+ final ptc = currentHeight > 0 ? (walletHeight / currentHeight).clamp(0.0, 1.0) : 0.0;
238
+ syncStatus = SyncingSyncStatus(blocksLeft, ptc);
239
+ }
240
+
241
+ void _broadcastSyncProgress(final int currentHeight, final int walletHeight) {
242
+ runInAction(() {
243
+ for (final wallet in walletsByAccountId.values) {
244
+ wallet._applySyncProgress(currentHeight, walletHeight);
245
}
246
+ });
247
+ }
248
176
- if (!WarpApi.validAddress(coin, address)) {
177
- throw Exception('Invalid Zcash address: $address');
249
+ Future<int> _getWalletDbHeight() async {
250
+ try {
251
+ return (await zkool_sync.getDbHeight(c: c)).height;
252
+ } catch (_) {
253
+ final accounts = await zkool_account.listAccounts(c: c);
254
+ final account = accounts.where((final a) => a.id == accountId).firstOrNull;
255
+ if (account != null) {
256
+ return account.height;
257
}
258
+ rethrow;
259
+ }
260
+ }
261
180
- int recipientPools = 7;
262
+ void _onSyncCheckpoint(final int currentHeight, final int checkpointHeight) {
263
+ final height = checkpointHeight > _syncCheckpointHeight
264
+ ? checkpointHeight
265
+ : _syncCheckpointHeight;
266
+ _broadcastSyncProgress(currentHeight, height);
267
+ }
268
+
269
+ @action
270
+ Future<void> _refreshSyncStatus() async {
271
+ try {
272
+ await withSharedCoinLock(() async {
273
+ c = await c.setAccount(account: accountId);
274
+ final currentHeight = await zkool_network.getCurrentHeight(c: c);
275
+ final walletDbHeight = await _getWalletDbHeight();
276
+ _broadcastSyncProgress(currentHeight, walletDbHeight);
277
+ });
278
+ } catch (e) {
279
+ printV("refresh sync status: $e");
280
+ }
281
+ }
282
182
- if (address.startsWith('t1') || address.startsWith('t3')) {
183
- recipientPools = 1; // Transparent only
184
- } else if (address.startsWith('zs')) {
185
- recipientPools = 2; // Sapling only
283
+ @action
284
+ Future<bool> _oneshotSync() async {
285
+ try {
286
+ if (isSyncing) {
287
+ return syncStatus is SyncedSyncStatus;
288
}
187
- // For unified addresses (u1...) and other types, use 7 (all pools)
188
-
189
- final builder = RecipientObjectBuilder(
190
- address: address,
191
- pools: recipientPools,
192
- amount: amount.amount.toInt(),
193
- feeIncluded: output.sendAll,
194
- replyTo: false,
195
- memo: memo.isNotEmpty ? memo : null,
289
+ isSyncing = true;
290
+ late final int currentHeight;
291
+ late final int walletDbHeight;
292
+ await withSharedCoinLock(() async {
293
+ c = await c.setAccount(account: accountId);
294
+ currentHeight = await zkool_network.getCurrentHeight(c: c);
295
+ walletDbHeight = await _getWalletDbHeight();
296
+ });
297
+ if (!await _anyAccountNeedsSync(currentHeight)) {
298
+ _syncCheckpointHeight = walletDbHeight;
299
+ if (syncStatus is! SyncedSyncStatus) {
300
+ syncStatus = SyncedSyncStatus();
301
+ }
302
+ isSyncing = false;
303
+ return true;
304
+ }
305
+ await zkool_sync.cancelSync();
306
+ late final List<int> accountList;
307
+ late final int lagHeight;
308
+ await withSharedCoinLock(() async {
309
+ final accounts = await zkool_account.listAccounts(c: c);
310
+ accountList = accounts.map((final a) => a.id).toList()
311
+ ..removeWhere((final a) => a == c.account);
312
+ c = await c.setAccount(account: accountId);
313
+ lagHeight = await _getLowestSyncHeight();
314
+ });
315
+ _broadcastSyncProgress(currentHeight, lagHeight);
316
+ final sync = zkool_sync.synchronize(
317
+ accounts: [c.account, ...accountList],
318
+ currentHeight: currentHeight,
319
+ actionsPerSync: 10000,
320
+ transparentLimit: 100,
321
+ checkpointAge: 200,
322
+ c: c,
323
+ fast: false,
324
);
197
-
198
- recipients.add(Recipient(builder.toBytes()));
325
+ await withSharedCoinLock(() async {
326
+ c = await c.setAccount(account: accountId);
327
+ });
328
+ final randInt = CRC32.compute("${DateTime.now().microsecondsSinceEpoch}").toRadixString(16);
329
+ oneshotSyncCount++;
330
+ final completer = Completer<void>();
331
+ var lastLoggedHeight = walletDbHeight;
332
+ var chainTip = currentHeight;
333
+ late final StreamSubscription<zkool_sync.SyncProgress> subscription;
334
+ subscription = sync.listen(
335
+ (final syncProgress) {
336
+ if (syncProgress.height > chainTip) {
337
+ chainTip = syncProgress.height;
338
+ }
339
+ if (syncProgress.height >= lastLoggedHeight + 5000) {
340
+ lastLoggedHeight = syncProgress.height;
341
+ printV(
342
+ "[${c.account} ($accountList)] [$oneshotSyncCount/$randInt] sync: ${syncProgress.height}",
343
+ );
344
+ unawaited(
345
+ zkool_network.getCurrentHeight(c: c).then((final tip) {
346
+ if (tip > chainTip) {
347
+ chainTip = tip;
348
+ _onSyncCheckpoint(chainTip, syncProgress.height);
349
+ }
350
+ }),
351
+ );
352
+ }
353
+ _onSyncCheckpoint(chainTip, syncProgress.height);
354
+ },
355
+ onError: (final e) {
356
+ printV("[${c.account} ($accountList)] [$oneshotSyncCount/$randInt] error syncing: $e");
357
+ runInAction(() {
358
+ syncStatus = FailedSyncStatus(
359
+ error:
360
+ e.toString().replaceAll("AnyhowException(", "").split("\n").firstOrNull ??
361
+ "Unknown error",
362
+ );
363
+ });
364
+ isSyncing = false;
365
+ if (!completer.isCompleted) {
366
+ completer.complete();
367
+ }
368
+ },
369
+ onDone: () async {
370
+ printV("[${c.account} ($accountList)] [$oneshotSyncCount/$randInt] synchronized");
371
+ oneshotSyncCount--;
372
+ isSyncing = false;
373
+ try {
374
+ await withSharedCoinLock(() async {
375
+ c = await c.setAccount(account: accountId);
376
+ if (await _anyAccountNeedsSyncUnlocked(currentHeight)) {
377
+ final lagHeight = await _getLowestSyncHeight();
378
+ _broadcastSyncProgress(currentHeight, lagHeight);
379
+ return;
380
+ }
381
+ runInAction(() {
382
+ for (final wallet in walletsByAccountId.values) {
383
+ wallet.syncStatus = SyncedSyncStatus();
384
+ }
385
+ });
386
+ for (final wallet in walletsByAccountId.values) {
387
+ unawaited(wallet.updateBalance());
388
+ unawaited(wallet.updateTransactions());
389
+ unawaited(
390
+ ZcashTaddressRotation.updateCache(mainAccountId: wallet.accountId)
391
+ .catchError((final e) {
392
+ printV("rotation cache refresh: $e");
393
+ }),
394
+ );
395
+ }
396
+ });
397
+ } catch (e) {
398
+ printV("sync done height refresh: $e");
399
+ }
400
+ if (!completer.isCompleted) {
401
+ completer.complete();
402
+ }
403
+ },
404
+ );
405
+ await completer.future;
406
+ await subscription.cancel();
407
+ return syncStatus is SyncedSyncStatus;
408
+ } catch (e) {
409
+ syncStatus = FailedSyncStatus(error: e.toString());
410
+ isSyncing = false;
411
+ printV("error syncing: $e");
412
+ return false;
413
}
414
+ }
415
+
416
+ @override
417
+ Future<PendingTransaction> createTransaction(final Object credentials) =>
418
+ _createTransaction(credentials);
419
+
420
+ Future<PendingTransaction> _createTransaction(
421
+ final Object credentials, {
422
+ final int tryReduceFeeAmount = 0,
423
+ }) async {
424
+ final creds = credentials as ZcashTransactionCredentials;
425
+ await updateBalance();
426
+
427
+ final zcashBalance = balance[CryptoCurrency.zec];
428
+ final availableBalance = zcashBalance?.available ?? Money.zero(currency);
429
201
- if (totalAmount > availableBalance) {
202
- throw Exception('Insufficient balance');
430
+ final recipients = <zkool_paydart.Recipient>[];
431
+
432
+ bool receipientPaysFee = false;
433
+
434
+ for (final output in creds.outputs) {
435
+ receipientPaysFee = receipientPaysFee || output.sendAll;
436
+ var amount = output.cryptoAmount;
437
+ if (output.sendAll) {
438
+ amount = availableBalance - Money.fromInt(tryReduceFeeAmount, currency);
439
+ }
440
+ final recipientAddress = output.isParsedAddress ? output.extractedAddress! : output.address;
441
+ recipients.add(
442
+ zkool_paydart.Recipient(
443
+ assetBase: zecBase,
444
+ address: recipientAddress,
445
+ amount: amount.amount,
446
+ userMemo: output.memo,
447
+ ),
448
+ );
449
}
450
205
- final fee = FeeT(
206
- fee: internalCalculateEstimatedFee(creds.priority, null),
207
- minFee: 0,
208
- maxFee: 0,
209
- scheme: 0, // Fixed fee scheme
210
- );
451
+ final sendAmount = creds.outputs
452
+ .map((final out) => out.cryptoAmount)
453
+ .reduce((final a, final b) => a + b);
454
+
455
+ if (availableBalance == sendAmount) {
456
+ receipientPaysFee = true;
457
+ }
458
459
// pools parameter: bitmask for which pools to use for sending
460
// 1=Transparent, 2=Sapling, 4=Orchard, 7=All pools
214
- await ZcashWalletBase.loadProver();
461
// Using 7 (all pools) allows spending from any pool type
216
- final txPlan = await ZcashWalletService.runInDbMutex(
217
- () => WarpApi.prepareTx(
218
- coin,
219
- accountId,
220
- recipients,
221
- 7, // pools: All pools (Transparent + Sapling + Orchard) - allows spending from any pool
222
- 1, // senderUAType: 0 = unified address
223
- 0, // anchorOffset
224
- fee,
225
- ),
462
+ try {
463
+ return await runWithCoin(
464
+ accountId: accountId,
465
+ func: (coin) async {
466
+ final txPlan = await zkool_pay.prepare(
467
+ recipients: recipients,
468
+ options: zkool_pay.PaymentOptions(
469
+ srcPools: 7,
470
+ recipientPaysFee: receipientPaysFee,
471
+ smartTransparent: false,
472
+ ),
473
+ c: coin,
474
+ );
475
+ final txFee = _feeFromTxPlan(txPlan, creds.priority, tryReduceFeeAmount, coin: coin);
476
+ return PendingZcashTransaction(
477
+ zcashWallet: this as ZcashWallet,
478
+ credentials: creds,
479
+ txPlan: txPlan,
480
+ fee: txFee,
481
+ availableBalance: availableBalance,
482
+ );
483
+ },
484
+ );
485
+ } catch (e) {
486
+ if (tryReduceFeeAmount != 0) rethrow;
487
+ final estr = e.toString();
488
+ const prefix = "Not enough funds, ";
489
+ const suffix = " more ZEC required";
490
+ if (estr.contains(prefix) && estr.contains(suffix)) {
491
+ final start = estr.indexOf(prefix) + prefix.length;
492
+ final end = estr.indexOf(suffix, start);
493
+ final amtStr = estr.substring(start, end);
494
+ final amt = double.tryParse(amtStr);
495
+ if (amt == null) rethrow;
496
+ final feeInt = (amt * 100000000).ceil();
497
+ return _createTransaction(credentials, tryReduceFeeAmount: feeInt);
498
+ }
499
+ rethrow;
500
+ }
501
+ }
502
+
503
+ static const _dispPhrase = "Received to disposable address";
504
+
505
+ ZcashTransactionInfo _zcashInfoFromMempoolTx(
506
+ final zkool_mempool.MempoolTx tx,
507
+ final int accountId,
508
+ ) {
509
+ final accountNotes = tx.notes.where((final n) => n.account == accountId);
510
+ final netValue = accountNotes.fold<BigInt>(
511
+ BigInt.zero,
512
+ (final sum, final note) => sum + BigInt.from(note.value),
513
+ );
514
+ final direction = netValue >= BigInt.zero
515
+ ? TransactionDirection.incoming
516
+ : TransactionDirection.outgoing;
517
+ final memo = accountNotes
518
+ .map((final n) => n.memo)
519
+ .whereType<String>()
520
+ .where((final m) => m.isNotEmpty)
521
+ .firstOrNull;
522
+ final recipientAddresses = _recipientAddresses(
523
+ accountNotes.map((final n) => n.address).whereType<String>(),
524
+ direction,
525
);
526
228
- return PendingZcashTransaction(
229
- zcashWallet: this as ZcashWallet,
230
- credentials: creds,
231
- txPlan: txPlan,
232
- fee: Money.fromInt(internalCalculateEstimatedFee(creds.priority, null), currency),
233
- availableBalance: availableBalance,
527
+ final info = ZcashTransactionInfo(
528
+ id: ZcashWalletService.normalizeTxId(tx.txid),
529
+ amount: Money(netValue.abs(), currency),
530
+ fee: Money.zero(currency),
531
+ direction: direction,
532
+ isPending: true,
533
+ date: DateTime.now(),
534
+ height: 0,
535
+ confirmations: 0,
536
+ to: recipientAddresses.isEmpty ? '' : recipientAddresses.first,
537
+ memo: memo,
538
);
539
+ if (recipientAddresses.isNotEmpty) {
540
+ info.outputAddresses = recipientAddresses;
541
+ }
542
+ return info;
543
}
544
237
- static const _dispPhrase = "Received to disposable address";
238
- Future<List<ShieldedTx>> getShieldTxForUi() async {
239
- final tx = (ZcashTaddressRotation.shieldedAccountsTx[accountId] ?? <ShieldedTx>[])
240
- .map((final v) {
241
- final unpacked = v.unpack();
242
- unpacked.memo ??= "";
243
- unpacked.memo = "${unpacked.memo}\n$_dispPhrase".trim();
244
- final List<int> buff = base64.decode(
245
- ZcashTaddressRotation.flatBuffersPack(unpacked.pack),
246
- );
247
- return ShieldedTx(buff);
248
- })
249
- .where((final t) => t.value > 0);
545
+ ZcashTransactionInfo _zcashInfoFromZkoolTx(
546
+ final ZkoolTx tx,
547
+ final int currentHeight, {
548
+ final String? extraMemo,
549
+ final bool isRotationReceive = false,
550
+ final bool isShieldAction = false,
551
+ final TransactionDirection? directionOverride,
552
+ final BigInt? amountOverride,
553
+ }) {
554
+ final confirmations = tx.height > 0 && currentHeight >= tx.height
555
+ ? currentHeight - tx.height + 1
556
+ : 0;
557
+ final memo = extraMemo != null ? "${tx.memo ?? ''}\n$extraMemo".trim() : tx.memo;
558
+ final direction = directionOverride ?? tx.direction;
559
+ final recipientAddresses = _recipientAddresses(_paymentOutputAddresses(tx), direction);
560
+ final info = ZcashTransactionInfo(
561
+ id: tx.txHash,
562
+ amount: Money(amountOverride ?? tx.value, currency),
563
+ fee: Money.zero(currency),
564
+ direction: direction,
565
+ isPending: tx.height == 0,
566
+ date: tx.time,
567
+ height: tx.height,
568
+ confirmations: confirmations,
569
+ to: recipientAddresses.isEmpty ? '' : recipientAddresses.first,
570
+ memo: memo?.isNotEmpty == true ? memo : null,
571
+ txType: tx.type,
572
+ isRotationReceive: isRotationReceive,
573
+ isShieldAction: isShieldAction,
574
+ );
575
+ if (recipientAddresses.isNotEmpty) {
576
+ info.outputAddresses = recipientAddresses;
577
+ }
578
+ return info;
579
+ }
580
251
- return tx.toList();
581
+ bool _isShieldActionTx(final ZkoolTx tx, {required final Set<String> rotationSweepHashes}) {
582
+ if (ZcashWalletService.isAutoshieldTx(tx.txHash)) {
583
+ return true;
584
+ }
585
+ if (rotationSweepHashes.contains(tx.txHash)) {
586
+ return true;
587
+ }
588
+ if (_isPayToSelfAutoshield(tx)) {
589
+ return true;
590
+ }
591
+ if (tx.direction == TransactionDirection.outgoing &&
592
+ (tx.type == TxType.shield || tx.type == TxType.transparentSelfTransfer)) {
593
+ return true;
594
+ }
595
+ return false;
596
}
597
254
- static Map<int, List<ShieldedTx>> temporarySentTx = {};
598
+ bool _isPayToSelfAutoshield(final ZkoolTx tx) {
599
+ if (tx.type != TxType.shield && tx.type != TxType.transparentSelfTransfer) {
600
+ return false;
601
+ }
602
+ if (tx.transparentOrSaplingSpent <= BigInt.zero) {
603
+ return false;
604
+ }
605
+ if (tx.orchardReceived <= BigInt.zero) {
606
+ return false;
607
+ }
608
+ for (final dest in tx.outputAddresses) {
609
+ if (_addressBelongsToWallet(dest)) {
610
+ return true;
611
+ }
612
+ }
613
+ return tx.orchardReceived > BigInt.zero;
614
+ }
615
256
- static String txChecksumKey(final ShieldedTx tx) {
257
- final direction = tx.value > 0 ? TransactionDirection.incoming : TransactionDirection.outgoing;
258
- return 'tx${direction}_${tx.id}_${tx.timestamp}_${CRC32.compute(tx.toString())}';
616
+ bool _shouldSplitAutoshieldTx(final ZkoolTx tx, {required final bool isShield}) {
617
+ if (!isShield) {
618
+ return false;
619
+ }
620
+ if (ZcashWalletService.isAutoshieldTx(tx.txHash) || _isPayToSelfAutoshield(tx)) {
621
+ return tx.transparentOrSaplingSpent > BigInt.zero && tx.orchardReceived > BigInt.zero;
622
+ }
623
+ return false;
624
}
625
261
- @override
262
- Future<Map<String, ZcashTransactionInfo>> fetchTransactions() async {
263
- await ZcashWalletService.loadShieldTxs();
264
- final txs = (await ZcashWalletService.runInDbMutex(
265
- () => WarpApi.getTxs(coin, accountId),
266
- )).toList();
267
- // ShieldedTx{id: 26, txId: 4d1be06ce2c2debec8d98ce4e9434c8aac27c980488b459017d423fdcab37f93, height: 3195705, shortTxId: 4d1be06c, timestamp: 1767730944, name: null, value: 1000000, address: null, memo: , messages: MemoVec{memos: null}}
626
+ static String _txResultKey(final String txHash, {final String suffix = ''}) =>
627
+ 'tx_$txHash$suffix';
628
269
- final shieldTx = await getShieldTxForUi();
629
+ static int _txDisplayPriority(final ZcashTransactionInfo info) {
630
+ if (info.additionalInfo['isAutoShield'] == true) {
631
+ return 3;
632
+ }
633
+ if (info.additionalInfo['isRotationReceive'] == true) {
634
+ return 2;
635
+ }
636
+ return 1;
637
+ }
638
271
- txs.addAll(shieldTx);
272
- final txIds = txs.map((final tx) => tx.txId!.replaceAll('"', '')).toSet();
273
- temporarySentTx[accountId]?.removeWhere(
274
- (final ttx) => txIds.contains(ttx.txId!.replaceAll('"', '')),
275
- );
276
- txs.addAll(temporarySentTx[accountId] ?? []);
639
+ void _offerTx(final Map<String, ZcashTransactionInfo> byHash, final ZcashTransactionInfo info) {
640
+ final hash = info.txHash;
641
+ final existing = byHash[hash];
642
+ if (existing == null) {
643
+ byHash[hash] = info;
644
+ return;
645
+ }
646
+ final infoPriority = _txDisplayPriority(info);
647
+ final existingPriority = _txDisplayPriority(existing);
648
+ if (infoPriority > existingPriority) {
649
+ byHash[hash] = info;
650
+ return;
651
+ }
652
+ if (infoPriority == existingPriority &&
653
+ info.additionalInfo['isAutoShield'] == true &&
654
+ info.direction == TransactionDirection.outgoing &&
655
+ existing.direction == TransactionDirection.incoming) {
656
+ byHash[hash] = info;
657
+ }
658
+ }
659
278
- txs.sort((final a, final b) => a.height.compareTo(b.height));
279
- final Map<String, ZcashTransactionInfo> result = {};
280
- int currentHeight = 0;
281
- try {
282
- currentHeight = await WarpApi.getLatestHeight(coin);
283
- } catch (e) {
284
- printV("Error getting latest height: $e");
660
+ bool _addressBelongsToWallet(final String addr) {
661
+ final addrs = walletAddresses;
662
+ if (addrs.containsAddress(addr) ||
663
+ addrs.hiddenAddresses.contains(addr) ||
664
+ addrs.usedAddresses.contains(addr)) {
665
+ return true;
666
+ }
667
+ for (final infos in addrs.addressInfos.values) {
668
+ for (final info in infos) {
669
+ if (info.address == addr) {
670
+ return true;
671
+ }
672
+ }
673
+ }
674
+ for (final own in [
675
+ addrs.orchardAddress,
676
+ addrs.unifiedAddress,
677
+ addrs.saplingAddress,
678
+ addrs.transparentAddress,
679
+ addrs.address,
680
+ ]) {
681
+ if (own == null || own.isEmpty || own.startsWith('unknown ')) {
682
+ continue;
683
+ }
684
+ if (addr == own || addr.startsWith(own) || own.startsWith(addr)) {
685
+ return true;
686
+ }
687
}
688
+ return false;
689
+ }
690
287
- for (final tx in txs) {
288
- final direction = tx.value > 0
289
- ? TransactionDirection.incoming
290
- : TransactionDirection.outgoing;
291
-
292
- final confirmations = tx.height > 0 && currentHeight > 0 ? currentHeight - tx.height + 1 : 0;
293
-
294
- final txChecksum = txChecksumKey(tx);
295
- final txId = tx.txId ?? tx.shortTxId ?? txChecksum;
296
-
297
- final txInfo = ZcashTransactionInfo(
298
- id: txId.trim().replaceAll('"', ''),
299
- amount: Money.fromInt(tx.value.abs(), currency),
300
- fee: Money.zero(currency),
301
- direction: direction,
302
- isPending: tx.height == 0,
303
- date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000),
304
- height: tx.height,
305
- confirmations: confirmations,
306
- to: tx.address ?? '',
307
- memo: tx.memo,
308
- );
309
- // if (txInfo.additionalInfo['autoShield'] == true) {
310
- // continue;
311
- // }
312
- result[txChecksum] = txInfo;
691
+ List<String> _paymentOutputAddresses(final ZkoolTx tx) {
692
+ final all = tx.outputsWithAddress.toList();
693
+ final external = all.where((final o) => !_addressBelongsToWallet(o.address)).toList();
694
+ final outputs = external.isNotEmpty ? external : all;
695
+ final transparent = outputs.where((final o) => o.pool == NotePool.transparent.index).toList();
696
+ if (transparent.length >= 2) {
697
+ return transparent.map((final o) => o.address).toList();
698
+ }
699
+ return outputs.map((final o) => o.address).toList();
700
+ }
701
+
702
+ List<String> _recipientAddresses(
703
+ final Iterable<String> raw,
704
+ final TransactionDirection direction,
705
+ ) {
706
+ final seen = <String>{};
707
+ final addresses = [
708
+ for (final address in raw)
709
+ if (address.trim().isNotEmpty && seen.add(address.trim())) address.trim(),
710
+ ];
711
+ if (addresses.isEmpty) {
712
+ return [];
713
}
714
715
+ final external = direction == TransactionDirection.incoming
716
+ ? addresses
717
+ : [
718
+ for (final address in addresses)
719
+ if (!_addressBelongsToWallet(address)) address,
720
+ ];
721
+ final list = external.isNotEmpty ? external : addresses;
722
+ if (list.isEmpty) {
723
+ return [];
724
+ }
725
+
726
+ final embedded = {
727
+ for (final address in list)
728
+ if (address.startsWith('u')) ..._uaReceivers(address),
729
+ };
730
+ var result = embedded.isEmpty
731
+ ? list
732
+ : [
733
+ for (final address in list)
734
+ if (address.startsWith('u') || !embedded.contains(address)) address,
735
+ ];
736
+ if (result.isEmpty) {
737
+ result = list;
738
+ }
739
+
740
+ final standalone = [
741
+ for (final address in result)
742
+ if (!address.startsWith('u') && !embedded.contains(address)) address,
743
+ ];
744
+ if (standalone.length >= 2) {
745
+ result = standalone;
746
+ }
747
+
748
+ if (direction == TransactionDirection.incoming) {
749
+ return [
750
+ result.firstWhere(
751
+ (final a) => a.startsWith('u'),
752
+ orElse: () =>
753
+ result.firstWhere((final a) => a.startsWith('z'), orElse: () => result.first),
754
+ ),
755
+ ];
756
+ }
757
return result;
758
}
759
760
+ Set<String> _uaReceivers(final String ua) {
761
+ try {
762
+ final receivers = zkool_account.receiversFromUa(ua: ua, c: ZcashWalletBase.c);
763
+ return {
764
+ for (final address in [receivers.taddr, receivers.saddr, receivers.oaddr])
765
+ if (address != null && address.isNotEmpty) address,
766
+ };
767
+ } catch (_) {
768
+ return {};
769
+ }
770
+ }
771
+
772
@override
319
- Object get keys {
320
- final backup = WarpApi.getBackup(coin, accountId);
773
+ Future<Map<String, ZcashTransactionInfo>> fetchTransactions() async {
774
+ await ZcashWalletService.loadShieldTxs();
775
+ final (txs, currentHeight) = await runWithCoin(
776
+ accountId: accountId,
777
+ func: (coin) async {
778
+ final txsI = await zkool_account.listTxHistory(c: coin);
779
+ final txsA = await Future.wait(
780
+ txsI.map((final tx) => zkool_account.getTxDetails(idTx: tx.id, c: coin)),
781
+ );
782
+ final txs = <ZkoolTx>[];
783
+ for (int i = 0; i < txsI.length; i++) {
784
+ txs.add(ZkoolTx(txsI[i], txsA[i]));
785
+ }
786
+ txs.sort((final a, final b) => a.height.compareTo(b.height));
787
+ var currentHeight = 1;
788
+ try {
789
+ currentHeight = await zkool_network.getCurrentHeight(c: coin);
790
+ } catch (e) {
791
+ printV("failed to get height: $e");
792
+ }
793
+ return (txs, currentHeight);
794
+ },
795
+ );
796
+ final Map<String, ZcashTransactionInfo> byHash = {};
797
+ final rotationTxs = ZcashTaddressRotation.rotationTxsForMainAccount(accountId);
798
+ final rotationSweepHashes = <String>{
799
+ for (final tx in rotationTxs)
800
+ if (tx.direction == TransactionDirection.outgoing) tx.txHash,
801
+ };
802
+
803
+ for (final tx in rotationTxs) {
804
+ if (tx.direction == TransactionDirection.incoming) {
805
+ _offerTx(
806
+ byHash,
807
+ _zcashInfoFromZkoolTx(tx, currentHeight, extraMemo: _dispPhrase, isRotationReceive: true),
808
+ );
809
+ continue;
810
+ }
811
+ _offerTx(byHash, _zcashInfoFromZkoolTx(tx, currentHeight, isShieldAction: true));
812
+ }
813
+
814
+ final Map<String, ZcashTransactionInfo> splitEntries = {};
815
+ for (final tx in txs) {
816
+ final isShield = _isShieldActionTx(tx, rotationSweepHashes: rotationSweepHashes);
817
+ if (_shouldSplitAutoshieldTx(tx, isShield: isShield)) {
818
+ byHash.remove(tx.txHash);
819
+ splitEntries[_txResultKey(tx.txHash, suffix: '_shield')] = _zcashInfoFromZkoolTx(
820
+ tx,
821
+ currentHeight,
822
+ isShieldAction: true,
823
+ directionOverride: TransactionDirection.outgoing,
824
+ amountOverride: tx.transparentOrSaplingSpent,
825
+ );
826
+ splitEntries[_txResultKey(tx.txHash, suffix: '_recv')] = _zcashInfoFromZkoolTx(
827
+ tx,
828
+ currentHeight,
829
+ directionOverride: TransactionDirection.incoming,
830
+ amountOverride: tx.orchardReceived,
831
+ );
832
+ continue;
833
+ }
834
+ _offerTx(byHash, _zcashInfoFromZkoolTx(tx, currentHeight, isShieldAction: isShield));
835
+ }
836
+
837
+ final knownHashes = {for (final tx in txs) tx.txHash, ...byHash.keys};
838
+ for (final mempoolTx in ZcashMempoolService.instance.txsForAccount(accountId)) {
839
+ final hash = ZcashWalletService.normalizeTxId(mempoolTx.txid);
840
+ if (knownHashes.contains(hash)) {
841
+ continue;
842
+ }
843
+ final info = _zcashInfoFromMempoolTx(mempoolTx, accountId);
844
+ if (info.amount.isZero) {
845
+ continue;
846
+ }
847
+ _offerTx(byHash, info);
848
+ }
849
+
850
return {
322
- // "seed": backup.seed,
323
- // "index": backup.index,
324
- "privateSpendKey": backup.sk,
325
- "privateViewKey": backup.fvk,
326
- "uvk": backup.uvk,
327
- "tsk": backup.tsk,
328
- if (lastKnownRestoreHeight != null) "restoreHeight": lastKnownRestoreHeight.toString(),
851
+ for (final entry in byHash.entries) _txResultKey(entry.key): entry.value,
852
+ ...splitEntries,
853
};
854
}
855
856
+ Future<void> _initKeys() async {
857
+ try {
858
+ c = await c.setAccount(account: accountId);
859
+ final ufvk = await zkool_account.getAccountUfvk(account: accountId, c: c, pools: 7);
860
+
861
+ keys = {
862
+ "privateViewKey": ufvk,
863
+ if (lastKnownRestoreHeight != null) "restoreHeight": lastKnownRestoreHeight.toString(),
864
+ };
865
+ } catch (e) {
866
+ keys = {"privateViewKey": e.toString()};
867
+ }
868
+ try {
869
+ c = await c.setAccount(account: accountId);
870
+ final s = (await zkool_account.getAccountSeed(account: accountId, c: c));
871
+
872
+ if (s == null) {
873
+ throw Exception("seed not found");
874
+ }
875
+ final seedPhrase = s.mnemonic.split(" ");
876
+ if ([13, 25].contains(seedPhrase.length)) {
877
+ passphrase = seedPhrase.removeLast();
878
+ } else {
879
+ passphrase = s.phrase;
880
+ }
881
+ seed = s.mnemonic.trim();
882
+ } catch (e) {
883
+ seed = e.toString();
884
+ }
885
+ }
886
+
887
+ @override
888
+ Object keys = {};
889
+
890
@override
891
String get password => _password!;
892
893
@override
894
Future<void> renameWalletFiles(final String newWalletName) async {
337
- final currentWalletPath = await pathForWallet(name: name, type: type);
338
- final currentCacheFile = File(currentWalletPath);
339
- final newWalletPath = await pathForWallet(name: newWalletName, type: type);
340
- if (currentCacheFile.existsSync()) {
341
- await currentCacheFile.copy(newWalletPath);
895
+ await renameWalletFilesForName(fromName: name, toName: newWalletName);
896
+ }
897
+
898
+ static Future<void> renameWalletFilesForName({
899
+ required final String fromName,
900
+ required final String toName,
901
+ }) async {
902
+ if (fromName == toName) {
903
+ return;
904
+ }
905
+ final currentWalletDir = Directory(await pathForWalletDir(name: fromName, type: _type));
906
+ if (!currentWalletDir.existsSync()) {
907
+ throw Exception('Wallet directory not found: $fromName');
908
+ }
909
+ final newWalletDirPath = '${await pathForWalletTypeDir(type: _type)}/$toName';
910
+ if (Directory(newWalletDirPath).existsSync()) {
911
+ throw Exception('Cannot rename wallet: "$toName" already exists');
912
+ }
913
+ await currentWalletDir.rename(newWalletDirPath);
914
+ for (final suffix in const ['', '.v2']) {
915
+ final oldFile = File('$newWalletDirPath/$fromName$suffix');
916
+ if (oldFile.existsSync()) {
917
+ await oldFile.rename('$newWalletDirPath/$toName$suffix');
918
+ }
919
}
343
- Directory(currentWalletPath).deleteSync(recursive: true);
920
}
921
922
@override
923
bool get hasRescan => true;
924
349
- static Future<void> storeZcashHeight(final int height) async {
350
- lastKnownRestoreHeight = height;
351
- final zcashDir = await pathForWalletTypeDir(type: WalletType.zcash);
352
- final zcashInitialSync = File(p.join(zcashDir, ".initial-sync-marker"));
353
- zcashInitialSync.writeAsBytesSync([0x00]);
354
- zcashInitialSync.writeAsStringSync(height.toString(), mode: FileMode.writeOnlyAppend);
925
+ static int? lastKnownRestoreHeight = null;
926
+
927
+ static int zashiAnnouncedBlockHeight = 2419420;
928
+
929
+ Future<dynamic> _getAddressesForAccount(final int id) async {
930
+ return runWithCoin(
931
+ accountId: id,
932
+ func: (final coin) => zkool_account.getAddresses(c: coin, uaPools: 7),
933
+ );
934
}
935
357
- static int? lastKnownRestoreHeight = null;
358
- static Future<int?> loadZcashHeight() async {
359
- final zcashDir = await pathForWalletTypeDir(type: WalletType.zcash);
360
- final zcashInitialSync = File(p.join(zcashDir, ".initial-sync-marker"));
361
- if (!await zcashInitialSync.exists()) {
362
- return null;
363
- }
364
- final bytes = await zcashInitialSync.readAsBytes();
365
- if (bytes.isEmpty) {
366
- return null;
367
- }
368
- final heightString = String.fromCharCodes(bytes.skip(1));
369
- lastKnownRestoreHeight = int.tryParse(heightString);
370
- return lastKnownRestoreHeight;
936
+ bool _addressesMatch(final dynamic old, final dynamic new_) {
937
+ return old.ua == new_.ua &&
938
+ old.oaddr == new_.oaddr &&
939
+ old.saddr == new_.saddr &&
940
+ old.taddr == new_.taddr;
941
}
942
373
- static int zashiAnnouncedBlockHeight = 2419420;
943
+ Future<void> _switchToAccount(final int newAccountId, final int height) async {
944
+ walletsByAccountId.remove(accountId);
945
+ accountId = newAccountId;
946
+ walletsByAccountId[newAccountId] = this;
947
+ walletAddresses.accountId = newAccountId;
948
+ c = await c.setAccount(account: newAccountId);
949
+ lastKnownRestoreHeight = height;
950
+ await walletAddresses.init();
951
+ await _initKeys();
952
+ }
953
954
@override
955
@action
956
Future<void> rescan({required final int height}) async {
378
- try {
379
- syncStatus = StartingScanSyncStatus(height);
380
- printV("rescanning from: $height");
381
- await storeZcashHeight(height);
382
- await ZcashWalletService.runInDbMutex(() async => WarpApi.rescanFrom(coin, height));
383
- await startSync();
384
- } catch (e) {
385
- printV("Rescan error: $e");
386
- syncStatus = FailedSyncStatus(error: e.toString());
387
- }
957
+ await zkool_sync.rewindSync(height: height, account: accountId, c: c);
958
+ // try {
959
+ // syncStatus = StartingScanSyncStatus(height);
960
+ // printV("rescanning from: $height");
961
+ // await zkool_sync.cancelSync();
962
+ // isSyncing = false;
963
+
964
+ // await runWithCoinMutex.acquire();
965
+ // try {
966
+ // final oldAddresses = await _getAddressesForAccount(accountId);
967
+
968
+ // c = await c.setAccount(account: accountId);
969
+ // final accountSeed = await zkool_account.getAccountSeed(account: accountId, c: c);
970
+ // if (accountSeed == null) {
971
+ // throw Exception('Cannot rescan: seed not available');
972
+ // }
973
+
974
+ // final newAccountId = await restoreZcashWalletFromSeed(
975
+ // name: name,
976
+ // seed: accountSeed.mnemonic,
977
+ // passphrase: accountSeed.phrase,
978
+ // birthHeight: height,
979
+ // );
980
+
981
+ // final newAddresses = await _getAddressesForAccount(newAccountId);
982
+ // if (!_addressesMatch(oldAddresses, newAddresses)) {
983
+ // throw Exception('Rescan address verification failed');
984
+ // }
985
+
986
+ // await saveAccountId(name, newAccountId);
987
+ // await _switchToAccount(newAccountId, height);
988
+ // } finally {
989
+ // runWithCoinMutex.release();
990
+ // }
991
+
992
+ // syncStatus = ConnectedSyncStatus();
993
+ // } catch (e) {
994
+ // printV("Rescan error: $e");
995
+ // syncStatus = FailedSyncStatus(error: e.toString());
996
+ // rethrow;
997
+ // }
998
}
999
1000
bool _isTransactionUpdating = false;
1027
}
1028
}
1029
420
- Future<void> updateTransactionsHistory() => updateTransactions();
421
-
1030
@override
1031
Future<void> save() async {}
1032
1033
Future<void> init() async {
1034
try {
1035
+ await ZcashTaddressRotation.init();
1036
await walletAddresses.init();
1037
1038
await updateBalance();
1039
await updateTransactions();
1040
+ unawaited(
1041
+ ZcashTaddressRotation.updateCache(mainAccountId: accountId)
1042
+ .catchError((final e) => printV("rotation cache refresh: $e")),
1043
+ );
1044
+ await _initKeys();
1045
} catch (e) {
1046
printV("Wallet init error: $e");
1047
}
1048
}
1049
1050
@override
437
- String? get seed {
438
- try {
439
- final backup = WarpApi.getBackup(coin, accountId);
440
- final seed = backup.seed?.split(" ");
441
- if (seed == null) {
442
- return null;
443
- }
444
- if ([13, 25].contains(seed.length)) {
445
- seed.removeLast();
446
- }
447
- return seed.join(" ").trim();
448
- } catch (e) {
449
- return null;
450
- }
451
- }
1051
+ String? seed = "";
1052
1053
@override
454
- String? get passphrase {
455
- try {
456
- final backup = WarpApi.getBackup(coin, accountId);
457
- final seed = backup.seed?.split(" ");
458
- if (seed == null) {
459
- return null;
460
- }
461
- if ([13, 25].contains(seed.length)) {
462
- final passphrase = seed.removeLast();
463
- return passphrase;
464
- }
465
- return null;
466
- } catch (e) {
467
- return null;
468
- }
469
- }
1054
+ String? passphrase = "";
1055
1056
@override
1057
Future<String> signMessage(final String message, {final String? address = null}) {
1061
@override
1062
@action
1063
Future<void> startSync() async {
1064
+ if (syncStatus is AttemptingSyncStatus ||
1065
+ syncStatus is SyncronizingSyncStatus ||
1066
+ syncStatus is SyncingSyncStatus) {
1067
+ return;
1068
+ }
1069
try {
480
- syncStatus = AttemptingSyncStatus();
481
-
482
- _initialSyncHeight = 0;
483
- _lastKnownBlockHeight = 0;
484
-
485
- _startSyncStatusUpdates();
486
-
487
- syncStatus = SyncronizingSyncStatus();
488
-
489
- unawaited(
490
- _runWarpSync().catchError((final e) {
491
- isNodeWorking = false;
492
- printV("WarpSync error in startSync: $e");
493
- syncStatus = FailedSyncStatus(error: e.toString());
494
- _stopSyncStatusUpdates();
495
- }),
496
- );
1070
+ _ensureSyncLoopRunning();
1071
+ unawaited(_oneshotSync());
1072
} catch (e) {
1073
isNodeWorking = false;
1074
printV("Sync error: $e");
1075
syncStatus = FailedSyncStatus(error: e.toString());
501
- _stopSyncStatusUpdates();
1076
rethrow;
1077
}
1078
}
1079
1080
static Mutex warpSyncMutex = Mutex();
1081
508
- static Future<void> initialSyncCheck() async {
509
- final zcashDir = await pathForWalletTypeDir(type: WalletType.zcash);
510
- final zcashInitialSync = File(p.join(zcashDir, ".initial-sync-marker"));
511
- if (!zcashInitialSync.existsSync()) {
512
- int chainHeight = 3000000; // fallback if node is offline
513
- try {
514
- chainHeight = await WarpApi.getLatestHeight(coin);
515
- } catch (e) {
516
- printV("Error getting latest height: $e");
517
- }
518
- await ZcashWalletService.runInDbMutex(
519
- () async => await WarpApi.rescanFrom(coin, chainHeight - 150000),
520
- );
521
- await storeZcashHeight(chainHeight);
522
- }
523
- }
524
-
525
- @action
526
- Future<void> _runWarpSync() async {
527
- Timer? _t;
528
- try {
529
- await warpSyncMutex.acquire();
530
- await initialSyncCheck();
531
- isNodeWorking = true;
532
- printV("Starting warpSync for coin $coin, account $accountId");
533
- int? initialQueue = null;
534
- void _cancelSyncIfShould(final Timer t) {
535
- initialQueue ??= ZcashWalletService.dbMutexQueue + 2;
536
- if (ZcashWalletService.dbMutexQueue <= initialQueue!) {
537
- initialQueue = ZcashWalletService.dbMutexQueue;
538
- return;
539
- }
540
- printV(
541
- "Canceling sync! (ZcashWalletService.dbMutexQueue: ${ZcashWalletService.dbMutexQueue} > initialQueue: ${initialQueue})",
542
- );
543
- WarpApi.cancelSync();
544
- t.cancel();
545
- _t = null;
546
- }
547
-
548
- unawaited(
549
- Future.delayed(Duration(seconds: 2)).then((_) {
550
- _t = Timer.periodic(Duration(milliseconds: 100), _cancelSyncIfShould);
551
- }),
552
- );
553
- final result = await ZcashWalletService.runInDbMutex(
554
- () => WarpApi.warpSync(coin, accountId, true, 0, 1000000, 0),
555
- );
556
- printV("warpSync completed with result: $result");
557
-
558
- await _updateSyncStatus();
559
- } catch (e) {
560
- syncStatus = FailedSyncStatus(error: e.toString());
561
- unawaited(Future.delayed(Duration(seconds: 1)).then((_) => unawaited(_runWarpSync())));
562
- _stopSyncStatusUpdates();
563
- } finally {
564
- isNodeWorking = false;
565
- warpSyncMutex.release();
566
- _t?.cancel();
567
- }
568
- }
569
-
570
- void _startSyncStatusUpdates() {
571
- _stopSyncStatusUpdates();
572
- _updateSyncStatus();
573
- _syncStatusTimer = Timer.periodic(const Duration(milliseconds: 5000), (_) {
574
- _updateSyncStatus().catchError((final e) {
575
- printV("Error in sync status update timer: $e");
576
- });
577
- });
578
- }
579
-
580
- void _stopSyncStatusUpdates() {
581
- _syncStatusTimer?.cancel();
582
- _syncStatusTimer = null;
583
- }
584
-
585
- void _startPeriodicSync() {
586
- printV("_startPeriodicSync");
587
- _stopPeriodicSync();
588
- _periodicSyncTimer = Timer.periodic(const Duration(seconds: 5), (_) async {
589
- try {
590
- final chainHeight = await WarpApi.getLatestHeight(coin);
591
- final dbHeight = WarpApi.getDbHeight(coin);
592
- final height = dbHeight.unpack();
593
- final syncHeight = height.height;
594
-
595
- if (syncHeight < chainHeight) {
596
- printV("Periodic sync: chainHeight=$chainHeight, syncHeight=$syncHeight, starting sync");
597
- await _runWarpSync();
598
- } else {
599
- await updateBalance();
600
- await updateTransactions();
601
- }
602
- } catch (e) {
603
- printV("Periodic sync error: $e");
604
- }
605
- });
606
- }
607
-
608
- void _stopPeriodicSync() {
609
- _periodicSyncTimer?.cancel();
610
- _periodicSyncTimer = null;
611
- }
612
-
613
- @action
614
- Future<void> _updateSyncStatus() async {
615
- try {
616
- final dbHeight = WarpApi.getDbHeight(coin);
617
- final height = dbHeight.unpack();
618
- final syncHeight = height.height;
619
-
620
- final chainHeight = await WarpApi.getLatestHeight(coin);
621
-
622
- if (_initialSyncHeight <= 0 && syncHeight > 0) {
623
- _initialSyncHeight = syncHeight;
624
- printV("Initialized sync height to: $_initialSyncHeight");
625
- if (syncHeight - 10 > dbHeight.height) {}
626
- }
627
-
628
- if (chainHeight <= 0) {
629
- if (syncStatus is! ConnectedSyncStatus && syncStatus is! ConnectingSyncStatus) {
630
- syncStatus = ConnectedSyncStatus();
631
- }
632
- try {
633
- await updateBalance();
634
- await updateTransactions();
635
- } catch (e) {
636
- printV("Error updating balance/transactions: $e");
637
- }
638
- return;
639
- }
640
-
641
- if (syncHeight <= 0) {
642
- if (syncStatus is! ConnectedSyncStatus &&
643
- syncStatus is! ConnectingSyncStatus &&
644
- syncStatus is! AttemptingSyncStatus) {
645
- syncStatus = ConnectedSyncStatus();
646
- }
647
- try {
648
- await updateBalance();
649
- await updateTransactions();
650
- } catch (e) {
651
- printV("Error updating balance/transactions: $e");
652
- }
653
- return;
654
- }
655
-
656
- if (syncHeight >= chainHeight && syncHeight > 0) {
657
- syncStatus = SyncedSyncStatus();
658
- _stopSyncStatusUpdates();
659
- await updateBalance();
660
- await updateTransactions();
661
- _startPeriodicSync();
662
- return;
663
- }
664
-
665
- if (_lastKnownBlockHeight != syncHeight) {
666
- _lastKnownBlockHeight = syncHeight;
667
- }
668
-
669
- if (syncHeight < 0 || chainHeight < syncHeight) {
670
- return;
671
- }
672
-
673
- final blocksLeft = chainHeight - syncHeight;
674
- if (blocksLeft <= 0) {
675
- syncStatus = SyncedSyncStatus();
676
- _stopSyncStatusUpdates();
677
- await updateBalance();
678
- await updateTransactions();
679
- _startPeriodicSync();
680
- return;
681
- }
682
-
683
- double ptc = 0.0;
684
- if (_initialSyncHeight > 0) {
685
- final track = chainHeight - _initialSyncHeight;
686
- final diff = track > 0 ? track - (chainHeight - syncHeight) : 0;
687
- ptc = track > 0 && diff >= 0 ? diff / track : 0.0;
688
- } else {
689
- ptc = syncHeight / chainHeight;
690
- }
691
-
692
- syncStatus = SyncingSyncStatus(blocksLeft, ptc.clamp(0.0, 1.0));
693
-
694
- await updateBalance();
695
- await updateTransactions();
696
- } catch (e) {
697
- printV("Sync status update error: $e");
698
- }
699
- }
700
-
701
- String getDiversifiedAddress(final int uaType, {final DateTime? time}) {
702
- try {
703
- final timestamp = (time ?? DateTime.now()).millisecondsSinceEpoch ~/ 1000;
704
- return WarpApi.getDiversifiedAddress(coin, accountId, uaType, timestamp);
705
- } catch (e) {
706
- printV("Error getting diversified address: $e");
707
- return "";
708
- }
709
- }
710
-
1082
static final autoShieldMutex = Mutex();
1083
+ static DateTime? _lastAutoShieldAt;
1084
Future<void> _autoShield() async {
1085
+ if (_lastAutoShieldAt != null &&
1086
+ _lastAutoShieldAt!.isAfter(DateTime.now().subtract(const Duration(seconds: 75)))) {
1087
+ return;
1088
+ }
1089
try {
1090
await autoShieldMutex.acquire();
1091
await _$autoShield();
716
- } catch (e) {
1092
+ } catch (e, s) {
1093
printV("shielding failed: $e");
718
- await Future.delayed(Duration(seconds: 30));
1094
+ s.toString().split("\n").forEach(printV);
1095
} finally {
1096
autoShieldMutex.release();
1097
}
1098
}
1099
1100
Future<void> _$autoShield() async {
725
- final chainHeight = await WarpApi.getLatestHeight(coin);
726
- final dbHeight = WarpApi.getDbHeight(coin);
727
- final height = dbHeight.unpack();
728
- final syncHeight = height.height;
729
- if (chainHeight != syncHeight) {
730
- printV("Not autoshielding: chainHeight(${chainHeight}) != syncHeight(${syncHeight})");
731
- return;
732
- }
733
- final bpConfirmed = WarpApi.getPoolBalances(coin, accountId, 0, false);
734
- if (bpConfirmed.transparent + bpConfirmed.sapling <= 20000) {
1101
+ if (syncStatus is! SyncedSyncStatus) {
1102
return;
1103
}
1104
+ final txId = await runWithCoin(
1105
+ accountId: accountId,
1106
+ func: (coin) async {
1107
+ final _notes = await zkool_account.listNotes(c: coin);
1108
+ final List<zkool_account.TxNote> txNotes = [];
1109
+ for (int i = 0; i < _notes.length; i++) {
1110
+ final note = _notes[i];
1111
+ final noteType = NotePool.values[note.pool];
1112
+ if ([NotePool.sapling, NotePool.transparent].contains(noteType)) {
1113
+ txNotes.add(note);
1114
+ }
1115
+ }
1116
738
- final recipientBuilder = RecipientObjectBuilder(
739
- address: (walletAddresses as ZcashWalletAddresses).orchardAddress,
740
- pools: 4,
741
- feeIncluded: true,
742
- amount: bpConfirmed.transparent + bpConfirmed.sapling,
743
- );
1117
+ final sweepable = txNotes.isEmpty
1118
+ ? BigInt.from(0)
1119
+ : txNotes.map((final txn) => txn.value).reduce((final a, final b) => a + b);
1120
745
- final recipient = Recipient(recipientBuilder.toBytes());
746
- final fee = FeeT(fee: 10000, minFee: 0, maxFee: 0, scheme: 0);
747
- await ZcashWalletBase.loadProver();
748
- final txPlan = await ZcashWalletService.runInDbMutex(
749
- () => WarpApi.prepareTx(
750
- coin,
751
- accountId,
752
- [recipient],
753
- 3, // pools: (Transparent + Sapling)
754
- 1,
755
- 0, // anchorOffset
756
- fee,
757
- ),
758
- );
759
- final _txId = await ZcashWalletService.runInDbMutex(
760
- () => WarpApi.signAndBroadcast(ZcashWalletBase.coin, accountId, txPlan),
1121
+ if (sweepable <= BigInt.from(_autoShieldMinSweep)) {
1122
+ return null;
1123
+ }
1124
+
1125
+ final txPlan = await zkool_pay.prepare(
1126
+ recipients: [
1127
+ zkool_paydart.Recipient(
1128
+ assetBase: zecBase,
1129
+ address: walletAddresses.orchardAddress!,
1130
+ amount: sweepable,
1131
+ ),
1132
+ ],
1133
+ options: zkool_pay.PaymentOptions(
1134
+ srcPools: 3,
1135
+ recipientPaysFee: true,
1136
+ smartTransparent: false,
1137
+ ),
1138
+ c: coin,
1139
+ );
1140
+
1141
+ final signTx = await zkool_pay.signTransaction(pczt: txPlan, c: coin);
1142
+ final txBytes = await zkool_pay.extractTransaction(package: signTx);
1143
+ final currentHeight = await zkool_network.getCurrentHeight(c: coin);
1144
+ return await zkool_pay.broadcastTransaction(
1145
+ height: currentHeight,
1146
+ txBytes: txBytes,
1147
+ c: coin,
1148
+ );
1149
+ },
1150
);
762
- await ZcashWalletService.addShieldedTx(_txId);
763
- printV("shielded: $_txId");
1151
+ if (txId == null) {
1152
+ return;
1153
+ }
1154
+
1155
+ await ZcashWalletService.addShieldedTx(txId);
1156
+ _lastAutoShieldAt = DateTime.now();
1157
+ printV("shielded: $txId");
1158
await updateTransactions();
765
- await updateBalance();
766
- await Future.delayed(Duration(seconds: 75 * 5)); // do not re-try doing that
1159
+ await _refreshBalance(runAutoShield: false);
1160
}
1161
769
- @override
770
- @action
771
- Future<void> updateBalance() async {
1162
+ Future<void> _refreshBalance({required final bool runAutoShield}) async {
1163
try {
773
- final poolBalances = WarpApi.getPoolBalances(coin, accountId, 0, true);
774
- final balances = poolBalances.unpack();
775
- // final notes = WarpApi.getNotesSync(coin, accountId);
776
- // int frozenBalance = 0;
777
- // for (final note in notes) {
778
- // if (note.excluded) {
779
- // frozenBalance += note.value;
780
- // }
781
- // }
782
- final total = balances.orchard + balances.sapling + balances.transparent;
783
- final spendable = total - balances.transparent;
784
-
785
- final confirmedPoolBalances = WarpApi.getPoolBalances(coin, accountId, 3, true);
786
- final confirmedBalances = confirmedPoolBalances.unpack();
787
- final confirmedTotal =
788
- confirmedBalances.orchard + confirmedBalances.sapling + confirmedBalances.transparent;
789
-
790
- int knownOutPending = 0;
791
- ZcashWalletBase.temporarySentTx[accountId]?.forEach((final sTx) {
792
- knownOutPending += sTx.value; // it's negative
793
- });
794
- final confirmedSpendable = confirmedTotal - balances.transparent + knownOutPending;
1164
+ final bal = await runWithCoin(
1165
+ accountId: accountId,
1166
+ func: (coin) => zkool_sync.balance(c: coin),
1167
+ );
1168
796
- unawaited(_autoShield());
1169
+ // 0 - transparent
1170
+ // 1 - sapling
1171
+ // 2 - orchard
1172
+ final confirmedTotal = bal.field0.reduce((final a, final b) => a + b);
1173
+
1174
+ // int knownOutPending = 0;
1175
+ // ZcashWalletBase.temporarySentTx[accountId]?.forEach((final sTx) {
1176
+ // knownOutPending += sTx.value; // it's negative
1177
+ // });
1178
+ final confirmedSpendable = confirmedTotal - bal.field0[0];
1179
+
1180
+ if (runAutoShield) {
1181
+ await _autoShield();
1182
+ }
1183
1184
balance[CryptoCurrency.zec] = ZcashBalance(
799
- Money.fromInt(confirmedSpendable, currency),
800
- Money.fromInt(spendable - confirmedSpendable, currency),
1185
+ Money(confirmedSpendable, currency),
1186
+ Money(confirmedTotal - confirmedSpendable, currency),
1187
frozen: Money.zero(currency),
1188
);
1189
} catch (e, stackTrace) {
1192
}
1193
}
1194
1195
+ @override
1196
+ @action
1197
+ Future<void> updateBalance() async {
1198
+ await _refreshBalance(runAutoShield: true);
1199
+ }
1200
+
1201
@override
1202
Future<bool> verifyMessage(
1203
final String message,
1207
throw UnimplementedError();
1208
}
1209
818
- @observable
819
- late WalletAddresses walletAddresses = ZcashWalletAddresses(accountId, walletInfo);
1210
+ @override
1211
+ late ZcashWalletAddresses walletAddresses = ZcashWalletAddresses(accountId, walletInfo);
1212
1213
static Future<ZcashWallet> create(final WalletCredentials credentials) async {
822
- await _init();
1214
+ await $init();
1215
final newWalletCredentials = credentials as ZcashNewWalletCredentials;
1216
1217
String mnemonic;
1222
mnemonic = bip39.generateMnemonic(strength: strength);
1223
}
1224
833
- final accountId = await _restoreZcashWalletFromSeed(
1225
+ final birthHeight = await ZcashHeight.getBlockHeightByTime(DateTime.now());
1226
+
1227
+ final accountId = await restoreZcashWalletFromSeed(
1228
name: credentials.name,
1229
seed: mnemonic,
1230
passphrase: newWalletCredentials.passphrase,
1231
+ birthHeight: birthHeight,
1232
);
838
- await _saveAccountId(credentials.name, accountId);
1233
+ await saveAccountId(credentials.name, accountId);
1234
final wallet = await open(
1235
name: credentials.name,
1236
password: credentials.password!,
1237
walletInfo: credentials.walletInfo!,
1238
);
844
- await wallet.walletAddresses.saveAddressesInBox();
1239
+ await wallet.init();
1240
return wallet;
1241
}
1242
1243
static Future<ZcashWallet> restore(final WalletCredentials credentials) async {
849
- await _init();
1244
+ await $init();
1245
final fromSeedCredentials = credentials as ZcashFromSeedWalletCredentials;
1246
final String? seed = fromSeedCredentials.seed;
1247
if (seed == null || seed.isEmpty) {
1248
throw Exception('Seed phrase is required for wallet restoration');
1249
}
1250
856
- final accountId = await _restoreZcashWalletFromSeed(
1251
+ final accountId = await restoreZcashWalletFromSeed(
1252
name: credentials.name,
1253
seed: seed,
1254
passphrase: fromSeedCredentials.passphrase,
1255
+ birthHeight: credentials.height!,
1256
);
861
- await _saveAccountId(credentials.name, accountId);
1257
+ await saveAccountId(credentials.name, accountId);
1258
final wallet = await open(
1259
name: credentials.name,
1260
password: credentials.password!,
1261
walletInfo: credentials.walletInfo!,
1262
);
867
- await wallet.walletAddresses.saveAddressesInBox();
868
- printV("height: ${credentials.height}");
869
- if (credentials.height != null) {
870
- await storeZcashHeight(credentials.height!);
871
- unawaited(
872
- Future.delayed(Duration(seconds: 2)).then(
873
- (_) => ZcashWalletService.runInDbMutex(
874
- () async => await WarpApi.rescanFrom(coin, credentials.height ?? 0),
875
- ),
876
- ),
877
- );
878
- }
1263
+ await wallet.init();
1264
return wallet;
1265
}
1266
1267
static Future<ZcashWallet> restoreKeys(final WalletCredentials credentials) async {
883
- await _init();
1268
+ await $init();
1269
final fromKeysCredentials = credentials as ZcashFromKeysWalletCredentials;
1270
final String? keys = fromKeysCredentials.privateKey;
1271
if (keys == null || keys.isEmpty) {
1277
throw Exception('Key is not in secret-extended-key-main1 format');
1278
}
1279
895
- final accountId = await _restoreZcashWalletFromSeed(
1280
+ final accountId = await restoreZcashWalletFromSeed(
1281
name: credentials.name,
1282
seed: keys,
1283
passphrase: fromKeysCredentials.passphrase,
1284
+ birthHeight: credentials.height!,
1285
);
900
- await _saveAccountId(credentials.name, accountId);
1286
+ await saveAccountId(credentials.name, accountId);
1287
final wallet = await open(
1288
name: credentials.name,
1289
password: credentials.password!,
1290
walletInfo: credentials.walletInfo!,
1291
);
906
- await wallet.walletAddresses.saveAddressesInBox();
1292
+ await wallet.init();
1293
printV("height: ${credentials.height}");
908
- if (credentials.height != null) {
909
- await storeZcashHeight(credentials.height!);
910
- unawaited(
911
- Future.delayed(Duration(seconds: 2)).then(
912
- (_) => ZcashWalletService.runInDbMutex(
913
- () async => await WarpApi.rescanFrom(coin, credentials.height ?? 0),
914
- ),
915
- ),
916
- );
917
- }
1294
return wallet;
1295
}
1296
1299
required final String password,
1300
required final WalletInfo walletInfo,
1301
}) async {
926
- await _init();
927
- if (password.isNotEmpty) {
928
- WarpApi.setDbPasswd(coin, password);
929
- }
1302
+ await $init();
1303
+ // if (password.isNotEmpty) {
1304
+ // setDbPasswd(coin, password);
1305
+ // }
1306
final accountId = await getZcashAccountIdForName(name);
1307
if (accountId == null) {
932
- throw Exception("Wallet account not found for name: $name");
1308
+ throw Exception("accountId is null");
1309
}
1310
+ c = await c.setAccount(account: accountId);
1311
final wallet = ZcashWallet(
1312
walletInfo,
1313
await walletInfo.getDerivationInfo(),
1314
accountId: accountId,
1315
);
939
- await wallet.walletAddresses.init();
1316
+ await wallet._initKeys();
1317
return wallet;
1318
}
1319
943
- static Future<int> _restoreZcashWalletFromSeed({
1320
+ static Future<int> restoreZcashWalletFromSeed({
1321
required final String name,
945
- required String seed,
946
- required String? passphrase,
1322
+ required final String seed,
1323
+ required final String? passphrase,
1324
+ required final int birthHeight,
1325
}) async {
948
- if (passphrase?.isNotEmpty == true) {
949
- passphrase = passphrase!.replaceAll(" ", "_");
950
- seed = "${seed} ${passphrase}";
951
- }
952
- final accountId = await ZcashWalletService.runInDbMutex(
953
- () => WarpApi.newAccount(coin, name, seed, 0),
1326
+ // if (passphrase?.isNotEmpty == true) {
1327
+ // passphrase = passphrase!.replaceAll(" ", "_");
1328
+ // seed = "${seed} ${passphrase}";
1329
+ // }
1330
+
1331
+ final accountId = await newAccount(
1332
+ name: name,
1333
+ height: birthHeight,
1334
+ seed: seed,
1335
+ passphrase: passphrase ?? '',
1336
);
1337
return accountId;
1338
}
1339
1340
+ static Future<int?> getLegacyZcashAccountIdForName(final String name) async {
1341
+ final wPath = (await pathForWallet(name: name, type: _type));
1342
+ final f = File(wPath);
1343
+ if (!f.existsSync()) {
1344
+ final accs = await zkool_account.listAccounts(c: c);
1345
+ for (final acc in accs) {
1346
+ if (acc.name == name) {
1347
+ return acc.id;
1348
+ }
1349
+ }
1350
+ }
1351
+ final content = f.readAsStringSync();
1352
+ return int.tryParse(content.trim());
1353
+ }
1354
+
1355
static Future<int?> getZcashAccountIdForName(final String name) async {
959
- final wPath = await pathForWallet(name: name, type: _type);
1356
+ final wPath = (await pathForWallet(name: name, type: _type)) + ".v2";
1357
final f = File(wPath);
1358
if (!f.existsSync()) {
962
- final accounts = WarpApi.getAccountList(coin);
963
- for (final account in accounts) {
964
- if (account.name == name) {
965
- return account.id;
1359
+ final accs = await zkool_account.listAccounts(c: c);
1360
+ for (final acc in accs) {
1361
+ if (acc.name == name) {
1362
+ return acc.id;
1363
}
1364
}
968
- return null;
1365
}
1366
final content = f.readAsStringSync();
1367
return int.tryParse(content.trim());
1368
}
1369
974
- static Future<void> _saveAccountId(final String name, final int accountId) async {
975
- final wPath = await pathForWallet(name: name, type: _type);
1370
+ static Future<void> saveAccountId(final String name, final int accountId) async {
1371
+ final wPath = (await pathForWallet(name: name, type: _type)) + ".v2";
1372
+ final dirName = Directory(wPath).parent.path;
1373
+ if (!Directory(dirName).existsSync()) {
1374
+ Directory(dirName).createSync(recursive: true);
1375
+ }
1376
final f = File(wPath);
1377
f.writeAsStringSync(accountId.toString());
1378
}
1381
1382
static Future<String> getDbDataPath() async {
1383
final pathForWalletType = await pathForWalletTypeDir(type: _type);
984
- final dbDataPath = "${pathForWalletType}/zec.db";
1384
+ final dbDataPath = "${pathForWalletType}/zec.v2.db";
1385
if (!Directory(pathForWalletType).existsSync()) {
1386
Directory(pathForWalletType).createSync(recursive: true);
1387
}
1388
return dbDataPath;
1389
}
1390
991
- static Future<String> getTorDir() async {
992
- final pathForWalletType = await pathForWalletTypeDir(type: _type);
993
- final torPath = "${pathForWalletType}/tor";
994
- if (!Directory(torPath).existsSync()) {
995
- Directory(torPath).createSync(recursive: true);
996
- }
997
- return torPath;
998
- }
999
-
1000
- static Future<String> getFsBlockCacheDir() async {
1391
+ static Future<String> getDbDataPathLegacyYwallet() async {
1392
final pathForWalletType = await pathForWalletTypeDir(type: _type);
1002
- final fsBlockCacheDir = "${pathForWalletType}/blockCache";
1393
+ final dbDataPath = "${pathForWalletType}/zec.db";
1394
if (!Directory(pathForWalletType).existsSync()) {
1395
Directory(pathForWalletType).createSync(recursive: true);
1396
}
1006
- return fsBlockCacheDir;
1397
+ return dbDataPath;
1398
}
1399
1009
- static String? dbDataPath;
1400
static bool _initialized = false;
1401
1402
static void unlockDatabase(final String password) {
1403
_password = password;
1404
}
1405
1406
+ static var c = zkool_coin.Coin();
1407
+
1408
static String? _password;
1017
- static Future<void> _init() async {
1409
+ static Future<void> $init() async {
1410
if (_initialized) return;
1019
- dbDataPath = await getDbDataPath();
1020
- printV("WarpApi.initWallet");
1411
+ _initialized = true;
1412
+ printV(r".$init()");
1413
+ await zkool_frb.RustLib.init();
1414
+ ZcashMempoolService.instance.onAccountsUpdated = (final accountIds) {
1415
+ for (final accountId in accountIds) {
1416
+ unawaited(refreshWalletForAccount(accountId));
1417
+ }
1418
+ };
1419
+ final dbFile = File(await getDbDataPath());
1420
+ final ywalletDbFile = File(await getDbDataPathLegacyYwallet());
1421
+ await zkool_network.initDatadir(directory: dbFile.parent.path);
1422
+ // c = await c.openDatabase(dbFilepath: dbFile.path, password: 'cw_zcash_migration');
1423
+ c = await c.openDatabase(dbFilepath: dbFile.path, password: null);
1424
+ printV("initWallet");
1425
if (_password == null) {
1426
throw Exception("Zcash wallet locked! Please contact support");
1427
}
1024
- if (!File(dbDataPath!).existsSync()) {
1428
+ if (!dbFile.existsSync()) {
1429
//TODO(mrcyjanek): copy-encrypt
1430
}
1027
- // coin+1 = ycash
1028
- WarpApi.setDbPasswd(coin, '');
1029
- WarpApi.setDbPasswd(coin + 1, '');
1030
- WarpApi.initWallet(coin, dbDataPath!);
1031
- WarpApi.initWallet(coin + 1, dbDataPath!);
1032
- try {
1033
- WarpApi.migrateData(coin);
1034
- WarpApi.migrateData(coin + 1);
1035
- } catch (e) {
1036
- printV("zec init failed: $e");
1037
- } // do not fail on network exception
1038
- await loadZcashHeight();
1039
-
1040
- unawaited(loadProver());
1431
+ if (!ywalletDbFile.existsSync()) {
1432
+ //TODO(mrcyjanek): migrate to zkool
1433
+ }
1434
1042
- await ZcashTaddressRotation.init();
1043
- await ZcashTransactionInfo.init();
1435
_initialized = true;
1436
}
1437
1047
- static bool isProverLoaded = false;
1048
- static Future<void> loadProver() async {
1049
- Uint8List? spend;
1050
- Uint8List? output;
1051
- final cacheDir = await getApplicationCacheDirectory();
1052
- try {
1053
- final spendBundle = await rootBundle.load('scripts/zcash_lib/assets/sapling-spend.params');
1054
- final outputBundle = await rootBundle.load('scripts/zcash_lib/assets/sapling-output.params');
1055
- spend = spendBundle.buffer.asUint8List();
1056
- output = outputBundle.buffer.asUint8List();
1057
- if (spend.length == 0 || output.length == 0) {
1058
- spend = await File(cacheDir.path + "/sapling-spend.params").readAsBytesSync();
1059
- output = await File(cacheDir.path + "/sapling-output.params").readAsBytesSync();
1060
- }
1061
- if (spend.length == 0 || output.length == 0) throw Exception("NUH UH");
1062
- } catch (e) {
1063
- printV("$e. Fine, I'll download them.");
1064
- final spendResponse = await ProxyWrapper().get(
1065
- clearnetUri: Uri.parse("https://download.z.cash/downloads/sapling-spend.params"),
1066
- );
1067
- final outputResponse = await ProxyWrapper().get(
1068
- clearnetUri: Uri.parse("https://download.z.cash/downloads/sapling-output.params"),
1069
- );
1070
- spend = spendResponse.bodyBytes;
1071
- output = outputResponse.bodyBytes;
1072
- await File(cacheDir.path + "/sapling-spend.params").writeAsBytes(spend);
1073
- await File(cacheDir.path + "/sapling-output.params").writeAsBytes(output);
1074
- }
1075
- WarpApi.initProver(spend, output);
1076
- isProverLoaded = true;
1438
+ static Future<int> getHeightByDate(final DateTime date) async {
1439
+ final height = await ZcashHeight.getBlockHeightByTime(date);
1440
+ return height;
1441
}
1442
1079
- static Future<int> getBlockHeightByTime(final DateTime time) async {
1080
- final genesisTime = DateTime.utc(2016, 10, 28);
1081
- const genesisHeight = 0;
1082
-
1083
- final firstHalvingTime = DateTime.utc(2020, 11, 18);
1084
- const firstHalvingHeight = 1046400;
1085
-
1086
- final secondHalvingTime = DateTime.utc(2024, 11, 23);
1087
- const secondHalvingHeight = 2726400;
1088
-
1089
- final t = time.toUtc().millisecondsSinceEpoch;
1090
- final t0 = genesisTime.millisecondsSinceEpoch;
1091
- final t1 = firstHalvingTime.millisecondsSinceEpoch;
1092
- final t2 = secondHalvingTime.millisecondsSinceEpoch;
1093
-
1094
- if (t <= t0) return genesisHeight;
1095
-
1096
- if (t < t1) {
1097
- return _interpolate(genesisHeight, firstHalvingHeight, t0, t1, t);
1098
- }
1099
-
1100
- if (t < t2) {
1101
- return _interpolate(firstHalvingHeight, secondHalvingHeight, t1, t2, t);
1102
- }
1103
-
1104
- final secondsSince2 = (t - t2) / 1000.0;
1105
- final blocksSince2 = (secondsSince2 / 76.0).floor();
1106
- return secondHalvingHeight + blocksSince2;
1443
+ static Future<int> newAccount({
1444
+ required final String name,
1445
+ required final int height,
1446
+ required final String seed,
1447
+ required final String passphrase,
1448
+ }) async {
1449
+ final id = await zkool_account.newAccount(
1450
+ na: zkool_account.NewAccount(
1451
+ name: name,
1452
+ restore: true,
1453
+ passphrase: passphrase,
1454
+ key: seed,
1455
+ aindex: 0,
1456
+ birth: height,
1457
+ folder: '',
1458
+ useInternal: true,
1459
+ internal: false,
1460
+ ledger: false,
1461
+ ),
1462
+ c: c,
1463
+ );
1464
+ return id;
1465
}
1466
1109
- static int _interpolate(
1110
- final int hStart,
1111
- final int hEnd,
1112
- final int tStart,
1113
- final int tEnd,
1114
- final int t,
1115
- ) {
1116
- if (tEnd == tStart) return hStart;
1117
- final ratio = (t - tStart) / (tEnd - tStart);
1118
- return (hStart + (hEnd - hStart) * ratio).round();
1119
- }
1467
+ static final runWithCoinMutex = Mutex();
1468
+ static int runWithCoinCount = 0;
1469
1121
- static Future<int> getHeightByDate(final DateTime date) async {
1122
- int height = await getBlockHeightByTime(date);
1470
+ static Future<T> withSharedCoinLock<T>(final FutureOr<T> Function() func) async {
1471
+ await runWithCoinMutex.acquire();
1472
try {
1124
- final h2 = await WarpApi.getBlockHeightByTime(coin, date);
1125
- height = h2;
1126
- } catch (e) {
1127
- printV("getHeightByDate: $e");
1473
+ return await func();
1474
+ } finally {
1475
+ runWithCoinMutex.release();
1476
}
1129
- return height;
1130
- }
1131
-
1132
- bool couldBeZashiWallet() {
1133
- final backup = WarpApi.getBackup(coin, accountId);
1134
- final seed = backup.seed?.split(" ");
1135
- if (seed == null) return false;
1136
- if (zkoolSweep != null) return false;
1137
- if (!(syncStatus is SyncedSyncStatus)) return false;
1138
- return seed.length == 24;
1477
}
1478
1141
- static ZkoolSweep? zkoolSweep;
1142
-
1143
- static bool _didRunRescanInternalChange = false;
1144
- Future<void> rescanInternalChange() async {
1145
- if (_didRunRescanInternalChange) {
1146
- return;
1479
+ static FutureOr<T> runWithCoin<T>({
1480
+ required final int accountId,
1481
+ required final FutureOr<T> Function(zkool_coin.Coin c) func,
1482
+ }) async {
1483
+ var newC = zkool_coin.Coin();
1484
+ newC = await newC.openDatabase(dbFilepath: c.dbFilepath);
1485
+ newC = await newC.setAccount(account: accountId);
1486
+ newC = await newC.setLwd(serverType: c.serverType, url: c.url);
1487
+ newC = await newC.setUseTor(useTor: c.useTor);
1488
+
1489
+ runWithCoinCount++;
1490
+ printV("run with coin: $runWithCoinCount");
1491
+ await runWithCoinMutex.acquire();
1492
+ try {
1493
+ newC = await newC.setAccount(account: accountId);
1494
+ return await func(newC);
1495
+ } finally {
1496
+ runWithCoinMutex.release();
1497
+ runWithCoinCount--;
1498
}
1148
- _didRunRescanInternalChange = true;
1149
- final bal =
1150
- balance[CryptoCurrency.zec]!.available +
1151
- balance[CryptoCurrency.zec]!.unavailable +
1152
- (balance[CryptoCurrency.zec]!.frozen ?? Money.zero(CryptoCurrency.zec));
1153
- final osCacheDir = await getApplicationCacheDirectory();
1154
- final cacheDir = osCacheDir.createTempSync("zkool-import");
1155
- zkoolSweep = ZkoolSweep(
1156
- currentBalance: bal.amount.toInt(),
1157
- cacheDir: cacheDir.path,
1158
- seed: seed ?? '',
1159
- passphrase: password,
1160
- address: (walletAddresses as ZcashWalletAddresses).orchardAddress,
1161
- url: (lastNode!.isSSL ? 'https://' : 'http://') + lastNode!.uriRaw,
1162
- height: (await loadZcashHeight()) ?? zashiAnnouncedBlockHeight,
1163
- );
1164
- unawaited(zkoolSweep!.start());
1165
- int count = 0;
1166
- Timer.periodic(Duration(milliseconds: 1000 ~/ 120), (final Timer t) {
1167
- final msg = ZkoolSweep.msg;
1168
- if (kDebugMode && (++count % (120 ~/ 10)) == 0) {
1169
- printV(msg.message);
1170
- }
1171
- if (msg.blocksLeft == 0 && msg.networkHeight != 0) {
1172
- zkoolSweep = null;
1173
- t.cancel();
1174
- }
1175
- if (msg.blocksLeft == 0) {
1176
- syncStatus = ConnectedSyncStatus();
1177
- return;
1178
- }
1179
- syncStatus = SyncingSyncStatus(msg.blocksLeft, msg.progress);
1180
- });
1499
}
1500
}