feat: zkool2 (#3165)

* feat: zkool2 * feat: rescan on zkool fix: sending to extracted addresses fix: labeling shielding txs fix: autoshield endless loop fix: autoshield t addr not firing fix: autoshield destinations chore: remove old routes for rescan with zkool other minor fixes/cleanups * fix: zcash hardfork * fix: update sync progress more reliably * fix: pending transaction loading fix: autoshield txs fix: taddress fix: fee estimation fix: tx send errors * chore: bump zkool2 to latest version fix: reduce amount of taddrs generated fix: derivation path for internal wallets fix: display address in correct hidden/usable spots fix: get wallet db height before it syncs for the first time fix: taddr rotation with passphrase fix: sync progress ui refresh fix: taddress generation not getting capped fix: hidden addresses not being hidden fix: pending transactions not being shown fix: t address cache misses fix: t address rotation not getting refreshed fix: t address high index not being used * fix: refresh t addresses * fix: patch rust side to make t addr rotation work * fix: ui issue in recipient address fix: zcash multiaddress send to show in the ui fix: potential race condition * minor fixes * format * fix: flashing on receive screen * remove unused 'autoShield' * review

cyan committed Jul 17, 2026 at 09:01 UTC 28c2cc152ef310c0d2e51fadd378521cf925051e
33 files changed +2814 -1622
.github/workflows/pr_test_build_linux.yml
-5
@@ -159,11 +159,6 @@ jobs:
159 - name: Generate localization
160 run: |
161 dart run tool/generate_localization.dart
162 - - name: Workaround zcash
163 - run: |
164 - mkdir -p scripts/zcash_lib/assets/
165 - touch scripts/zcash_lib/assets/sapling-spend.params
166 - touch scripts/zcash_lib/assets/sapling-output.params
162 - name: Build linux
163 run: |
164 flutter build linux --dart-define=hasDevOptions=true --release
.gitignore
+2 -1
@@ -226,4 +226,5 @@ scripts/android/yttrium
226 scripts/bitbox_flutter
227 scripts/reown_flutter
228
229 -*.salive
\ No newline at end of file
229 +*.salive
230 +android/.kotlin
assets/zcash_node_list.yml
+2 -2
@@ -5,8 +5,8 @@
5 useSSL: true
6 isEnabledForAutoSwitching: true
7 label: "Cake Wallet"
8 --
9 - uri: zec.rocks:443
8 +- uri: zec.rocks:443
9 + is_default: true
10 useSSL: true
11 isEnabledForAutoSwitching: true
12 label: "Zec.rocks"
cw_core/lib/get_height_by_date_zec.dart new
+416
@@ -0,0 +1,416 @@
1 +class ZcashHeight {
2 + // for i in $(seq 1 331); do echo "_Checkpoint($(date -j -f "%Y-%m-%d %H:%M:%S" "$(curl -s "https://mainnet.zcashexplorer.app/blocks/${i}0000" | grep -oE '[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}')" "+%s"), ${i}0000),"; done > points.txt
3 + static final List<_Checkpoint> _points = [
4 + _Checkpoint(1479114667, 10000),
5 + _Checkpoint(1480618460, 20000),
6 + _Checkpoint(1482129143, 30000),
7 + _Checkpoint(1483635610, 40000),
8 + _Checkpoint(1485145514, 50000),
9 + _Checkpoint(1486655742, 60000),
10 + _Checkpoint(1488163642, 70000),
11 + _Checkpoint(1489668244, 80000),
12 + _Checkpoint(1491175107, 90000),
13 + _Checkpoint(1492681094, 100000),
14 + _Checkpoint(1494185626, 110000),
15 + _Checkpoint(1495691260, 120000),
16 + _Checkpoint(1497196465, 130000),
17 + _Checkpoint(1498702428, 140000),
18 + _Checkpoint(1500209116, 150000),
19 + _Checkpoint(1501716823, 160000),
20 + _Checkpoint(1503222893, 170000),
21 + _Checkpoint(1504730866, 180000),
22 + _Checkpoint(1506236963, 190000),
23 + _Checkpoint(1507744233, 200000),
24 + _Checkpoint(1509254578, 210000),
25 + _Checkpoint(1510768449, 220000),
26 + _Checkpoint(1512275502, 230000),
27 + _Checkpoint(1513783951, 240000),
28 + _Checkpoint(1515292290, 250000),
29 + _Checkpoint(1516799219, 260000),
30 + _Checkpoint(1518306180, 270000),
31 + _Checkpoint(1519813817, 280000),
32 + _Checkpoint(1521319150, 290000),
33 + _Checkpoint(1522828107, 300000),
34 + _Checkpoint(1524336223, 310000),
35 + _Checkpoint(1525844954, 320000),
36 + _Checkpoint(1527354342, 330000),
37 + _Checkpoint(1528860301, 340000),
38 + _Checkpoint(1530365206, 350000),
39 + _Checkpoint(1531873693, 360000),
40 + _Checkpoint(1533380392, 370000),
41 + _Checkpoint(1534888873, 380000),
42 + _Checkpoint(1536393381, 390000),
43 + _Checkpoint(1537899117, 400000),
44 + _Checkpoint(1539406330, 410000),
45 + _Checkpoint(1540912214, 420000),
46 + _Checkpoint(1542422891, 430000),
47 + _Checkpoint(1543931111, 440000),
48 + _Checkpoint(1545437758, 450000),
49 + _Checkpoint(1546945529, 460000),
50 + _Checkpoint(1548450107, 470000),
51 + _Checkpoint(1549957767, 480000),
52 + _Checkpoint(1551466960, 490000),
53 + _Checkpoint(1552969806, 500000),
54 + _Checkpoint(1554477648, 510000),
55 + _Checkpoint(1555981451, 520000),
56 + _Checkpoint(1557489241, 530000),
57 + _Checkpoint(1558997282, 540000),
58 + _Checkpoint(1560503840, 550000),
59 + _Checkpoint(1562011717, 560000),
60 + _Checkpoint(1563518091, 570000),
61 + _Checkpoint(1565027136, 580000),
62 + _Checkpoint(1566532760, 590000),
63 + _Checkpoint(1568039395, 600000),
64 + _Checkpoint(1569547911, 610000),
65 + _Checkpoint(1571051853, 620000),
66 + _Checkpoint(1572559831, 630000),
67 + _Checkpoint(1574069743, 640000),
68 + _Checkpoint(1575576895, 650000),
69 + _Checkpoint(1576603014, 660000),
70 + _Checkpoint(1577356346, 670000),
71 + _Checkpoint(1578109375, 680000),
72 + _Checkpoint(1578863180, 690000),
73 + _Checkpoint(1579616443, 700000),
74 + _Checkpoint(1580369806, 710000),
75 + _Checkpoint(1581122786, 720000),
76 + _Checkpoint(1581876634, 730000),
77 + _Checkpoint(1582630471, 740000),
78 + _Checkpoint(1583383678, 750000),
79 + _Checkpoint(1584133944, 760000),
80 + _Checkpoint(1584887940, 770000),
81 + _Checkpoint(1585641057, 780000),
82 + _Checkpoint(1586395543, 790000),
83 + _Checkpoint(1587148500, 800000),
84 + _Checkpoint(1587901493, 810000),
85 + _Checkpoint(1588655183, 820000),
86 + _Checkpoint(1589408476, 830000),
87 + _Checkpoint(1590163464, 840000),
88 + _Checkpoint(1590916379, 850000),
89 + _Checkpoint(1591669786, 860000),
90 + _Checkpoint(1592423887, 870000),
91 + _Checkpoint(1593176771, 880000),
92 + _Checkpoint(1593930930, 890000),
93 + _Checkpoint(1594684380, 900000),
94 + _Checkpoint(1595439209, 910000),
95 + _Checkpoint(1596192463, 920000),
96 + _Checkpoint(1596946009, 930000),
97 + _Checkpoint(1597700256, 940000),
98 + _Checkpoint(1598453538, 950000),
99 + _Checkpoint(1599207483, 960000),
100 + _Checkpoint(1599960598, 970000),
101 + _Checkpoint(1600714563, 980000),
102 + _Checkpoint(1601467884, 990000),
103 + _Checkpoint(1602220941, 1000000),
104 + _Checkpoint(1602974722, 1010000),
105 + _Checkpoint(1603728848, 1020000),
106 + _Checkpoint(1604485637, 1030000),
107 + _Checkpoint(1605238381, 1040000),
108 + _Checkpoint(1605992807, 1050000),
109 + _Checkpoint(1606747750, 1060000),
110 + _Checkpoint(1607500606, 1070000),
111 + _Checkpoint(1608255278, 1080000),
112 + _Checkpoint(1609008684, 1090000),
113 + _Checkpoint(1609763782, 1100000),
114 + _Checkpoint(1610517871, 1110000),
115 + _Checkpoint(1611271575, 1120000),
116 + _Checkpoint(1612025546, 1130000),
117 + _Checkpoint(1612779940, 1140000),
118 + _Checkpoint(1613533993, 1150000),
119 + _Checkpoint(1614287669, 1160000),
120 + _Checkpoint(1615042297, 1170000),
121 + _Checkpoint(1615792993, 1180000),
122 + _Checkpoint(1616546145, 1190000),
123 + _Checkpoint(1617299867, 1200000),
124 + _Checkpoint(1618054038, 1210000),
125 + _Checkpoint(1618808513, 1220000),
126 + _Checkpoint(1619563243, 1230000),
127 + _Checkpoint(1620318159, 1240000),
128 + _Checkpoint(1621071298, 1250000),
129 + _Checkpoint(1621826156, 1260000),
130 + _Checkpoint(1622580237, 1270000),
131 + _Checkpoint(1623335553, 1280000),
132 + _Checkpoint(1624090141, 1290000),
133 + _Checkpoint(1624844712, 1300000),
134 + _Checkpoint(1625597720, 1310000),
135 + _Checkpoint(1626351652, 1320000),
136 + _Checkpoint(1627106054, 1330000),
137 + _Checkpoint(1627860648, 1340000),
138 + _Checkpoint(1628614015, 1350000),
139 + _Checkpoint(1629368398, 1360000),
140 + _Checkpoint(1630122679, 1370000),
141 + _Checkpoint(1630877190, 1380000),
142 + _Checkpoint(1631631884, 1390000),
143 + _Checkpoint(1632385851, 1400000),
144 + _Checkpoint(1633141006, 1410000),
145 + _Checkpoint(1633895592, 1420000),
146 + _Checkpoint(1634649550, 1430000),
147 + _Checkpoint(1635403121, 1440000),
148 + _Checkpoint(1636157669, 1450000),
149 + _Checkpoint(1636915133, 1460000),
150 + _Checkpoint(1637670036, 1470000),
151 + _Checkpoint(1638424121, 1480000),
152 + _Checkpoint(1639177413, 1490000),
153 + _Checkpoint(1639931234, 1500000),
154 + _Checkpoint(1640684476, 1510000),
155 + _Checkpoint(1641438827, 1520000),
156 + _Checkpoint(1642192402, 1530000),
157 + _Checkpoint(1642946383, 1540000),
158 + _Checkpoint(1643700901, 1550000),
159 + _Checkpoint(1644455522, 1560000),
160 + _Checkpoint(1645208504, 1570000),
161 + _Checkpoint(1645962081, 1580000),
162 + _Checkpoint(1646715150, 1590000),
163 + _Checkpoint(1647465727, 1600000),
164 + _Checkpoint(1648219045, 1610000),
165 + _Checkpoint(1648973131, 1620000),
166 + _Checkpoint(1649727422, 1630000),
167 + _Checkpoint(1650481408, 1640000),
168 + _Checkpoint(1651234287, 1650000),
169 + _Checkpoint(1651989865, 1660000),
170 + _Checkpoint(1652743327, 1670000),
171 + _Checkpoint(1653497455, 1680000),
172 + _Checkpoint(1654252094, 1690000),
173 + _Checkpoint(1655005944, 1700000),
174 + _Checkpoint(1655759708, 1710000),
175 + _Checkpoint(1656513765, 1720000),
176 + _Checkpoint(1657268358, 1730000),
177 + _Checkpoint(1658022453, 1740000),
178 + _Checkpoint(1658777019, 1750000),
179 + _Checkpoint(1659531133, 1760000),
180 + _Checkpoint(1660283907, 1770000),
181 + _Checkpoint(1661037645, 1780000),
182 + _Checkpoint(1661791977, 1790000),
183 + _Checkpoint(1662544394, 1800000),
184 + _Checkpoint(1663298018, 1810000),
185 + _Checkpoint(1664051450, 1820000),
186 + _Checkpoint(1664805547, 1830000),
187 + _Checkpoint(1665558994, 1840000),
188 + _Checkpoint(1666312456, 1850000),
189 + _Checkpoint(1667066473, 1860000),
190 + _Checkpoint(1667823393, 1870000),
191 + _Checkpoint(1668576623, 1880000),
192 + _Checkpoint(1669330657, 1890000),
193 + _Checkpoint(1670084156, 1900000),
194 + _Checkpoint(1670837831, 1910000),
195 + _Checkpoint(1671591094, 1920000),
196 + _Checkpoint(1672343975, 1930000),
197 + _Checkpoint(1673097703, 1940000),
198 + _Checkpoint(1673852016, 1950000),
199 + _Checkpoint(1674605810, 1960000),
200 + _Checkpoint(1675360050, 1970000),
201 + _Checkpoint(1676113710, 1980000),
202 + _Checkpoint(1676867174, 1990000),
203 + _Checkpoint(1677620242, 2000000),
204 + _Checkpoint(1678374852, 2010000),
205 + _Checkpoint(1679125063, 2020000),
206 + _Checkpoint(1679878121, 2030000),
207 + _Checkpoint(1680632254, 2040000),
208 + _Checkpoint(1681386518, 2050000),
209 + _Checkpoint(1682140381, 2060000),
210 + _Checkpoint(1682894564, 2070000),
211 + _Checkpoint(1683648178, 2080000),
212 + _Checkpoint(1684401932, 2090000),
213 + _Checkpoint(1685156464, 2100000),
214 + _Checkpoint(1685910040, 2110000),
215 + _Checkpoint(1686663406, 2120000),
216 + _Checkpoint(1687417875, 2130000),
217 + _Checkpoint(1688172069, 2140000),
218 + _Checkpoint(1688925610, 2150000),
219 + _Checkpoint(1689679500, 2160000),
220 + _Checkpoint(1690432691, 2170000),
221 + _Checkpoint(1691186349, 2180000),
222 + _Checkpoint(1691940901, 2190000),
223 + _Checkpoint(1692694780, 2200000),
224 + _Checkpoint(1693449743, 2210000),
225 + _Checkpoint(1694203665, 2220000),
226 + _Checkpoint(1694957643, 2230000),
227 + _Checkpoint(1695710712, 2240000),
228 + _Checkpoint(1696465787, 2250000),
229 + _Checkpoint(1697218701, 2260000),
230 + _Checkpoint(1697973031, 2270000),
231 + _Checkpoint(1698726864, 2280000),
232 + _Checkpoint(1699483348, 2290000),
233 + _Checkpoint(1700237650, 2300000),
234 + _Checkpoint(1700991210, 2310000),
235 + _Checkpoint(1701745742, 2320000),
236 + _Checkpoint(1702500723, 2330000),
237 + _Checkpoint(1703253121, 2340000),
238 + _Checkpoint(1704007238, 2350000),
239 + _Checkpoint(1704760935, 2360000),
240 + _Checkpoint(1705514669, 2370000),
241 + _Checkpoint(1706268087, 2380000),
242 + _Checkpoint(1707021423, 2390000),
243 + _Checkpoint(1707776257, 2400000),
244 + _Checkpoint(1708529542, 2410000),
245 + _Checkpoint(1709283065, 2420000),
246 + _Checkpoint(1710037165, 2430000),
247 + _Checkpoint(1710787780, 2440000),
248 + _Checkpoint(1711541488, 2450000),
249 + _Checkpoint(1712294996, 2460000),
250 + _Checkpoint(1713048843, 2470000),
251 + _Checkpoint(1713803486, 2480000),
252 + _Checkpoint(1714557127, 2490000),
253 + _Checkpoint(1715311181, 2500000),
254 + _Checkpoint(1716064271, 2510000),
255 + _Checkpoint(1716818792, 2520000),
256 + _Checkpoint(1717572765, 2530000),
257 + _Checkpoint(1718326333, 2540000),
258 + _Checkpoint(1719080275, 2550000),
259 + _Checkpoint(1719834297, 2560000),
260 + _Checkpoint(1720588042, 2570000),
261 + _Checkpoint(1721341832, 2580000),
262 + _Checkpoint(1722095122, 2590000),
263 + _Checkpoint(1722848604, 2600000),
264 + _Checkpoint(1723602357, 2610000),
265 + _Checkpoint(1724355843, 2620000),
266 + _Checkpoint(1725110085, 2630000),
267 + _Checkpoint(1725863681, 2640000),
268 + _Checkpoint(1726616892, 2650000),
269 + _Checkpoint(1727371443, 2660000),
270 + _Checkpoint(1728124893, 2670000),
271 + _Checkpoint(1728878141, 2680000),
272 + _Checkpoint(1729631572, 2690000),
273 + _Checkpoint(1730385888, 2700000),
274 + _Checkpoint(1731142437, 2710000),
275 + _Checkpoint(1731896266, 2720000),
276 + _Checkpoint(1732650193, 2730000),
277 + _Checkpoint(1733403626, 2740000),
278 + _Checkpoint(1734157390, 2750000),
279 + _Checkpoint(1734911442, 2760000),
280 + _Checkpoint(1735665720, 2770000),
281 + _Checkpoint(1736419845, 2780000),
282 + _Checkpoint(1737173511, 2790000),
283 + _Checkpoint(1737926795, 2800000),
284 + _Checkpoint(1738681511, 2810000),
285 + _Checkpoint(1739434236, 2820000),
286 + _Checkpoint(1740188214, 2830000),
287 + _Checkpoint(1740941352, 2840000),
288 + _Checkpoint(1741692338, 2850000),
289 + _Checkpoint(1742445247, 2860000),
290 + _Checkpoint(1743198927, 2870000),
291 + _Checkpoint(1743952947, 2880000),
292 + _Checkpoint(1744706370, 2890000),
293 + _Checkpoint(1745461377, 2900000),
294 + _Checkpoint(1746214735, 2910000),
295 + _Checkpoint(1746967908, 2920000),
296 + _Checkpoint(1747722271, 2930000),
297 + _Checkpoint(1748474582, 2940000),
298 + _Checkpoint(1749228582, 2950000),
299 + _Checkpoint(1749983372, 2960000),
300 + _Checkpoint(1750737278, 2970000),
301 + _Checkpoint(1751490762, 2980000),
302 + _Checkpoint(1752243931, 2990000),
303 + _Checkpoint(1752997873, 3000000),
304 + _Checkpoint(1753751756, 3010000),
305 + _Checkpoint(1754504935, 3020000),
306 + _Checkpoint(1755259449, 3030000),
307 + _Checkpoint(1756012904, 3040000),
308 + _Checkpoint(1756765335, 3050000),
309 + _Checkpoint(1757519206, 3060000),
310 + _Checkpoint(1758272484, 3070000),
311 + _Checkpoint(1759025597, 3080000),
312 + _Checkpoint(1759778957, 3090000),
313 + _Checkpoint(1760532780, 3100000),
314 + _Checkpoint(1761286210, 3110000),
315 + _Checkpoint(1762039783, 3120000),
316 + _Checkpoint(1762796627, 3130000),
317 + _Checkpoint(1763550532, 3140000),
318 + _Checkpoint(1764303860, 3150000),
319 + _Checkpoint(1765057381, 3160000),
320 + _Checkpoint(1765811222, 3170000),
321 + _Checkpoint(1766564356, 3180000),
322 + _Checkpoint(1767318326, 3190000),
323 + _Checkpoint(1768071956, 3200000),
324 + _Checkpoint(1768824957, 3210000),
325 + _Checkpoint(1769578781, 3220000),
326 + _Checkpoint(1770332206, 3230000),
327 + _Checkpoint(1771086265, 3240000),
328 + _Checkpoint(1771840180, 3250000),
329 + _Checkpoint(1772592783, 3260000),
330 + _Checkpoint(1773343947, 3270000),
331 + _Checkpoint(1774097085, 3280000),
332 + _Checkpoint(1774850161, 3290000),
333 + _Checkpoint(1775603525, 3300000),
334 + _Checkpoint(1776358117, 3310000),
335 + ];
336 +
337 + static int getBlockHeightByTime(DateTime time) {
338 + final t = time.toUtc().millisecondsSinceEpoch;
339 +
340 + if (_points.isEmpty) return 0;
341 +
342 + if (t <= _points.first.timeMs) {
343 + return 1;
344 + }
345 +
346 + for (int i = 0; i < _points.length - 1; i++) {
347 + final p0 = _points[i];
348 + final p1 = _points[i + 1];
349 +
350 + if (t >= p0.timeMs && t < p1.timeMs) {
351 + return _interpolate(
352 + p0.height,
353 + p1.height,
354 + p0.timeMs,
355 + p1.timeMs,
356 + t,
357 + );
358 + }
359 + }
360 +
361 + final last = _points[_points.length - 1];
362 + final prev = _points[_points.length - 2];
363 +
364 + final avgBlockTimeMs = (last.timeMs - prev.timeMs) / (last.height - prev.height);
365 +
366 + final blocksSinceLast = ((t - last.timeMs) / avgBlockTimeMs).floor();
367 +
368 + return last.height + blocksSinceLast;
369 + }
370 +
371 + static DateTime getTimeByBlockHeight(int height) {
372 + if (_points.isEmpty) return DateTime.fromMillisecondsSinceEpoch(0, isUtc: true);
373 + if (height <= 1) {
374 + return DateTime.fromMillisecondsSinceEpoch(_points.first.timeMs, isUtc: true);
375 + }
376 +
377 + for (int i = 0; i < _points.length - 1; i++) {
378 + final p0 = _points[i];
379 + final p1 = _points[i + 1];
380 + if (height >= p0.height && height < p1.height) {
381 + final t = _interpolate(
382 + p0.timeMs,
383 + p1.timeMs,
384 + p0.height,
385 + p1.height,
386 + height,
387 + );
388 + return DateTime.fromMillisecondsSinceEpoch(t, isUtc: true);
389 + }
390 + }
391 +
392 + final last = _points[_points.length - 1];
393 + final prev = _points[_points.length - 2];
394 + final avgBlockTimeMs = (last.timeMs - prev.timeMs) / (last.height - prev.height);
395 + final msSinceLast = ((height - last.height) * avgBlockTimeMs).floor();
396 + return DateTime.fromMillisecondsSinceEpoch(last.timeMs + msSinceLast, isUtc: true);
397 + }
398 +
399 + static int _interpolate(
400 + int h0,
401 + int h1,
402 + int t0,
403 + int t1,
404 + int t,
405 + ) {
406 + final ratio = (t - t0) / (t1 - t0);
407 + return h0 + ((h1 - h0) * ratio).floor();
408 + }
409 +}
410 +
411 +class _Checkpoint {
412 + final int timeMs;
413 + final int height;
414 +
415 + _Checkpoint(int timeSec, this.height) : timeMs = timeSec * 1000;
416 +}
cw_zcash/lib/cw_zcash.dart
-1
@@ -4,7 +4,6 @@
4 library;
5
6 export 'src/zcash_balance.dart';
7 -export 'src/zcash_mnemonic_is_incorrect_exception.dart';
7 export 'src/zcash_transaction_info.dart';
8 export 'src/zcash_transactions.dart';
9 export 'src/zcash_wallet.dart';
cw_zcash/lib/src/legacy/zkool_sweep.dart deleted
-196
@@ -1,196 +0,0 @@
1 -import 'dart:io';
2 -
3 -import 'package:cw_core/utils/print_verbose.dart';
4 -import 'package:path/path.dart' as p;
5 -import 'package:zkool/src/rust/api/account.dart' as zkool_account;
6 -import 'package:zkool/src/rust/api/coin.dart' as zkool_coin;
7 -import 'package:zkool/src/rust/api/sync.dart' as zkool_sync;
8 -import 'package:zkool/src/rust/api/pay.dart' as zkool_pay;
9 -import 'package:zkool/src/rust/api/key.dart' as zkool_key;
10 -import 'package:zkool/src/rust/api/network.dart' as zkool_network;
11 -import 'package:zkool/src/rust/pay.dart' as zkool_paydart;
12 -import 'package:zkool/src/rust/frb_generated.dart' as zkool_frb;
13 -
14 -class ZkoolSweep {
15 - ZkoolSweep({
16 - required this.currentBalance,
17 - required this.cacheDir,
18 - required this.seed,
19 - required this.passphrase,
20 - required this.address,
21 - required this.url,
22 - required this.height,
23 - });
24 - final int currentBalance;
25 - final String seed;
26 - final String cacheDir;
27 - final String passphrase;
28 - final String address;
29 - final String url;
30 - final int height;
31 - static SweepUpdate msg = SweepUpdate(message: 'Idling', currentHeight: 0, networkHeight: 0);
32 - Future<void> start() async {
33 - try {
34 - await _init();
35 - if (!zkool_key.isValidAddress(address: address)) {
36 - msg.message = "$address is not valid";
37 - return;
38 - }
39 -
40 - msg = SweepUpdate(message: 'Starting import', currentHeight: 0, networkHeight: 0);
41 - await _newAccount();
42 - msg.message = "Starting sync";
43 - await warpSync();
44 - msg.message = "Waiting";
45 - while (msg.blocksLeft != 0 && msg.networkHeight != 0) {
46 - await Future.delayed(Duration(seconds: 1));
47 - }
48 - await Future.delayed(Duration(seconds: 5));
49 - final balance = await getBalance();
50 - if (balance.toInt() == balance) {
51 - msg.message = "No need to sweep, balance is the same";
52 - return;
53 - }
54 - msg.message = "Sweeping";
55 - await sweep(balance);
56 - } catch (e) {
57 - msg.message = e.toString();
58 - printV("zkool_sweep: $e");
59 - rethrow;
60 - }
61 - }
62 -
63 - Future<BigInt> getBalance() async {
64 - final b = await zkool_sync.balance(c: c);
65 - return b.field0.reduce((final a, final b) => a + b);
66 - }
67 -
68 - Future<void> sweep(final BigInt amount) async {
69 - msg.message = "prepare";
70 - final tx = await zkool_pay.prepare(
71 - recipients: [zkool_paydart.Recipient(address: address, amount: amount)],
72 - options: zkool_pay.PaymentOptions(
73 - srcPools: 7,
74 - recipientPaysFee: true,
75 - smartTransparent: false,
76 - ),
77 - c: c,
78 - );
79 - msg.message = "getLatestHeight";
80 - final height = await getLatestHeight();
81 - msg.message = "signTransaction";
82 - final signTx = await zkool_pay.signTransaction(pczt: tx, c: c);
83 - msg.message = "extractTransaction";
84 - final txBytes = await zkool_pay.extractTransaction(package: signTx);
85 - msg.message = "broadcastTransaction";
86 - final result = await zkool_pay.broadcastTransaction(height: height, txBytes: txBytes, c: c);
87 - msg.message = "result";
88 - if (result.isEmpty) {
89 - msg.message = "Unknown error";
90 - throw Exception(result);
91 - }
92 - }
93 -
94 - static var c = zkool_coin.Coin();
95 -
96 - static bool _isInit = false;
97 -
98 - File get dbFile => File(p.join(cacheDir, "zkool-tmp-mirror.db"));
99 -
100 - Future<int> warpSync() async {
101 - final currentHeight = await getLatestHeight();
102 - msg.networkHeight = currentHeight;
103 - final sync = zkool_sync.synchronize(
104 - accounts: [currentAccount],
105 - currentHeight: currentHeight,
106 - actionsPerSync: 10000,
107 - transparentLimit: 100,
108 - checkpointAge: 200,
109 - c: c,
110 - );
111 - bool success = false;
112 - await sync.listen(
113 - (final syncProgress) {
114 - msg.currentHeight = syncProgress.height;
115 - msg.message = "Sync ${syncProgress.height} / ${syncProgress.time}";
116 - },
117 - onError: (final e) {
118 - msg.message = e.toString();
119 - },
120 - onDone: () {
121 - msg.message = "Sync done!";
122 - success = true;
123 - },
124 - );
125 -
126 - if (msg.currentHeight != currentHeight && msg.currentHeight != 0) {
127 - msg.message = "Canceling sync";
128 - await zkool_sync.cancelSync();
129 - await Future.delayed(Duration(seconds: 1));
130 - return warpSync();
131 - }
132 - return 0;
133 - }
134 -
135 - Future<int> getLatestHeight() async {
136 - try {
137 - return await zkool_network.getCurrentHeight(c: c);
138 - } catch (e) {
139 - printV("getLatestHeight: $e");
140 - }
141 - return 0;
142 - }
143 -
144 - Future<void> _newAccount() async {
145 - final id = await zkool_account.newAccount(
146 - na: zkool_account.NewAccount(
147 - name: 'restoretmp' + DateTime.now().microsecondsSinceEpoch.toString(),
148 - restore: true,
149 - passphrase: "",
150 - key: seed,
151 - aindex: 0,
152 - birth: height,
153 - folder: '',
154 - useInternal: true,
155 - internal: false,
156 - ledger: false,
157 - ),
158 - c: c,
159 - );
160 - currentAccount = id;
161 - c = await c.setAccount(account: id);
162 - }
163 -
164 - int currentAccount = 0;
165 -
166 - Future<void> _init() async {
167 - if (_isInit) return;
168 - _isInit = true;
169 - printV(".init()");
170 - await zkool_frb.RustLib.init();
171 - await zkool_network.initDatadir(directory: dbFile.parent.path);
172 - c = await c.openDatabase(dbFilepath: dbFile.path, password: 'cw_zcash_migration');
173 - c = c.setLwd(url: url, serverType: 0);
174 - printV(".init(): Done");
175 - }
176 -}
177 -
178 -class SweepUpdate {
179 - SweepUpdate({required this.message, required this.currentHeight, required this.networkHeight});
180 - String message;
181 - int currentHeight;
182 - int networkHeight;
183 - double get progress {
184 - if (networkHeight == 0) return 0.01;
185 - return currentHeight / networkHeight;
186 - }
187 -
188 - int get blocksLeft => networkHeight - currentHeight;
189 -
190 - Map<String, dynamic> toJson() => {
191 - "message": message,
192 - "currentHeight": currentHeight,
193 - "networkHeight": networkHeight,
194 - "progress": progress,
195 - };
196 -}
cw_zcash/lib/src/migrate.dart new
+96
@@ -0,0 +1,96 @@
1 +import 'dart:math';
2 +
3 +import 'package:cw_core/get_height_by_date_zec.dart';
4 +import 'package:cw_core/pathForWallet.dart';
5 +import 'package:cw_core/utils/print_verbose.dart';
6 +import 'package:cw_core/wallet_type.dart';
7 +import 'package:cw_zcash/cw_zcash.dart';
8 +import 'package:sqflite_common_ffi/sqflite_ffi.dart';
9 +import 'package:path/path.dart' as p;
10 +
11 +class ZkoolSeed {
12 + ZkoolSeed({required this.seed, required this.passphrase});
13 +
14 + factory ZkoolSeed.fromYwalletSeed({required final String ywalletSeed}) {
15 + final splSeed = ywalletSeed.split(" ");
16 + String seed = "";
17 + ;
18 + String passphrase = "";
19 + if ([13, 25].contains(splSeed.length)) {
20 + seed = splSeed.take(splSeed.length - 1).join(" ");
21 + passphrase = ywalletSeed.replaceFirst("${seed} ", "");
22 + } else {
23 + seed = ywalletSeed;
24 + }
25 + return ZkoolSeed(seed: seed, passphrase: passphrase);
26 + }
27 +
28 + String seed;
29 + String passphrase;
30 +}
31 +
32 +class YwalletAccountInfo {
33 + YwalletAccountInfo({
34 + required this.idAccount,
35 + required this.name,
36 + required this.seed,
37 + required this.aindex,
38 + required this.sk,
39 + required this.ivk,
40 + required this.address,
41 + });
42 + final int idAccount;
43 + final String name;
44 + final String seed;
45 + late final zkoolSeed = ZkoolSeed.fromYwalletSeed(ywalletSeed: seed);
46 + final int aindex;
47 + final String sk;
48 + final String ivk;
49 + final String address;
50 + static YwalletAccountInfo fromJson(final Map<String, dynamic> j) => YwalletAccountInfo(
51 + idAccount: j['id_account'],
52 + name: j['name'],
53 + seed: j['seed'],
54 + aindex: j['aindex'],
55 + sk: j['sk'],
56 + ivk: j['ivk'],
57 + address: j['address'],
58 + );
59 +}
60 +
61 +Future<void> migrateOldSqliteToZkool2({required final String walletName}) async {
62 + final dbFile = p.join(await pathForWalletTypeDir(type: WalletType.zcash), "zec.db");
63 + final migrateDb = await openDatabase(dbFile);
64 + final outputRaw = await migrateDb.rawQuery(
65 + "SELECT `id_account`, `name`, `seed`, `aindex`, `sk`, `ivk`, `address` FROM `accounts`",
66 + );
67 + final output = outputRaw.map(YwalletAccountInfo.fromJson);
68 +
69 + final walletId = await ZcashWalletBase.getLegacyZcashAccountIdForName(walletName);
70 + for (final ya in output) {
71 + if (ya.idAccount != walletId) {
72 + continue;
73 + }
74 + printV("migrating account: ${ya.name}");
75 + int birthHeight = await ZcashHeight.getBlockHeightByTime(DateTime(2026, 1, 1));
76 +
77 + final accTxs = await migrateDb.rawQuery(
78 + "SELECT height FROM transactions WHERE account = ${ya.idAccount}",
79 + );
80 + for (final tx in accTxs) {
81 + final txHeight = int.tryParse(tx['height'].toString()) ?? birthHeight;
82 + birthHeight = min(birthHeight, txHeight);
83 + }
84 +
85 + final accountId = await ZcashWalletBase.restoreZcashWalletFromSeed(
86 + name: ya.name,
87 + seed: ya.zkoolSeed.seed,
88 + passphrase: ya.zkoolSeed.passphrase,
89 + birthHeight: birthHeight,
90 + );
91 + await ZcashWalletBase.saveAccountId(ya.name, accountId);
92 + return;
93 + }
94 +
95 + throw Exception("migration not finished (wallet name: $walletName not found)");
96 +}
cw_zcash/lib/src/pending_zcash_transaction.dart
+30 -48
@@ -1,14 +1,10 @@
1 -import 'dart:convert';
2 -import 'dart:math';
3 -
1 import 'package:cw_core/amount/money.dart';
5 -import 'package:cw_core/crypto_currency.dart';
6 -import 'package:cw_core/output_info.dart';
2 +import 'package:cw_core/exceptions.dart';
3 import 'package:cw_core/pending_transaction.dart';
4 +import 'package:cw_core/utils/print_verbose.dart';
5 import 'package:cw_zcash/cw_zcash.dart';
9 -import 'package:cw_zcash/src/zcash_taddress_rotation.dart';
10 -import 'package:warp_api/data_fb_generated.dart';
11 -import 'package:warp_api/warp_api.dart';
6 +import 'package:zkool/src/rust/api/pay.dart' as zkool_pay;
7 +import 'package:zkool/src/rust/api/network.dart' as zkool_network;
8
9 class PendingZcashTransaction with PendingTransaction {
10 PendingZcashTransaction({
@@ -21,7 +17,7 @@ class PendingZcashTransaction with PendingTransaction {
17
18 final ZcashWallet zcashWallet;
19 final ZcashTransactionCredentials credentials;
24 - final String txPlan;
20 + final zkool_pay.PcztPackage txPlan;
21 String? _txId;
22 final Money availableBalance;
23
@@ -32,56 +28,42 @@ class PendingZcashTransaction with PendingTransaction {
28 String get hex => '';
29
30 @override
35 - Money get amount => Money.fromInt(_totalAmount, CryptoCurrency.zec);
36 -
37 - @override
38 - String get amountFormatted => amount.toString();
39 -
40 - int get _totalAmount {
31 + Money get amount {
32 final isAll = credentials.outputs.fold<bool>(false, (final a, final b) => a || (b.sendAll));
33 if (isAll) {
43 - return availableBalance.amount.toInt();
34 + return availableBalance - fee;
35 }
45 - return credentials.outputs.fold<int>(
46 - 0,
47 - (final a, final b) => a + b.cryptoAmount.amount.toInt(),
48 - );
36 + return credentials.outputs
37 + .map((final output) => output.cryptoAmount)
38 + .reduce((final a, final b) => a + b);
39 }
40
41 + @override
42 + String get amountFormatted => amount.toString();
43 +
44 @override
45 final Money fee;
46
47 @override
48 Future<void> commit() async {
56 - _txId = await ZcashWalletService.runInDbMutex(
57 - () => WarpApi.signAndBroadcast(ZcashWalletBase.coin, zcashWallet.accountId, txPlan),
58 - );
59 - ZcashWalletBase.temporarySentTx[zcashWallet.accountId] ??= [];
60 - ZcashWalletBase.temporarySentTx[zcashWallet.accountId]?.add(
61 - ShieldedTx(
62 - base64.decode(
63 - ZcashTaddressRotation.flatBuffersPack(
64 - ShieldedTxT(
65 - id: Random().nextInt(pow(2, 32).toInt()),
66 - txId: _txId,
67 - height: 0,
68 - timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000,
69 - value: -_totalAmount,
70 - ).pack,
71 - ),
72 - ),
73 - ),
74 - );
75 - await ZcashTransactionInfo.addCachedDestinationAddress(
76 - _txId ?? '',
77 - credentials.outputs.reduce((final o1, final o2) {
78 - return OutputInfo(
79 - cryptoAmount: Money.zero(CryptoCurrency.zec),
80 - address: "${o1.address},${o2.address}",
81 - sendAll: false,
82 - isParsedAddress: false,
49 + await ZcashWalletBase.runWithCoin(
50 + accountId: zcashWallet.accountId,
51 + func: (coin) async {
52 + final signTx = await zkool_pay.signTransaction(pczt: txPlan, c: coin);
53 + final txBytes = await zkool_pay.extractTransaction(package: signTx);
54 + final currentHeight = await zkool_network.getCurrentHeight(c: coin);
55 + final result = await zkool_pay.broadcastTransaction(
56 + height: currentHeight,
57 + txBytes: txBytes,
58 + c: coin,
59 );
84 - }).address,
60 + printV("result: $result");
61 + final txId = ZcashWalletService.normalizeTxId(result);
62 + if (txId.length != 64) {
63 + throw TransactionCommitFailed(errorMessage: result);
64 + }
65 + _txId = txId;
66 + },
67 );
68 await zcashWallet.updateTransactions();
69 await zcashWallet.updateBalance();
cw_zcash/lib/src/util/hex.dart new
+5
@@ -0,0 +1,5 @@
1 +import 'package:flutter/services.dart';
2 +
3 +String uint8ListToHex(final Uint8List bytes) {
4 + return bytes.map((final b) => b.toRadixString(16).padLeft(2, '0')).join();
5 +}
cw_zcash/lib/src/zcash_mempool.dart new
+118
@@ -0,0 +1,118 @@
1 +import 'dart:async';
2 +
3 +import 'package:cw_core/utils/print_verbose.dart';
4 +import 'package:cw_zcash/src/zcash_wallet_service.dart';
5 +import 'package:zkool/src/rust/api/coin.dart' as zkool_coin;
6 +import 'package:zkool/src/rust/api/mempool.dart' as zkool_mempool;
7 +
8 +typedef ZcashMempoolRefreshCallback = void Function(Set<int> accountIds);
9 +
10 +class ZcashMempoolService {
11 + ZcashMempoolService._();
12 +
13 + static final ZcashMempoolService instance = ZcashMempoolService._();
14 +
15 + zkool_mempool.Mempool? _mempool;
16 + final Map<String, zkool_mempool.MempoolTx> _txsById = {};
17 +
18 + zkool_mempool.Mempool get _mempoolInstance => _mempool ??= zkool_mempool.Mempool();
19 +
20 + ZcashMempoolRefreshCallback? onAccountsUpdated;
21 +
22 + bool _loopRunning = false;
23 + String? _nodeUrl;
24 + StreamSubscription<zkool_mempool.MempoolMsg>? _subscription;
25 +
26 + List<zkool_mempool.MempoolTx> txsForAccount(final int accountId) => _txsById.values
27 + .where(
28 + (final tx) =>
29 + tx.notes.any((final n) => n.account == accountId) ||
30 + tx.amounts.any((final a) => a.account == accountId),
31 + )
32 + .toList();
33 +
34 + Future<void> ensureRunning(final zkool_coin.Coin c) async {
35 + if (c.url.isEmpty) {
36 + return;
37 + }
38 + if (_loopRunning && _nodeUrl == c.url) {
39 + return;
40 + }
41 + await stop();
42 + _nodeUrl = c.url;
43 + _loopRunning = true;
44 + unawaited(_runLoop(c));
45 + }
46 +
47 + Future<void> stop() async {
48 + _loopRunning = false;
49 + await _subscription?.cancel();
50 + _subscription = null;
51 + _txsById.clear();
52 + try {
53 + await _mempool?.cancel();
54 + } catch (e) {
55 + printV("mempool cancel: $e");
56 + }
57 + _mempool = null;
58 + }
59 +
60 + Future<void> _runLoop(final zkool_coin.Coin c) async {
61 + while (_loopRunning && _nodeUrl == c.url) {
62 + try {
63 + final completer = Completer<void>();
64 + _subscription = _mempoolInstance
65 + .run(c: c)
66 + .listen(
67 + (final msg) {
68 + if (!_loopRunning) {
69 + return;
70 + }
71 + switch (msg) {
72 + case zkool_mempool.MempoolMsg_BlockHeight():
73 + _onBlockMined();
74 + case zkool_mempool.MempoolMsg_TxId(:final field0):
75 + _onMempoolTx(field0);
76 + }
77 + },
78 + onDone: () {
79 + if (!completer.isCompleted) {
80 + completer.complete();
81 + }
82 + },
83 + onError: (final e) {
84 + printV("mempool stream error: $e");
85 + if (!completer.isCompleted) {
86 + completer.complete();
87 + }
88 + },
89 + );
90 + await completer.future;
91 + } catch (e) {
92 + printV("mempool loop error: $e");
93 + }
94 + if (_loopRunning) {
95 + await Future.delayed(const Duration(seconds: 5));
96 + }
97 + }
98 + }
99 +
100 + void _onBlockMined() {
101 + final affected = _allAffectedAccounts();
102 + _txsById.clear();
103 + onAccountsUpdated?.call(affected);
104 + }
105 +
106 + void _onMempoolTx(final zkool_mempool.MempoolTx tx) {
107 + final normalized = ZcashWalletService.normalizeTxId(tx.txid);
108 + _txsById[normalized] = tx;
109 + onAccountsUpdated?.call(_affectedAccountsForTx(tx));
110 + }
111 +
112 + Set<int> _allAffectedAccounts() => _txsById.values.expand(_affectedAccountsForTx).toSet();
113 +
114 + Set<int> _affectedAccountsForTx(final zkool_mempool.MempoolTx tx) => {
115 + ...tx.notes.map((final n) => n.account),
116 + ...tx.amounts.map((final a) => a.account),
117 + };
118 +}
cw_zcash/lib/src/zcash_mnemonic_is_incorrect_exception.dart deleted
-5
@@ -1,5 +0,0 @@
1 -class ZcashMnemonicIsIncorrectException implements Exception {
2 - @override
3 - String toString() =>
4 - 'Zcash mnemonic has incorrect format. Mnemonic should contain 12 or 24 words separated by space.';
5 -}
cw_zcash/lib/src/zcash_taddress_rotation.dart
+469 -340
@@ -12,368 +12,484 @@
12 // turned off and replaced with proper solution.
13
14 import 'dart:async';
15 -import 'dart:convert';
16 -import 'dart:io';
15 +import 'dart:math' show max;
16
18 -import 'package:cw_core/pathForWallet.dart';
17 +import 'package:cw_core/sync_status.dart';
18 +import 'package:cw_core/transaction_direction.dart';
19 import 'package:cw_core/utils/print_verbose.dart';
20 -import 'package:cw_core/wallet_info.dart';
21 -import 'package:cw_core/wallet_type.dart';
20 import 'package:cw_zcash/src/util/crc32.dart';
21 import 'package:cw_zcash/src/zcash_wallet.dart';
22 import 'package:cw_zcash/src/zcash_wallet_service.dart';
25 -import 'package:flutter/foundation.dart';
26 -import 'package:warp_api/data_fb_generated.dart';
27 -import 'package:warp_api/warp_api.dart';
28 -import 'package:flat_buffers/flat_buffers.dart' as fb;
29 -import 'package:path/path.dart' as p;
23 +import 'package:cw_zcash/src/zkool_compat.dart';
24 +import 'package:cw_zcash/src/zkooltx.dart';
25 +import 'package:zkool/src/rust/api/account.dart' as zkool_account;
26 +import 'package:zkool/src/rust/api/coin.dart' as zkool_coin;
27 +import 'package:zkool/src/rust/api/sync.dart' as zkool_sync;
28 +import 'package:zkool/src/rust/api/pay.dart' as zkool_pay;
29 +import 'package:zkool/src/rust/api/network.dart' as zkool_network;
30 +import 'package:zkool/src/rust/api/sweep.dart' as zkool_sweep;
31 +import 'package:zkool/src/rust/pay.dart' as zkool_paydart;
32
33 class ZcashTaddressRotation {
32 - ZcashTaddressRotation(this.accountIndex);
33 - final int accountIndex;
34 -
34 static bool _isStarted = false;
35 + static zkool_coin.Coin get c => ZcashWalletBase.c;
36 + static const int _sweepThreshold = 30000;
37 + static const int _lookahead = 5;
38 + // Matches transparentLimit in ZcashWalletBase._oneshotSync.
39 + static const int _transparentSyncLimit = 100;
40
37 - static const coin = ZcashWalletBase.coin;
38 - static String seedForOffset(final String seed) {
39 - final seedWords = seed.split(" ");
41 + static zkool_account.Seed seedForOffset(final zkool_account.Seed seed) {
42 + final seedStr = "${seed.mnemonic} ${seed.phrase.replaceAll(" ", "_")}".trim();
43 + final seedWords = seedStr.split(" ");
44 if ([12, 24].contains(seedWords.length)) {
45 seedWords.add("");
46 }
47 final lastI = seedWords.length - 1;
44 - final crc = CRC32.compute(seed);
45 - seedWords[lastI] += ":tgen:${crc}";
46 - return seedWords.join(" ");
48 + final crc = CRC32.compute(seedStr);
49 + seedWords[lastI] += ":tgen:$crc";
50 + final phrase = seedWords.removeLast();
51 + return zkool_account.Seed(mnemonic: seedWords.join(" "), phrase: phrase, aindex: 0);
52 + }
53 +
54 + static bool zkoolAccountSeedIsEqual(final zkool_account.Seed a, final zkool_account.Seed b) {
55 + return a.mnemonic == b.mnemonic && a.phrase == b.phrase;
56 }
57
49 - static bool isSeedForWallet(final String? mainWallet, final String? subWallet) {
58 + static bool isSeedForWallet(
59 + final zkool_account.Seed? mainWallet,
60 + final zkool_account.Seed? subWallet,
61 + ) {
62 if (mainWallet == null || subWallet == null) return false;
51 - return seedForOffset(mainWallet.trim()) == subWallet;
63 + return zkoolAccountSeedIsEqual(seedForOffset(mainWallet), subWallet);
64 }
65
54 - static Map<int, List<Account>> rotationAccounts = {};
55 - static Map<int, List<Account>> rotationAccountsUsable = {};
56 - static Map<int, List<ShieldedTx>> shieldedAccountsTx = {};
57 - static Future<void> init() async {
58 - printV("Deserializing previous state");
59 - if (_isStarted) {
66 + static Map<int, List<ZkoolTx>> shieldedAccountsTx = {};
67 + static final Map<int, int> _rotationAccountByMain = {};
68 + static final Set<int> _resyncedRotationAccounts = {};
69 + static final Set<String> _poolRescanAttempted = {};
70 + static bool _sweepJobRunning = false;
71 + static final Map<int, DateTime> _lastSweepBroadcastAt = {};
72 +
73 + static bool _poolNeedsRescan(final List<zkool_account.TAddressTxCount> slots) {
74 + if (slots.isEmpty) {
75 + return false;
76 + }
77 + final unused = slots.where((final e) => e.txCount == 0);
78 + if (unused.isEmpty) {
79 + return false;
80 + }
81 + final head = unused.first;
82 + final tail = slots.last.dindex;
83 + if (slots.any((final e) => e.dindex > head.dindex && e.txCount > 0)) {
84 + return true;
85 + }
86 + return head.dindex + _transparentSyncLimit <= tail;
87 + }
88 +
89 + static Future<void> _scanTransparentChain(final int rotationAccountId) async {
90 + await ZcashWalletBase.runWithCoin(
91 + accountId: rotationAccountId,
92 + func: (final coin) async {
93 + final all = await zkool_account.fetchAddressTxCount(
94 + c: coin,
95 + aggregate: false,
96 + poolFilter: 1,
97 + );
98 + final external = all.where((final e) => e.scope == 0).toList();
99 + if (external.isEmpty) {
100 + return;
101 + }
102 + final maxD = external.map((final e) => e.dindex).reduce(max);
103 + final scanner = await zkool_sweep.TransparentScanner.newInstance();
104 + final height = await zkool_network.getCurrentHeight(c: coin);
105 + final completer = Completer<void>();
106 + final sub = scanner
107 + .run(endHeight: height, gapLimit: maxD + _lookahead + 1, c: coin)
108 + .listen(
109 + (_) {},
110 + onError: (final e) {
111 + printV("transparent chain scan: $e");
112 + if (!completer.isCompleted) {
113 + completer.complete();
114 + }
115 + },
116 + onDone: () {
117 + if (!completer.isCompleted) {
118 + completer.complete();
119 + }
120 + },
121 + );
122 + await Future.any([
123 + completer.future,
124 + Future.delayed(Duration(seconds: (maxD + 2).clamp(8, 45))),
125 + ]);
126 + await sub.cancel();
127 + },
128 + );
129 + }
130 +
131 + static Future<void> _resyncRotationPool(
132 + final int rotationAccountId,
133 + final int transparentLimit,
134 + ) async {
135 + await ZcashWalletBase.runWithCoin(
136 + accountId: rotationAccountId,
137 + func: (final coin) async {
138 + await zkool_sync.cancelSync();
139 + await zkool_account.resetSync(id: rotationAccountId, c: coin);
140 + final height = await zkool_network.getCurrentHeight(c: coin);
141 + final sync = zkool_sync.synchronize(
142 + accounts: [rotationAccountId],
143 + currentHeight: height,
144 + actionsPerSync: 10000,
145 + transparentLimit: transparentLimit,
146 + checkpointAge: 200,
147 + c: coin,
148 + fast: false,
149 + );
150 + final completer = Completer<void>();
151 + sync.listen(
152 + (_) {},
153 + onError: (final e) {
154 + printV("rotation pool resync error: $e");
155 + if (!completer.isCompleted) {
156 + completer.complete();
157 + }
158 + },
159 + onDone: () {
160 + if (!completer.isCompleted) {
161 + completer.complete();
162 + }
163 + },
164 + );
165 + await completer.future;
166 + },
167 + );
168 + }
169 +
170 + static Future<void> _ensurePoolScanned(final int rotationAccountId) async {
171 + var slots = await _transparentSlots(rotationAccountId);
172 + final firstSession = _resyncedRotationAccounts.add(rotationAccountId);
173 + if (!firstSession && !_poolNeedsRescan(slots)) {
174 return;
175 }
62 - _isStarted = true;
63 - final pfw = await pathForWalletTypeDir(type: WalletType.zcash);
64 - try {
65 - final f = File(p.join(pfw, "zec-taddr.json"));
66 - if (!f.existsSync()) {
67 - f.writeAsBytesSync(serialize());
176 + if (!firstSession) {
177 + final head = slots.where((final e) => e.txCount == 0).firstOrNull?.dindex;
178 + final key = '$rotationAccountId:$head';
179 + if (_poolRescanAttempted.contains(key)) {
180 + return;
181 }
69 - deserialize(f.readAsBytesSync());
70 - } catch (e) {
71 - printV("Failed to deserialize T address DB for zcash, it doesn't really matter: $e");
182 + _poolRescanAttempted.add(key);
183 }
184 + await _ensureLookahead(rotationAccountId);
185 + await _scanTransparentChain(rotationAccountId);
186 + slots = await _transparentSlots(rotationAccountId);
187 + final limit = slots.isEmpty ? _transparentSyncLimit : slots.last.dindex + 1;
188 + printV("rotation pool resync account $rotationAccountId limit $limit");
189 + await _resyncRotationPool(rotationAccountId, limit);
190 + }
191
192 + static Future<void> init() async {
193 + if (_isStarted) {
194 + return;
195 + }
196 + _isStarted = true;
197 unawaited(
198 (() async {
76 - await Future.delayed(Duration(seconds: 5));
199 + await Future.delayed(const Duration(seconds: 2));
200 return _jobRunner();
201 })(),
202 );
203 }
204
82 - static Future<void> serializeToFile() async {
83 - final pfw = await pathForWalletTypeDir(type: WalletType.zcash);
84 - final f = File(p.join(pfw, "zec-taddr.json"));
85 - f.writeAsBytesSync(serialize());
86 - }
205 + static Future<int?> getRotationAccountForMainAccount(final int mainAccountId) async {
206 + final cached = _rotationAccountByMain[mainAccountId];
207 + if (cached != null) {
208 + return cached;
209 + }
210 + try {
211 + await ZcashWalletBase.runWithCoinMutex.acquire();
212 + final wSeed = await zkool_account.getAccountSeed(c: c, account: mainAccountId);
213 + if (wSeed == null) {
214 + printV("Not running Taddr rotation - seed not found");
215 + return null;
216 + }
217 + final seed = seedForOffset(wSeed);
218 + final accs = await zkool_account.listAccounts(c: c);
219 + for (int i = 0; i < accs.length; i++) {
220 + final accSeed = await zkool_account.getAccountSeed(c: c, account: accs[i].id);
221 + if (accSeed?.mnemonic == seed.mnemonic &&
222 + accSeed?.phrase == seed.phrase &&
223 + accSeed?.aindex == seed.aindex) {
224 + _rotationAccountByMain[mainAccountId] = accs[i].id;
225 + return accs[i].id;
226 + }
227 + }
228
88 - static Uint8List serialize() {
89 - final data = {
90 - "rotationAccounts": rotationAccounts.map(
91 - (final k, final v) => MapEntry(
92 - k.toRadixString(16),
93 - v.map((final a) => flatBuffersPack(a.unpack().pack)).toList(),
94 - ),
95 - ),
96 - if (kDebugMode)
97 - "_rotationAccounts": rotationAccounts.map(
98 - (final k, final v) =>
99 - MapEntry(k.toRadixString(16), v.map((final a) => a.toString()).toList()),
100 - ),
101 - "rotationAccountsUsable": rotationAccountsUsable.map(
102 - (final k, final v) => MapEntry(
103 - k.toRadixString(16),
104 - v.map((final a) => flatBuffersPack(a.unpack().pack)).toList(),
105 - ),
106 - ),
107 - if (kDebugMode)
108 - "_rotationAccountsUsable": rotationAccountsUsable.map(
109 - (final k, final v) =>
110 - MapEntry(k.toRadixString(16), v.map((final a) => a.toString()).toList()),
111 - ),
112 - "shieldedAccountsTx": shieldedAccountsTx.map(
113 - (final k, final v) => MapEntry(
114 - k.toRadixString(16),
115 - v.map((final a) => flatBuffersPack(a.unpack().pack)).toList(),
116 - ),
117 - ),
118 - if (kDebugMode)
119 - "_shieldedAccountsTx": shieldedAccountsTx.map(
120 - (final k, final v) =>
121 - MapEntry(k.toRadixString(16), v.map((final a) => a.toString()).toList()),
122 - ),
123 - };
229 + zkool_account.Account? acc;
230 + for (final a in accs) {
231 + if (a.id == mainAccountId) {
232 + acc = a;
233 + break;
234 + }
235 + }
236 + if (acc == null) {
237 + printV("Main account $mainAccountId not found for Taddr rotation");
238 + return null;
239 + }
240
125 - if (kDebugMode) {
126 - return utf8.encode(JsonEncoder.withIndent(" ").convert(data));
241 + final name = CRC32.compute(wSeed.mnemonic).toString();
242 + final id = await newAccount(
243 + name: name,
244 + seed: seed.mnemonic,
245 + passphrase: seed.phrase,
246 + height: acc.birth,
247 + aindex: seed.aindex,
248 + );
249 + _rotationAccountByMain[mainAccountId] = id;
250 + return id;
251 + } finally {
252 + ZcashWalletBase.runWithCoinMutex.release();
253 }
128 - return utf8.encode(jsonEncode(data));
254 }
255
131 - static String flatBuffersPack(final int Function(fb.Builder fbBuilder) pack) {
132 - final fbBuilder = fb.Builder();
133 - final offset = pack(fbBuilder);
134 - fbBuilder.finish(offset);
135 - return base64.encode(fbBuilder.buffer);
256 + // External (scope 0) transparent slots for the rotation account, ordered by
257 + // dindex. zkool derives every slot in 0..=dindex, so unused ones (txCount==0)
258 + // form the disposable look-ahead pool.
259 + static Future<List<zkool_account.TAddressTxCount>> _transparentSlots(
260 + final int rotationAccountId,
261 + ) async {
262 + final all = await ZcashWalletBase.runWithCoin(
263 + accountId: rotationAccountId,
264 + func: (final coin) =>
265 + zkool_account.fetchAddressTxCount(c: coin, aggregate: false, poolFilter: 1),
266 + );
267 + return all.where((final e) => e.scope == 0).toList()
268 + ..sort((final a, final b) => a.dindex.compareTo(b.dindex));
269 }
270
138 - static void deserialize(final Uint8List bytes) {
139 - // todo: replace keys with sums and reverse in serialize
140 - // final List<String> seeds = [];
141 - // for (int i = 0; i < accounts.length; i++) {
142 - // final acc = accounts[i];
143 - // final backup = WarpApi.getBackup(coin, acc.id);
144 - // await WarpApi.transparentSync(coin, acc.id, syncHeight);
145 - // seeds.add(backup.seed!);
146 - // }
147 - try {
148 - final Map<dynamic, dynamic> data = jsonDecode(utf8.decode(bytes));
149 - rotationAccounts = (data["rotationAccounts"] as Map<String, dynamic>).map(
150 - (final k, final v) => MapEntry(
151 - int.parse(k, radix: 16),
152 - (v as List).map((final a) => Account(atob(a))).toList(),
153 - ),
271 + // Keep _lookahead unused disposable addresses ready, generating more whenever
272 + // the pool runs low. Newly generated addresses are scanned by ongoing sync
273 + // (the latest addresses are always in the scan window), so no resync needed.
274 + static Future<void> _ensureLookahead(final int rotationAccountId) async {
275 + final slots = await _transparentSlots(rotationAccountId);
276 + final unused = slots.where((final e) => e.txCount == 0).length;
277 + final toGenerate = _lookahead - unused;
278 + for (var i = 0; i < toGenerate; i++) {
279 + await ZcashWalletBase.runWithCoin(
280 + accountId: rotationAccountId,
281 + func: (final coin) => zkool_account.generateNextDindex(c: coin),
282 );
155 - rotationAccountsUsable = (data["rotationAccountsUsable"] as Map<String, dynamic>).map(
156 - (final k, final v) => MapEntry(
157 - int.parse(k, radix: 16),
158 - (v as List).map((final a) => Account(atob(a))).toList(),
159 - ),
160 - );
161 - shieldedAccountsTx = (data["shieldedAccountsTx"] as Map<String, dynamic>).map(
162 - (final k, final v) => MapEntry(
163 - int.parse(k, radix: 16),
164 - (v as List).map((final a) => ShieldedTx(atob(a))).toList(),
165 - ),
166 - );
167 - } catch (e) {
168 - rethrow;
283 + }
284 + if (toGenerate > 0) {
285 + printV("topped up disposable taddr pool by $toGenerate (had $unused)");
286 }
287 }
288
172 - static Uint8List atob(final String value) =>
173 - Uint8List.fromList(List<int>.from(base64.decode(value)));
289 + static bool _isWalletSynced(final int mainAccountId) {
290 + final wallet = ZcashWalletBase.walletsByAccountId[mainAccountId];
291 + return wallet != null && wallet.syncStatus is SyncedSyncStatus;
292 + }
293 +
294 + static Future<BigInt> _rotationTransparentBalance(final int rotationAccount) async {
295 + return ZcashWalletBase.runWithCoin(
296 + accountId: rotationAccount,
297 + func: (final coin) async {
298 + final b = await zkool_sync.balance(c: coin);
299 + return b.field0.elementAt(0);
300 + },
301 + );
302 + }
303 +
304 + static Future<List<ZkoolTx>> _loadTxHistory(final int accountId) async {
305 + return ZcashWalletBase.runWithCoin(
306 + accountId: accountId,
307 + func: (final coin) async {
308 + final txsI = await zkool_account.listTxHistory(c: coin);
309 + final txs = <ZkoolTx>[];
310 + for (int i = 0; i < txsI.length; i++) {
311 + final tx = txsI[i];
312 + try {
313 + final details = await zkool_account.getTxDetails(idTx: tx.id, c: coin);
314 + txs.add(ZkoolTx(tx, details));
315 + } catch (e) {
316 + printV("getTxDetails skipped for tx ${tx.id}: $e");
317 + }
318 + }
319 + return txs;
320 + },
321 + );
322 + }
323 +
324 + static bool _canBroadcastSweep(final int mainAccountId) {
325 + final lastSweep = _lastSweepBroadcastAt[mainAccountId];
326 + if (lastSweep == null) {
327 + return true;
328 + }
329 + return !lastSweep.isAfter(DateTime.now().subtract(const Duration(seconds: 120)));
330 + }
331
332 static Future<void> createAndSweepTAddresses() async {
176 - int chainHeight = 0;
333 + if (_sweepJobRunning) {
334 + return;
335 + }
336 + _sweepJobRunning = true;
337 try {
178 - chainHeight = await WarpApi.getLatestHeight(coin);
179 - } catch (e) {
180 - printV("Error getting latest height: $e");
181 - }
182 - final dbHeight = WarpApi.getDbHeight(coin);
183 - final height = dbHeight.unpack();
184 - final syncHeight = height.height;
185 - if (chainHeight != syncHeight) {
186 - printV(
187 - "Waiting for sync to finish: chainHeight(${chainHeight}) != syncHeight(${syncHeight})",
188 - );
338 + final mainAccountIds = ZcashWalletBase.walletsByAccountId.keys.toList();
339 + if (mainAccountIds.isEmpty) {
340 + await createAndSweepTAddressesForAccount(c.account);
341 + return;
342 + }
343 + for (final mainAccountId in mainAccountIds) {
344 + try {
345 + await createAndSweepTAddressesForAccount(mainAccountId);
346 + } catch (e, s) {
347 + printV("createAndSweepTAddresses for $mainAccountId error: $e");
348 + s.toString().split("\n").forEach(printV);
349 + }
350 + }
351 + } finally {
352 + _sweepJobRunning = false;
353 + }
354 + }
355 +
356 + static Future<void> createAndSweepTAddressesForAccount(final int cId) async {
357 + if (!_isWalletSynced(cId)) {
358 + printV("rotation sweep waiting for wallet sync (account $cId)");
359 return;
360 }
191 - final Map<int, List<ShieldedTx>> newShieldedAccountsTx = {};
192 - final accounts = WarpApi.getAccountList(coin);
193 - if (accounts.isEmpty) return;
194 - final Map<int, String> seeds = {};
195 - for (int i = 0; i < accounts.length; i++) {
196 - final acc = accounts[i];
197 - final backup = WarpApi.getBackup(coin, acc.id);
198 - await WarpApi.transparentSync(coin, acc.id, syncHeight);
199 - if (backup.seed == null) continue;
200 - seeds[acc.id] = backup.seed!;
361 +
362 + printV("createAndSweepTAddresses for account $cId");
363 +
364 + final rotationAccount = await getRotationAccountForMainAccount(cId);
365 + if (rotationAccount == null) {
366 + printV("rotation account is null, bailing out");
367 + return;
368 }
202 - for (int i = 0; i < accounts.length; i++) {
203 - final seed = seeds[accounts[i].id]!;
204 - if ([12, 13, 24, 25].contains(seed.split(" ").length)) {
205 - if (seed.split(" ").last.contains(":tgen:")) continue;
206 - rotationAccounts[accounts[i].id] = [];
207 - }
369 +
370 + await _ensureLookahead(rotationAccount);
371 + await _ensurePoolScanned(rotationAccount);
372 + await _refreshWalletAddresses(cId);
373 +
374 + BigInt transparentBal = BigInt.zero;
375 + try {
376 + transparentBal = await _rotationTransparentBalance(rotationAccount);
377 + printV("rotation account $rotationAccount transparent balance: $transparentBal");
378 + } catch (e) {
379 + printV("getTBalance: $e");
380 + return;
381 }
209 - final raKeys = rotationAccounts.keys.toList();
210 - seeds.removeWhere((_, final val) => raKeys.contains(val));
211 - printV("raKeys: ${raKeys.length}");
212 - for (int i = 0; i < raKeys.length; i++) {
213 - final accs = accounts.where((final a) {
214 - final seed = WarpApi.getBackup(coin, a.id).seed;
215 - return isSeedForWallet(seeds[raKeys[i]], seed);
216 - }).toList();
217 - rotationAccounts[raKeys[i]]!.addAll(accs);
218 - final acc = accountForSeed(seeds[raKeys[i]]!)!;
219 - final txs = WarpApi.getTxsSync(coin, acc.id);
220 - newShieldedAccountsTx[acc.id] ??= [];
221 - newShieldedAccountsTx[acc.id]!.addAll(txs);
222 - }
223 - rotationAccountsUsable = rotationAccounts.map((final k, final v) => MapEntry(k, v.toList()));
224 - printV("rotationAccounts: ${rotationAccounts.length}");
225 -
226 - for (int i = 0; i < raKeys.length; i++) {
227 - final acc = accountForSeed(seeds[raKeys[i]]!)!;
228 - newShieldedAccountsTx[acc.id] = [];
229 - for (int j = 0; j < (rotationAccounts[raKeys[i]]?.length ?? 0); j++) {
230 - final txs = WarpApi.getTxsSync(coin, rotationAccounts[raKeys[i]]![j].id);
231 - rotationAccounts[raKeys[i]]![j];
232 - newShieldedAccountsTx[acc.id]!.addAll(txs);
233 - }
234 - rotationAccountsUsable[raKeys[i]]!.removeWhere((final a) {
235 - final txs = WarpApi.getTxsSync(coin, a.id);
236 - return txs.isNotEmpty;
237 - });
238 -
239 - // remove hidden addresses
240 - final wis = await WalletInfo.selectList('type = ?', [WalletType.zcash.index]);
241 -
242 - final List<String> hiddenAddresses_ = [];
243 - for (int k = 0; k < wis.length; k++) {
244 - final addrs = await wis[k].getHiddenAddresses();
245 - final addr2 = await wis[k].getUsedAddresses();
246 - hiddenAddresses_.addAll(addrs);
247 - hiddenAddresses_.addAll(addr2);
248 - }
249 - final Set<String> hiddenAddresses = hiddenAddresses_.toSet();
250 - rotationAccountsUsable[raKeys[i]]!.removeWhere((final a) {
251 - return hiddenAddresses.contains(WarpApi.getTAddr(coin, a.id));
252 - });
253 - }
254 - printV("rotationAccountsUsable: ${rotationAccountsUsable.length}");
255 -
256 - bool didAddNewAccount = false;
257 - for (int i = 0; i < raKeys.length; i++) {
258 - if (rotationAccountsUsable[raKeys[i]]!.length < 5) {
259 - final seed = seedForOffset(seeds[raKeys[i]]!);
260 - final name = CRC32.compute(raKeys[i].toString()).toString();
261 - final id = await ZcashWalletService.runInDbMutex(
262 - () => WarpApi.newAccount(coin, name, seed, rotationAccounts[raKeys[i]]!.length),
263 - );
264 - printV("new id: $id / $seed");
265 - printV("${rotationAccounts[raKeys[i]]}");
266 - printV(raKeys[i]);
267 - rotationAccounts[raKeys[i]]!.forEach((final a) {
268 - final b = WarpApi.getBackup(coin, a.id);
269 - printV("${a.id}");
270 - });
271 - didAddNewAccount = true;
272 - }
382 +
383 + if (transparentBal < BigInt.from(_sweepThreshold)) {
384 + await updateCache(mainAccountId: cId);
385 + await _refreshWalletAfterRotationChange(cId);
386 + return;
387 }
274 - printV("didAddNewAccount: ${didAddNewAccount}");
275 - if (didAddNewAccount) {
276 - return createAndSweepTAddresses();
388 +
389 + if (!_canBroadcastSweep(cId)) {
390 + printV("rotation sweep rate-limited for account $cId");
391 + await updateCache(mainAccountId: cId);
392 + await _refreshWalletAfterRotationChange(cId);
393 + return;
394 }
395
279 - for (var i = 0; i < raKeys.length; i++) {
280 - final accs = rotationAccounts[raKeys[i]]!;
281 - inner:
282 - for (var j = 0; j < accs.length; j++) {
283 - int bal = 0;
284 - try {
285 - bal += await WarpApi.getTBalance(coin, accs[j].id);
286 - } catch (e) {
287 - printV("getTBalance: $e");
288 - return;
396 + final toAddress = await ZcashWalletBase.runWithCoin(
397 + accountId: cId,
398 + func: (final coin) async {
399 + final addrs = await zkool_account.getAddresses(c: coin, uaPools: 7);
400 + final orchard = addrs.oaddr;
401 + if (orchard == null || orchard.isEmpty) {
402 + throw Exception('Orchard address unavailable for rotation sweep');
403 }
290 - if (bal < 30000) continue inner;
291 - final to = accountForSeed(seeds[raKeys[i]]!)!;
292 - // final toAddress = WarpApi.getTAddr(coin, to.id);
293 - // final fromSeed = WarpApi.getBackup(coin, to.id);
294 - // final txId = await ZcashWalletService.runInDbMutex(
295 - // () => WarpApi.sweepTransparentSeed(
296 - // to.coin,
297 - // to.id,
298 - // chainHeight,
299 - // fromSeed.seed!,
300 - // 7,
301 - // toAddress,
302 - // 0,
303 - // 1,
304 - // FeeT(fee: 10000, minFee: 0, maxFee: 0, scheme: 0),
305 - // ),
306 - // );
307 - final recipientBuilder = RecipientObjectBuilder(
308 - address: WarpApi.getAddress(coin, to.id, 4),
309 - pools: 4,
310 - feeIncluded: true,
311 - amount: bal,
312 - );
313 -
314 - final recipient = Recipient(recipientBuilder.toBytes());
315 - final fee = FeeT(fee: 10000, minFee: 0, maxFee: 0, scheme: 0);
316 - await ZcashWalletBase.loadProver();
317 - final txPlan = await ZcashWalletService.runInDbMutex(
318 - () => WarpApi.prepareTx(
319 - coin,
320 - accs[j].id,
321 - [recipient],
322 - 3, // pools: (Transparent + Sapling)
323 - 1,
324 - 0, // anchorOffset
325 - fee,
404 + return orchard;
405 + },
406 + );
407 + final result = await ZcashWalletBase.runWithCoin(
408 + accountId: rotationAccount,
409 + func: (final coin) async {
410 + final tx = await zkool_pay.prepare(
411 + recipients: [
412 + zkool_paydart.Recipient(assetBase: zecBase, address: toAddress, amount: transparentBal),
413 + ],
414 + options: zkool_pay.PaymentOptions(
415 + srcPools: 1,
416 + recipientPaysFee: true,
417 + smartTransparent: false,
418 ),
419 + c: coin,
420 );
328 - final _txId = await ZcashWalletService.runInDbMutex(
329 - () => WarpApi.signAndBroadcast(ZcashWalletBase.coin, accs[j].id, txPlan),
330 - );
331 - await ZcashWalletService.addShieldedTx(_txId);
332 - printV("shielded: $_txId");
421 + final height = await zkool_network.getCurrentHeight(c: coin);
422 + final signTx = await zkool_pay.signTransaction(pczt: tx, c: coin);
423 + final txBytes = await zkool_pay.extractTransaction(package: signTx);
424 + return zkool_pay.broadcastTransaction(height: height, txBytes: txBytes, c: coin);
425 + },
426 + );
427 + printV("rotation sweep broadcast: $result");
428 + if (result.isEmpty) {
429 + throw Exception("rotation sweep broadcast failed");
430 + }
431
334 - await Future.delayed(Duration(seconds: 120)); // let it be
335 - return createAndSweepTAddresses();
336 - }
432 + await ZcashWalletService.addShieldedTx(result);
433 + _lastSweepBroadcastAt[cId] = DateTime.now();
434 + await updateCache(mainAccountId: cId);
435 + await _refreshWalletAfterRotationChange(cId);
436 + }
437 +
438 + static Future<void> _refreshWalletAfterRotationChange(final int mainAccountId) async {
439 + final wallet = ZcashWalletBase.walletsByAccountId[mainAccountId];
440 + if (wallet == null) {
441 + return;
442 }
338 - final nsatKeys = newShieldedAccountsTx.keys.toList();
339 - for (int i = 0; i < nsatKeys.length; i++) {
340 - inner:
341 - for (int j = 0; j < newShieldedAccountsTx[nsatKeys[i]]!.length; j++) {
342 - final tx = newShieldedAccountsTx[nsatKeys[i]]?[j];
343 - if (tx == null) {
344 - continue inner;
345 - }
346 - final _txId = tx.txId;
347 - if (_txId == null) {
348 - continue inner;
349 - }
350 - if (tx.value < 0) {
351 - await ZcashWalletService.addShieldedTx(_txId);
352 - // continue inner;
353 - }
354 - if (ZcashWalletService.autoshieldTx.contains(_txId)) {
355 - continue inner;
356 - }
443 + unawaited(wallet.updateTransactions());
444 + unawaited(wallet.updateBalance());
445 + }
446 +
447 + static Future<void> updateCache({required final int mainAccountId}) async {
448 + final rotationAccount = await getRotationAccountForMainAccount(mainAccountId);
449 + if (rotationAccount == null) {
450 + printV("rotationAccount is null");
451 + return;
452 + }
453 + await _ensureLookahead(rotationAccount);
454 + await _ensurePoolScanned(rotationAccount);
455 + final txs = await _loadTxHistory(rotationAccount);
456 + shieldedAccountsTx[mainAccountId] = txs;
457 + if (mainAccountId != rotationAccount && shieldedAccountsTx.containsKey(rotationAccount)) {
458 + shieldedAccountsTx.remove(rotationAccount);
459 + }
460 +
461 + for (final tx in shieldedAccountsTx[mainAccountId]!) {
462 + if (tx.direction == TransactionDirection.outgoing) {
463 + await ZcashWalletService.addShieldedTx(tx.txHash);
464 }
465 }
359 - shieldedAccountsTx = newShieldedAccountsTx.map((final k, final v) => MapEntry(k, v.toList()));
360 - await serializeToFile();
361 - return;
466 + await _refreshWalletAddresses(mainAccountId);
467 + await _refreshWalletAfterRotationChange(mainAccountId);
468 + }
469 +
470 + static Future<void> _refreshWalletAddresses(final int mainAccountId) async {
471 + final wallet = ZcashWalletBase.walletsByAccountId[mainAccountId];
472 + if (wallet == null) {
473 + return;
474 + }
475 + try {
476 + await wallet.walletAddresses.refreshRotationAddresses();
477 + } catch (e) {
478 + printV("rotation address refresh: $e");
479 + }
480 }
481
364 - static ShieldedTx reverseAmount(final ShieldedTx t) {
365 - final tt = t.unpack();
366 - tt.value = -tt.value;
367 - return ShieldedTx(base64.decode(flatBuffersPack(tt.pack)));
482 + static List<ZkoolTx> rotationTxsForMainAccount(final int mainAccountId) {
483 + return shieldedAccountsTx[mainAccountId] ?? const [];
484 }
485
370 - static Account? accountForSeed(final String seed) {
371 - final accounts = WarpApi.getAccountList(coin);
486 + static Future<zkool_account.Account?> accountForSeed(final zkool_account.Seed seed) async {
487 + final accounts = await zkool_account.listAccounts(c: c);
488 if (accounts.isEmpty) return null;
489 for (int i = 0; i < accounts.length; i++) {
490 final acc = accounts[i];
375 - final backup = WarpApi.getBackup(coin, acc.id);
376 - if (backup.seed == seed) {
491 + final backup = await zkool_account.getAccountSeed(c: c, account: acc.id);
492 + if (backup?.mnemonic == seed.mnemonic && backup?.phrase == seed.phrase) {
493 return acc;
494 }
495 }
@@ -383,63 +499,76 @@ class ZcashTaddressRotation {
499 static Future<void> _jobRunner() async {
500 for (;;) {
501 try {
386 - await Future.delayed(Duration(seconds: 5));
502 + await Future.delayed(const Duration(seconds: 5));
503 await createAndSweepTAddresses();
504 } catch (e, s) {
389 - printV(e);
505 + printV("createAndSweepTAddresses error: $e");
506 s.toString().split("\n").forEach(printV);
391 - rethrow;
507 } finally {
393 - await Future.delayed(Duration(seconds: 30));
508 + await Future.delayed(const Duration(seconds: 30));
509 }
510 }
511 }
512
398 - static String? addressForAccount(final int accountId, final Set<String>? hiddenAddresses) {
399 - final accs = rotationAccountsUsable[accountId] ?? [];
400 - for (int i = 0; i < accs.length; i++) {
401 - final addr = WarpApi.getTAddr(coin, accs[i].id);
402 - if (hiddenAddresses?.contains(addr) == true) {
403 - continue;
404 - }
405 - return addr;
406 - }
407 - return null;
408 - }
409 -
410 - static List<String>? allAddressesForAccount(final int accountId) {
411 - final acc = rotationAccounts[accountId];
412 - if (acc == null) {
413 - printV("Nothing found");
513 + // Current disposable address = first unused slot (lowest dindex).
514 + static Future<String?> addressForAccount(final int accountId) async {
515 + final rotationAccount = await getRotationAccountForMainAccount(accountId);
516 + if (rotationAccount == null) {
517 return null;
518 }
416 - for (int i = 0; i < acc.length; i++) {
417 - final b = WarpApi.getBackup(coin, acc[i].id);
418 - printV("$i. ${b.seed?.split(" ").last}, ${b.index}, ${WarpApi.getTAddr(coin, acc[i].id)}");
519 + final slots = await _transparentSlots(rotationAccount);
520 + final unused = slots.where((final e) => e.txCount == 0);
521 + if (unused.isNotEmpty) {
522 + return unused.first.address;
523 }
420 - return acc.map((final a) => WarpApi.getTAddr(coin, a.id)).toList();
524 + return slots.isNotEmpty ? slots.last.address : null;
525 }
526
423 - static List<String>? allUsedAddressesForAccount(final int accountId) {
424 - final seed = WarpApi.getBackup(coin, accountId).seed;
425 - if (seed == null) return [];
426 - final acc = rotationAccounts[seed]?.toList();
427 - if (acc == null) {
428 - printV("Nothing found");
429 - return null;
527 + static Future<List<String>> usedAddressesForAccount(final int accountId) async {
528 + final rotationAccount = await getRotationAccountForMainAccount(accountId);
529 + if (rotationAccount == null) {
530 + return const [];
531 }
431 - acc.removeWhere((final a1) {
432 - for (int i = 0; i < (rotationAccountsUsable[seed]?.length ?? 0); i++) {
433 - if (rotationAccountsUsable[seed]?[i].id == a1.id) {
434 - return true;
435 - }
436 - }
437 - return false;
438 - });
439 - for (int i = 0; i < acc.length; i++) {
440 - final b = WarpApi.getBackup(coin, acc[i].id);
441 - printV("$i. ${b.seed?.split(" ").last}, ${b.index}, ${WarpApi.getTAddr(coin, acc[i].id)}");
532 + final slots = await _transparentSlots(rotationAccount);
533 + return [
534 + for (final entry in slots)
535 + if (entry.txCount > 0) entry.address,
536 + ];
537 + }
538 +
539 + // The full disposable pool: used slots plus the unused look-ahead window.
540 + static Future<List<String>> allAddressesForAccount(final int accountId) async {
541 + final rotationAccount = await getRotationAccountForMainAccount(accountId);
542 + if (rotationAccount == null) {
543 + return const [];
544 }
443 - return acc.map((final a) => WarpApi.getTAddr(coin, a.id)).toList();
545 + final slots = await _transparentSlots(rotationAccount);
546 + return slots.map((final e) => e.address).toList();
547 + }
548 +
549 + static Future<int> newAccount({
550 + required final String name,
551 + required final String seed,
552 + required final String passphrase,
553 + required final int height,
554 + required final int aindex,
555 + }) async {
556 + final id = await zkool_account.newAccount(
557 + na: zkool_account.NewAccount(
558 + name: name,
559 + restore: true,
560 + passphrase: passphrase,
561 + key: seed,
562 + aindex: aindex,
563 + birth: height,
564 + folder: '',
565 + pools: 1,
566 + useInternal: true,
567 + internal: false,
568 + ledger: false,
569 + ),
570 + c: c,
571 + );
572 + return id;
573 }
574 }
cw_zcash/lib/src/zcash_transaction_info.dart
+14 -50
@@ -1,15 +1,9 @@
1 -import 'dart:convert';
2 -import 'dart:io';
3 -
1 import 'package:cw_core/amount/money.dart';
5 -import 'package:cw_core/pathForWallet.dart';
2 import 'package:cw_core/transaction_direction.dart';
3 import 'package:cw_core/transaction_info.dart';
4 import 'package:cw_core/format_amount.dart';
9 -import 'package:cw_core/utils/print_verbose.dart';
10 -import 'package:cw_core/wallet_type.dart';
5 import 'package:cw_zcash/cw_zcash.dart';
12 -import 'package:path/path.dart' as p;
6 +import 'package:cw_zcash/src/zkooltx.dart';
7
8 class ZcashTransactionInfo extends TransactionInfo {
9 ZcashTransactionInfo({
@@ -23,6 +17,9 @@ class ZcashTransactionInfo extends TransactionInfo {
17 required final int confirmations,
18 required final String to,
19 final String? memo,
20 + final TxType? txType,
21 + final bool isRotationReceive = false,
22 + final bool isShieldAction = false,
23 }) {
24 this.id = id;
25 this.amount = amount;
@@ -32,23 +29,21 @@ class ZcashTransactionInfo extends TransactionInfo {
29 this.date = date;
30 this.isPending = isPending;
31 this.confirmations = confirmations;
35 - this.to = getCachedDestinationAddress(id);
32 + this.to = to;
33 if (memo != null && memo.isNotEmpty) {
34 additionalInfo['memo'] = memo;
35 }
39 - additionalInfo['autoShield'] = false;
40 - // note: this won't work yet, fee is not in zcash_lib metadata,
41 - // leaving here so it can start working automagically in future
42 - if (amount == fee && to.startsWith("u")) {
43 - additionalInfo['autoShield'] = true;
36 + if (txType != null) {
37 + additionalInfo['txType'] = txType.name;
38 }
45 - // remove below when above starts working
46 - additionalInfo['autoShield'] = ZcashWalletService.autoshieldTx.contains(txHash);
47 - // --- == === cut here === == ---
48 - if (additionalInfo['autoShield'] == true) {
39 + additionalInfo['isRotationReceive'] = isRotationReceive;
40 + additionalInfo['isAutoShield'] = isShieldAction || ZcashWalletService.isAutoshieldTx(txHash);
41 +
42 + if (additionalInfo['isAutoShield'] == true) {
43 additionalInfo['memo'] ??= '';
50 - additionalInfo['memo'] += '\This is an auto-shielding transaction. Enjoy default privacy!';
51 - additionalInfo['memo'] = additionalInfo['memo'].trim();
44 + additionalInfo['memo'] =
45 + '${additionalInfo['memo']}\nThis is an auto-shielding transaction. Enjoy default privacy!.'
46 + .trim();
47 }
48 }
49
@@ -61,35 +56,4 @@ class ZcashTransactionInfo extends TransactionInfo {
56 void changeFiatAmount(final String amount) => _fiatAmount = formatAmount(amount);
57
58 String? get memo => additionalInfo['memo'] as String?;
64 -
65 - static final Map<String, String> _destinationAddressMap = {};
66 -
67 - static String? getCachedDestinationAddress(final String txId) {
68 - return _destinationAddressMap[txId] ??
69 - _destinationAddressMap['"$txId"'] ??
70 - _destinationAddressMap[txId.replaceAll('"', '')];
71 - }
72 -
73 - static Future<void> addCachedDestinationAddress(final String txId, final String address) async {
74 - _destinationAddressMap[txId] = address;
75 - final pfwt = await pathForWalletTypeDir(type: WalletType.zcash);
76 - final f = File(p.join(pfwt, "sent-tx-map.json"));
77 - f.writeAsStringSync(json.encode(_destinationAddressMap));
78 - }
79 -
80 - static Future<void> init() async {
81 - try {
82 - final pfwt = await pathForWalletTypeDir(type: WalletType.zcash);
83 - final f = File(p.join(pfwt, "sent-tx-map.json"));
84 - if (!f.existsSync()) {
85 - f.writeAsStringSync('{}');
86 - }
87 - final tmpMap = json.decode(f.readAsStringSync());
88 - tmpMap.forEach((final k, final v) {
89 - _destinationAddressMap[k.toString()] = v.toString();
90 - });
91 - } catch (e, s) {
92 - printV("failed to deserialize: $e, $s");
93 - }
94 - }
59 }
cw_zcash/lib/src/zcash_wallet.dart
+1067 -749
@@ -1,9 +1,10 @@
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';
@@ -12,26 +13,28 @@ import 'package:cw_core/sync_status.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
@@ -42,24 +45,51 @@ abstract class ZcashWalletBase
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) {
@@ -96,9 +126,8 @@ abstract class ZcashWalletBase
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;
@@ -115,14 +144,12 @@ abstract class ZcashWalletBase
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());
@@ -130,261 +157,844 @@ abstract class ZcashWalletBase
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;
@@ -417,56 +1027,31 @@ abstract class ZcashWalletBase
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}) {
@@ -476,328 +1061,129 @@ abstract class ZcashWalletBase
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) {
@@ -806,6 +1192,12 @@ abstract class ZcashWalletBase
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,
@@ -815,11 +1207,11 @@ abstract class ZcashWalletBase
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;
@@ -830,57 +1222,50 @@ abstract class ZcashWalletBase
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) {
@@ -892,29 +1277,20 @@ abstract class ZcashWalletBase
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
@@ -923,56 +1299,80 @@ abstract class ZcashWalletBase
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 }
@@ -981,202 +1381,120 @@ abstract class ZcashWalletBase
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 }
cw_zcash/lib/src/zcash_wallet_addresses.dart
+121 -88
@@ -6,7 +6,7 @@ import 'package:cw_core/wallet_info.dart';
6 import 'package:cw_zcash/cw_zcash.dart';
7 import 'package:cw_zcash/src/zcash_taddress_rotation.dart';
8 import 'package:mobx/mobx.dart';
9 -import 'package:warp_api/warp_api.dart';
9 +import 'package:zkool/src/rust/api/account.dart' as zkool_account;
10
11 part 'zcash_wallet_addresses.g.dart';
12
@@ -17,24 +17,34 @@ abstract class ZcashWalletAddressesBase extends WalletAddresses with Store {
17 addressPageType = ZcashReceivePageOption.typeFromString(walletInfo.addressPageType ?? "");
18 }
19
20 - final int accountId;
20 + int accountId;
21
22 static const int transparentType = 0;
23 static const int shieldedSaplingType = 1;
24 static const int shieldedOrchardType = 2;
25 static const int unifiedType = 3;
26
27 - String? _transparentAddress;
28 - String? _saplingAddress;
29 - String? _orchardAddress;
30 - String? _unifiedAddress;
27 + @observable
28 + String? transparentAddress;
29 +
30 + @observable
31 + String? _transparentObservableAddress;
32 +
33 + @observable
34 + String? saplingAddress;
35 +
36 + @observable
37 + String? orchardAddress;
38 +
39 + @observable
40 + String? unifiedAddress;
41
42 @override
43 @computed
44 String get latestAddress {
45 switch (addressPageType) {
46 case ZcashAddressType.transparent:
37 - return transparentAddress;
47 + return transparentAddress ?? "unknown transparentAddress";
48 case ZcashAddressType.transparentRotated:
49 // just to display something, after the wallet is synced the address
50 // will start rotating, and since we can't rotate address until the wallet syncs
@@ -46,16 +56,15 @@ abstract class ZcashWalletAddressesBase extends WalletAddresses with Store {
56 // 3) display first address from rotated pool which is also static until synced
57 // 2 seems like an obvious winner simply because it offers little to no benefit over 3
58 // and is noticably less complex in implementation
49 - return transparentAddressRotated ?? transparentAddress;
59 + return _transparentObservableAddress ??
60 + transparentAddress ??
61 + "unknown transparentAddressRotated";
62 case ZcashAddressType.shieldedSapling:
51 - return saplingAddress;
63 + return saplingAddress ?? "unknown saplingAddress";
64 case ZcashAddressType.shieldedOrchard:
53 - return orchardAddress;
65 + return orchardAddress ?? "unknown orchardAddress";
66 case ZcashAddressType.unifiedType:
55 - if (_unifiedAddress == null) {
56 - _unifiedAddress = WarpApi.getAddress(ZcashWalletBase.coin, accountId, 6);
57 - }
58 - return _unifiedAddress ?? "";
67 + return unifiedAddress ?? "unknown unifiedAddress";
68 }
69 }
70
@@ -79,63 +88,21 @@ abstract class ZcashWalletAddressesBase extends WalletAddresses with Store {
88 await walletInfo.save();
89 }
90
82 - String get transparentAddress {
83 - try {
84 - if (_transparentAddress == null) {
85 - _transparentAddress = WarpApi.getTAddr(ZcashWalletBase.coin, accountId);
86 - }
87 - return _transparentAddress ?? "";
88 - } catch (e) {
89 - return "";
90 - }
91 - }
92 -
93 - @computed
94 - String? get transparentAddressRotated {
95 - try {
96 - return ZcashTaddressRotation.addressForAccount(accountId, hiddenAddresses);
97 - } catch (e) {
98 - return null;
99 - }
100 - }
101 -
102 - String get saplingAddress {
103 - try {
104 - if (_saplingAddress == null) {
105 - _saplingAddress = WarpApi.getAddress(ZcashWalletBase.coin, accountId, 2);
106 - }
107 - return _saplingAddress ?? "";
108 - } catch (e) {
109 - return "";
110 - }
111 - }
112 -
113 - String get orchardAddress {
114 - try {
115 - if (_orchardAddress == null) {
116 - _orchardAddress = WarpApi.getAddress(ZcashWalletBase.coin, accountId, 4);
117 - }
118 - return _orchardAddress ?? "";
119 - } catch (e) {
120 - return "";
121 - }
122 - }
123 -
91 @override
92 @computed
126 - String get addressForExchange => transparentAddressRotated ?? transparentAddress;
93 + String get addressForExchange =>
94 + _transparentObservableAddress ?? transparentAddress ?? "unknown addressForExchange";
95
96 @override
97 @computed
130 - String get addressForBuy => transparentAddressRotated ?? transparentAddress;
98 + String get addressForBuy =>
99 + _transparentObservableAddress ?? transparentAddress ?? "unknown addressForBuy";
100
101 @override
102 bool containsAddress(final String address) {
103 return this.address == address || addressesMap.values.contains(address);
104 }
105
137 - static int get coin => ZcashWalletBase.coin;
138 -
106 @override
107 @observable
108 Set<String> hiddenAddresses = {};
@@ -149,37 +116,105 @@ abstract class ZcashWalletAddressesBase extends WalletAddresses with Store {
116 address = latestAddress;
117 }
118
119 + Future<void> _syncRotationHiddenAddresses() async {
120 + final all = (await ZcashTaddressRotation.allAddressesForAccount(accountId)).toSet();
121 + final used = (await ZcashTaddressRotation.usedAddressesForAccount(accountId)).toSet();
122 + hiddenAddresses.removeWhere(all.contains);
123 + hiddenAddresses.addAll(used);
124 + }
125 +
126 @override
127 Future<void> init() async {
128 + try {
129 + await _init();
130 + } catch (e) {
131 + printV("init failed, retrying in 2 seconds");
132 + printV(e);
133 + await Future.delayed(Duration(seconds: 2));
134 + await init();
135 + }
136 + }
137 +
138 + static bool isPlaceholderAddress(final String? address) =>
139 + address == null || address.isEmpty || address.startsWith('unknown ');
140 +
141 + String _resolvedWalletInfoAddress() {
142 + for (final candidate in [unifiedAddress, orchardAddress, saplingAddress, transparentAddress]) {
143 + if (!isPlaceholderAddress(candidate)) {
144 + return candidate!;
145 + }
146 + }
147 + return latestAddress;
148 + }
149 +
150 + Future<void> _init() async {
151 await _initAddresses();
152
156 - await ZcashTaddressRotation.init();
157 - int accountIndex = 0;
153 + final addr = await ZcashWalletBase.runWithCoin(
154 + accountId: accountId,
155 + func: (final coin) => zkool_account.getAddresses(c: coin, uaPools: 7),
156 + );
157 +
158 + transparentAddress = addr.taddr ?? 'unknown addr.taddr';
159 + saplingAddress = addr.saddr ?? 'unknown addr.saddr';
160 + orchardAddress = addr.oaddr ?? 'unknown addr.oaddr';
161 + unifiedAddress = addr.ua ?? 'unknown addr.ua';
162 + _transparentObservableAddress = await ZcashTaddressRotation.addressForAccount(accountId);
163 + final rotationAddrs = await ZcashTaddressRotation.allAddressesForAccount(accountId);
164 addressInfos = {
159 - 0:
160 - ZcashTaddressRotation.allAddressesForAccount(accountId)?.map((final v) {
161 - return WalletInfoAddressInfo(
162 - walletInfoId: walletInfo.internalId,
163 - mapKey: ++accountIndex,
164 - accountIndex: 0,
165 - address: v,
166 - label: "",
167 - );
168 - }).toList() ??
169 - [],
165 + 0: [
166 + for (int i = 0; i < rotationAddrs.length; i++)
167 + WalletInfoAddressInfo(
168 + walletInfoId: walletInfo.internalId,
169 + mapKey: i + 1,
170 + accountIndex: 0,
171 + address: rotationAddrs[i],
172 + label: "",
173 + ),
174 + ],
175 };
171 - hiddenAddresses.addAll(
172 - ZcashTaddressRotation.allUsedAddressesForAccount(accountId)?.toSet() ?? {},
173 - );
176 + await _syncRotationHiddenAddresses();
177 + await _applyAddressForCurrentType();
178 +
179 + if (isPlaceholderAddress(walletInfo.address)) {
180 + final resolved = _resolvedWalletInfoAddress();
181 + if (!isPlaceholderAddress(resolved)) {
182 + walletInfo.address = resolved;
183 + await walletInfo.save();
184 + }
185 + }
186 +
187 + await saveAddressesInBox();
188 + }
189
175 - // addressInfos[0]?.removeWhere((final test) => hiddenAddresses.contains(test.address));
190 + Future<void> refreshRotationAddresses() async {
191 + _transparentObservableAddress = await ZcashTaddressRotation.addressForAccount(accountId);
192 + final rotationAddrs = await ZcashTaddressRotation.allAddressesForAccount(accountId);
193 + addressInfos = {
194 + 0: [
195 + for (int i = 0; i < rotationAddrs.length; i++)
196 + WalletInfoAddressInfo(
197 + walletInfoId: walletInfo.internalId,
198 + mapKey: i + 1,
199 + accountIndex: 0,
200 + address: rotationAddrs[i],
201 + label: "",
202 + ),
203 + ],
204 + };
205 + await _syncRotationHiddenAddresses();
206 + await _applyAddressForCurrentType();
207 + }
208 +
209 + Future<void> _applyAddressForCurrentType() async {
210 if (_addressPageType == ZcashAddressType.transparentRotated) {
177 - final addr = ZcashTaddressRotation.addressForAccount(accountId, hiddenAddresses);
211 + final addr = await ZcashTaddressRotation.addressForAccount(accountId);
212 if (addr != null) {
213 address = addr;
214 }
215 + return;
216 }
182 - await saveAddressesInBox();
217 + address = latestAddress;
218 }
219
220 @observable
@@ -198,12 +233,17 @@ abstract class ZcashWalletAddressesBase extends WalletAddresses with Store {
233 @override
234 Future<void> saveAddressesInBox() async {
235 try {
236 + if (!isPlaceholderAddress(address)) {
237 + walletInfo.address = address;
238 + }
239 await walletInfo.setAddresses(addressesMap);
240 await walletInfo.setAddressInfos(addressInfos);
241 await walletInfo.setUsedAddresses(usedAddresses.toList());
242 await walletInfo.setHiddenAddresses(hiddenAddresses.toList());
243 await walletInfo.setManualAddresses(manualAddresses.toList());
244 + await walletInfo.save();
245 await _initAddresses();
246 + await _syncRotationHiddenAddresses();
247 } catch (e) {
248 printV("Error saving addresses: $e");
249 }
@@ -211,10 +251,7 @@ abstract class ZcashWalletAddressesBase extends WalletAddresses with Store {
251
252 @override
253 Future<void> updateAddressesInBox() async {
214 - _transparentAddress = null;
215 - _saplingAddress = null;
216 - _orchardAddress = null;
217 - _unifiedAddress = null;
254 + await _initAddresses();
255 }
256
257 @override
@@ -227,11 +264,7 @@ abstract class ZcashWalletAddressesBase extends WalletAddresses with Store {
264 if (addressPageType != ZcashAddressType.transparentRotated) {
265 return [];
266 }
230 - final List<WalletInfoAddressInfo> allInfos = [];
231 - for (final entry in addressInfos.entries) {
232 - allInfos.addAll(entry.value);
233 - }
234 - return allInfos;
267 + return addressInfos[0] ?? [];
268 }
269
270 @override
cw_zcash/lib/src/zcash_wallet_service.dart
+33 -20
@@ -9,6 +9,7 @@ import 'package:cw_core/wallet_info.dart';
9 import 'package:cw_core/wallet_service.dart';
10 import 'package:cw_core/wallet_type.dart';
11 import 'package:cw_zcash/cw_zcash.dart';
12 +import 'package:cw_zcash/src/migrate.dart';
13 import 'package:mutex/mutex.dart';
14 import 'package:path/path.dart' as p;
15
@@ -25,9 +26,9 @@ class ZcashWalletService
26 static Mutex dbMutex = Mutex();
27 static int dbMutexQueue = 0;
28 static Future<T> runInDbMutex<T>(final Future<T> Function() call) async {
29 + dbMutexQueue++;
30 + await dbMutex.acquire();
31 try {
29 - printV("dbMutexQueue: ${dbMutexQueue++}");
30 - await dbMutex.acquire();
32 return await call();
33 } finally {
34 dbMutex.release();
@@ -37,10 +38,15 @@ class ZcashWalletService
38
39 static Set<String> autoshieldTx = {};
40
41 + static String normalizeTxId(final String txId) =>
42 + txId.replaceAll(RegExp(r'[^a-fA-F0-9]'), '').toLowerCase();
43 +
44 + static bool isAutoshieldTx(final String txHash) => autoshieldTx.contains(normalizeTxId(txHash));
45 +
46 static Future<void> addShieldedTx(final String txId) async {
47 final pathForWalletType = await pathForWalletTypeDir(type: type);
42 - final shieldedListFile = p.join(pathForWalletType, "autoshield_list.txt");
43 - autoshieldTx.add(txId.replaceAll(RegExp(r'[^a-zA-Z0-9 ]'), ''));
48 + final shieldedListFile = p.join(pathForWalletType, "autoshield_list.v2.txt");
49 + autoshieldTx.add(normalizeTxId(txId));
50 final shieldedList = File(shieldedListFile);
51 shieldedList.writeAsStringSync(autoshieldTx.join("\n"));
52 }
@@ -48,12 +54,16 @@ class ZcashWalletService
54 static Future<void> loadShieldTxs() async {
55 try {
56 final pathForWalletType = await pathForWalletTypeDir(type: type);
51 - final shieldedListFile = p.join(pathForWalletType, "autoshield_list.txt");
57 + final shieldedListFile = p.join(pathForWalletType, "autoshield_list.v2.txt");
58 final shieldedList = File(shieldedListFile);
59 if (!shieldedList.existsSync()) {
60 return;
61 }
56 - autoshieldTx = shieldedList.readAsLinesSync().toSet();
62 + autoshieldTx = shieldedList
63 + .readAsLinesSync()
64 + .map(normalizeTxId)
65 + .where((final line) => line.isNotEmpty)
66 + .toSet();
67 } catch (e) {
68 printV("loadShieldTxs failed: $e");
69 }
@@ -74,12 +84,21 @@ class ZcashWalletService
84
85 @override
86 Future<bool> isWalletExit(final String name) async {
77 - final path = await pathForWallet(name: name, type: getType());
78 - return File(path).existsSync();
87 + final oldPath = (await pathForWallet(name: name, type: getType()));
88 + final path = (await pathForWallet(name: name, type: getType())) + ".v2";
89 + return File(path).existsSync() || File(oldPath).existsSync();
90 }
91
92 @override
93 Future<ZcashWallet> openWallet(final String name, final String password) async {
94 + await ZcashWalletBase.$init();
95 + if (await isWalletExit(name)) {
96 + final path = (await pathForWallet(name: name, type: getType())) + ".v2";
97 + if (!File(path).existsSync()) {
98 + await migrateOldSqliteToZkool2(walletName: name);
99 + }
100 + }
101 +
102 await loadShieldTxs();
103 final walletInfo = await WalletInfo.get(name, getType());
104 if (walletInfo == null) {
@@ -108,7 +127,7 @@ class ZcashWalletService
127
128 @override
129 Future<void> remove(final String wallet) async {
111 - final path = await pathForWalletDir(name: wallet, type: getType());
130 + final path = (await pathForWalletDir(name: wallet, type: getType()));
131 final file = Directory(path);
132 final isExist = file.existsSync();
133
@@ -129,17 +148,11 @@ class ZcashWalletService
148 if (currentWalletInfo == null) {
149 throw Exception('Wallet not found');
150 }
132 - final accountId = await ZcashWalletBase.getZcashAccountIdForName(currentName);
133 - if (accountId == null) {
134 - throw Exception('Wallet account not found for name: $currentName');
151 + if (!await isWalletExit(currentName)) {
152 + throw Exception('Wallet not found');
153 }
136 - final currentWallet = ZcashWallet(
137 - currentWalletInfo,
138 - await currentWalletInfo.getDerivationInfo(),
139 - accountId: accountId,
140 - );
154
142 - await currentWallet.renameWalletFiles(newName);
155 + await ZcashWalletBase.renameWalletFilesForName(fromName: currentName, toName: newName);
156
157 final newWalletInfo = currentWalletInfo;
158 newWalletInfo.id = WalletBase.idFor(newName, getType());
@@ -162,11 +175,11 @@ class ZcashWalletService
175 final bool? isTestnet,
176 }) async {
177 if (credentials.seed == null || credentials.seed!.isEmpty) {
165 - throw ZcashMnemonicIsIncorrectException();
178 + throw Exception("Seed is missing");
179 }
180
181 if (!bip39.validateMnemonic(credentials.seed!)) {
169 - throw ZcashMnemonicIsIncorrectException();
182 + throw Exception("Seed is not valid bip39");
183 }
184
185 return ZcashWalletBase.restore(credentials);
cw_zcash/lib/src/zkool_compat.dart new
+3
@@ -0,0 +1,3 @@
1 +import 'dart:typed_data';
2 +
3 +final zecBase = Uint8List(32);
cw_zcash/lib/src/zkooltx.dart new
+312
@@ -0,0 +1,312 @@
1 +import 'dart:convert';
2 +import 'dart:math';
3 +import 'dart:typed_data';
4 +
5 +import 'package:cw_core/get_height_by_date_zec.dart';
6 +import 'package:cw_core/transaction_direction.dart';
7 +import 'package:cw_zcash/src/util/hex.dart';
8 +import 'package:zkool/src/rust/api/account.dart' as zkool_account;
9 +
10 +enum TxType {
11 + unknown,
12 + selfTransfer, // 0
13 + receive, // 1
14 + sent, // 2
15 + unshield, // 4
16 + shield, // 8
17 + transparentSelfTransfer, // 12
18 +}
19 +
20 +enum NotePool { transparent, sapling, orchard, unknown }
21 +
22 +class ZkoolTx {
23 + ZkoolTx(final zkool_account.Tx tx, final zkool_account.TxAccount txAccount)
24 + : this._tx = tx,
25 + this._txAccount = txAccount;
26 + final zkool_account.Tx _tx;
27 + final zkool_account.TxAccount _txAccount;
28 +
29 + int get height => max(
30 + _tx.height,
31 + _txAccount.height,
32 + ); // sometimes returns 0 - higher number would be correct in this case
33 +
34 + DateTime get time {
35 + final ts = max(_tx.time, _txAccount.time) * 1000;
36 + if (ts == 0) {
37 + return ZcashHeight.getTimeByBlockHeight(height);
38 + }
39 + return DateTime.fromMillisecondsSinceEpoch(ts);
40 + }
41 +
42 + String? get to => _txAccount.outputs.firstOrNull?.address;
43 + Iterable<String> get outputAddresses =>
44 + _txAccount.outputs.map((final o) => o.address).whereType<String>();
45 + Iterable<({String address, int pool, BigInt value})> get outputsWithAddress => _txAccount.outputs
46 + .where((final o) => o.address.isNotEmpty)
47 + .map((final o) => (address: o.address, pool: o.pool, value: o.value));
48 + Iterable<int> get spendPools => _txAccount.spends.map((final s) => s.pool);
49 + Iterable<int> get notePools => _txAccount.notes.map((final n) => n.pool);
50 + String? get memo => _txAccount.memos.firstOrNull?.memo;
51 +
52 + BigInt get transparentOrSaplingSpent {
53 + if (_txAccount.spends.isEmpty) {
54 + return BigInt.zero;
55 + }
56 + return _txAccount.spends
57 + .where(
58 + (final s) => s.pool == NotePool.transparent.index || s.pool == NotePool.sapling.index,
59 + )
60 + .fold(BigInt.zero, (final a, final s) => a + s.value);
61 + }
62 +
63 + BigInt get orchardReceived {
64 + final fromNotes = _txAccount.notes
65 + .where((final n) => n.pool == NotePool.orchard.index)
66 + .fold(BigInt.zero, (final a, final n) => a + n.value);
67 + if (fromNotes > BigInt.zero) {
68 + return fromNotes;
69 + }
70 + return _txAccount.outputs
71 + .where((final o) => o.pool == NotePool.orchard.index)
72 + .fold(BigInt.zero, (final a, final o) => a + o.value);
73 + }
74 +
75 + String get txHash {
76 + final reversed = Uint8List.fromList(_txAccount.txid.reversed.toList());
77 + final txId = uint8ListToHex(reversed);
78 + return txId;
79 + }
80 +
81 + BigInt get value => _value.abs();
82 +
83 + BigInt get fee => value - _calcValue;
84 +
85 + BigInt get _value {
86 + if (_tx.value != 0) {
87 + return BigInt.from(_tx.value);
88 + }
89 +
90 + return _calcValue;
91 + }
92 +
93 + /////////// calc is short for calculator (calculated in this case)
94 + BigInt get _calcValue {
95 + final noteSum = _txAccount.notes.isEmpty
96 + ? BigInt.from(0)
97 + : _txAccount.notes.map((final note) => note.value).reduce((final a, final b) => a + b);
98 + final outputSum = _txAccount.outputs.isEmpty
99 + ? BigInt.from(0)
100 + : _txAccount.outputs
101 + .map((final output) => output.value)
102 + .reduce((final a, final b) => a + b);
103 + final spentSum = _txAccount.spends.isEmpty
104 + ? BigInt.from(0)
105 + : _txAccount.spends.map((final spent) => spent.value).reduce((final a, final b) => a + b);
106 +
107 + return noteSum - outputSum - spentSum;
108 + }
109 +
110 + TransactionDirection get direction {
111 + switch (type) {
112 + case TxType.selfTransfer:
113 + case TxType.receive:
114 + case TxType.unshield:
115 + case TxType.shield:
116 + case TxType.transparentSelfTransfer:
117 + return TransactionDirection.incoming;
118 + case TxType.sent:
119 + return TransactionDirection.outgoing;
120 + case TxType.unknown:
121 + return _value > BigInt.from(0)
122 + ? TransactionDirection.incoming
123 + : TransactionDirection.outgoing;
124 + }
125 + }
126 +
127 + TxType get type {
128 + switch (_tx.tpe) {
129 + case 0:
130 + return TxType.selfTransfer;
131 + case 1:
132 + return TxType.receive;
133 + case 2:
134 + return TxType.sent;
135 + case 4:
136 + return TxType.unshield;
137 + case 8:
138 + return TxType.shield;
139 + case 12:
140 + return TxType.transparentSelfTransfer;
141 + default:
142 + return TxType.unknown;
143 + }
144 + }
145 +
146 + Map<String, dynamic> toJson() => {
147 + "tx": {
148 + "id": _tx.id,
149 + "txid": base64.encode(_tx.txid),
150 + "height": _tx.height,
151 + "time": _tx.time,
152 + "value": value.toInt(),
153 + "tpe": _tx.tpe,
154 + "zsaValue": _tx.zsaValue,
155 + "assetDisplay": _tx.assetDisplay,
156 + },
157 + "txaccount": {
158 + "id": _txAccount.id,
159 + "account": _txAccount.account,
160 + "txid": base64.encode(_txAccount.txid),
161 + "height": _txAccount.height,
162 + "time": _txAccount.time,
163 + "notes": _txAccount.notes
164 + .map(
165 + (final note) => {
166 + "id": note.id,
167 + "pool": note.pool,
168 + "height": note.height,
169 + "tx": note.tx,
170 + "scope": note.scope,
171 + "value": note.value.toString(),
172 + "locked": note.locked,
173 + },
174 + )
175 + .toList(),
176 + "spends": _txAccount.spends
177 + .map(
178 + (final spend) => {
179 + "id": spend.id,
180 + "pool": spend.pool,
181 + "height": spend.height,
182 + "value": spend.value.toString(),
183 + "assetDisplay": spend.assetDisplay,
184 + },
185 + )
186 + .toList(),
187 + "outputs": _txAccount.outputs
188 + .map(
189 + (final output) => {
190 + "id": output.id,
191 + "pool": output.pool,
192 + "height": output.height,
193 + "value": output.value.toString(),
194 + "address": output.address,
195 + },
196 + )
197 + .toList(),
198 + "memos": _txAccount.memos
199 + .map(
200 + (final memo) => {
201 + "note": memo.note,
202 + "pool": memo.pool,
203 + "output": memo.output,
204 + "memo": memo.memo,
205 + },
206 + )
207 + .toList(),
208 + },
209 + };
210 +
211 + static int _asInt(final dynamic value, {final int fallback = 0}) {
212 + if (value == null) {
213 + return fallback;
214 + }
215 + if (value is int) {
216 + return value;
217 + }
218 + if (value is num) {
219 + return value.toInt();
220 + }
221 + return int.parse(value.toString());
222 + }
223 +
224 + static Uint8List _txidFromJson(final dynamic raw) {
225 + if (raw is Uint8List) {
226 + return raw;
227 + }
228 + if (raw is List) {
229 + return Uint8List.fromList(List<int>.from(raw));
230 + }
231 + if (raw is String) {
232 + return base64.decode(raw);
233 + }
234 + throw FormatException("Invalid txid in ZkoolTx json: $raw");
235 + }
236 +
237 + static List<T> _listFromJson<T>(final dynamic raw, final T Function(Map<String, dynamic>) parse) {
238 + if (raw is! List) {
239 + return const [];
240 + }
241 + return raw.map((final entry) => parse(Map<String, dynamic>.from(entry as Map))).toList();
242 + }
243 +
244 + static ZkoolTx fromJson(final Map<String, dynamic> json) {
245 + final txJson = Map<String, dynamic>.from((json["tx"] as Map?)?.cast<String, dynamic>() ?? json);
246 + final txAccountJson = Map<String, dynamic>.from(
247 + (json["txaccount"] as Map?)?.cast<String, dynamic>() ?? json,
248 + );
249 +
250 + return ZkoolTx(
251 + zkool_account.Tx(
252 + id: _asInt(txJson["id"]),
253 + txid: _txidFromJson(txJson["txid"]),
254 + height: _asInt(txJson["height"]),
255 + time: _asInt(txJson["time"]),
256 + value: _asInt(txJson["value"]),
257 + tpe: txJson["tpe"] == null ? null : _asInt(txJson["tpe"]),
258 + zsaValue: _asInt(txJson["zsaValue"]),
259 + assetDisplay: txJson["assetDisplay"] as String? ?? "",
260 + ),
261 + zkool_account.TxAccount(
262 + id: _asInt(txAccountJson["id"]),
263 + account: _asInt(txAccountJson["account"]),
264 + txid: _txidFromJson(txAccountJson["txid"]),
265 + height: _asInt(txAccountJson["height"]),
266 + time: _asInt(txAccountJson["time"]),
267 + notes: _listFromJson(
268 + txAccountJson["notes"],
269 + (final a) => zkool_account.TxNote(
270 + id: _asInt(a["id"]),
271 + pool: _asInt(a["pool"]),
272 + height: _asInt(a["height"]),
273 + tx: _asInt(a["tx"]),
274 + value: BigInt.parse(a["value"].toString()),
275 + scope: _asInt(a["scope"]),
276 + locked: a["locked"] as bool? ?? false,
277 + assetDisplay: a["assetDisplay"] as String? ?? "",
278 + ),
279 + ),
280 + spends: _listFromJson(
281 + txAccountJson["spends"],
282 + (final a) => zkool_account.TxSpend(
283 + id: _asInt(a["id"]),
284 + pool: _asInt(a["pool"]),
285 + height: _asInt(a["height"]),
286 + value: BigInt.parse(a["value"].toString()),
287 + assetDisplay: a["assetDisplay"] as String? ?? "",
288 + ),
289 + ),
290 + outputs: _listFromJson(
291 + txAccountJson["outputs"],
292 + (final a) => zkool_account.TxOutput(
293 + id: _asInt(a["id"]),
294 + pool: _asInt(a["pool"]),
295 + height: _asInt(a["height"]),
296 + value: BigInt.parse(a["value"].toString()),
297 + address: a["address"] as String? ?? "",
298 + ),
299 + ),
300 + memos: _listFromJson(
301 + txAccountJson["memos"],
302 + (final a) => zkool_account.TxMemo(
303 + note: a["note"] == null ? null : _asInt(a["note"]),
304 + pool: _asInt(a["pool"]),
305 + output: a["output"] == null ? null : _asInt(a["output"]),
306 + memo: a["memo"] as String?,
307 + ),
308 + ),
309 + ),
310 + );
311 + }
312 +}
cw_zcash/pubspec.yaml
+4 -4
@@ -9,8 +9,6 @@ environment:
9 dependencies:
10 flutter:
11 sdk: flutter
12 - warp_api:
13 - path: ../scripts/zcash_lib/packages/warp_api_ffi
12 cw_core:
13 path: ../cw_core
14 mobx: ^2.6.0
@@ -20,11 +18,13 @@ dependencies:
18 path: any
19 flat_buffers: any
20 zkool:
21 + # path: ../../zkool2
22 git:
24 - url: https://github.com/cake-tech/zkool2
25 - ref: b1642f260da4fa00dd2f8d4dd689073d0b56960a
23 + url: https://github.com/cake-tech/zkool2.git
24 + ref: dbd3dde8d8485428326a58e55660b7404666dd7c
25
26 path_provider: any
27 + sqflite_common_ffi: any
28 dependency_overrides:
29 decimal: ^2.3.3
30
ios/Podfile.lock
+71 -15
@@ -1,4 +1,14 @@
1 PODS:
2 + - AppAuth (1.7.6):
3 + - AppAuth/Core (= 1.7.6)
4 + - AppAuth/ExternalUserAgent (= 1.7.6)
5 + - AppAuth/Core (1.7.6)
6 + - AppAuth/ExternalUserAgent (1.7.6):
7 + - AppAuth/Core
8 + - AppCheckCore (11.2.0):
9 + - GoogleUtilities/Environment (~> 8.0)
10 + - GoogleUtilities/UserDefaults (~> 8.0)
11 + - PromisesObjC (~> 2.4)
12 - bitbox_flutter (0.0.1):
13 - Flutter
14 - breez_sdk_spark_flutter (0.14.0):
@@ -73,6 +83,34 @@ PODS:
83 - Flutter
84 - fluttertoast (0.0.2):
85 - Flutter
86 + - google_sign_in_ios (0.0.1):
87 + - AppAuth (>= 1.7.4)
88 + - Flutter
89 + - FlutterMacOS
90 + - GoogleSignIn (~> 8.0)
91 + - GTMSessionFetcher (>= 3.4.0)
92 + - GoogleSignIn (8.0.0):
93 + - AppAuth (< 2.0, >= 1.7.3)
94 + - AppCheckCore (~> 11.0)
95 + - GTMAppAuth (< 5.0, >= 4.1.1)
96 + - GTMSessionFetcher/Core (~> 3.3)
97 + - GoogleUtilities/Environment (8.1.0):
98 + - GoogleUtilities/Privacy
99 + - GoogleUtilities/Logger (8.1.0):
100 + - GoogleUtilities/Environment
101 + - GoogleUtilities/Privacy
102 + - GoogleUtilities/Privacy (8.1.0)
103 + - GoogleUtilities/UserDefaults (8.1.0):
104 + - GoogleUtilities/Logger
105 + - GoogleUtilities/Privacy
106 + - GTMAppAuth (4.1.1):
107 + - AppAuth/Core (~> 1.7)
108 + - GTMSessionFetcher/Core (< 4.0, >= 3.3)
109 + - GTMSessionFetcher (3.5.0):
110 + - GTMSessionFetcher/Full (= 3.5.0)
111 + - GTMSessionFetcher/Core (3.5.0)
112 + - GTMSessionFetcher/Full (3.5.0):
113 + - GTMSessionFetcher/Core
114 - image_picker_ios (0.0.1):
115 - Flutter
116 - in_app_review (2.0.0):
@@ -88,6 +126,7 @@ PODS:
126 - payjoin_flutter (0.23.0)
127 - permission_handler_apple (9.4.8):
128 - Flutter
129 + - PromisesObjC (2.4.0)
130 - quick_actions_ios (0.0.1):
131 - Flutter
132 - reown_yttrium (0.0.1):
@@ -174,6 +213,7 @@ DEPENDENCIES:
213 - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
214 - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
215 - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
216 + - google_sign_in_ios (from `.symlinks/plugins/google_sign_in_ios/darwin`)
217 - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
218 - in_app_review (from `.symlinks/plugins/in_app_review/ios`)
219 - integration_test (from `.symlinks/plugins/integration_test/ios`)
@@ -200,10 +240,17 @@ DEPENDENCIES:
240
241 SPEC REPOS:
242 https://github.com/CocoaPods/Specs.git:
243 + - AppAuth
244 + - AppCheckCore
245 - CryptoSwift
246 - DKImagePickerController
247 - DKPhotoGallery
248 + - GoogleSignIn
249 + - GoogleUtilities
250 + - GTMAppAuth
251 + - GTMSessionFetcher
252 - OrderedSet
253 + - PromisesObjC
254 - SDWebImage
255 - sqlite3
256 - SwiftyGif
@@ -248,6 +295,8 @@ EXTERNAL SOURCES:
295 :path: ".symlinks/plugins/flutter_secure_storage/ios"
296 fluttertoast:
297 :path: ".symlinks/plugins/fluttertoast/ios"
298 + google_sign_in_ios:
299 + :path: ".symlinks/plugins/google_sign_in_ios/darwin"
300 image_picker_ios:
301 :path: ".symlinks/plugins/image_picker_ios/ios"
302 in_app_review:
@@ -296,6 +345,8 @@ EXTERNAL SOURCES:
345 :path: ".symlinks/plugins/warp_api/ios"
346
347 SPEC CHECKSUMS:
348 + AppAuth: d4f13a8fe0baf391b2108511793e4b479691fb73
349 + AppCheckCore: cc8fd0a3a230ddd401f326489c99990b013f0c4f
350 bitbox_flutter: 9505732798041c413152669751beeaecc5fe400f
351 breez_sdk_spark_flutter: ed3b6709b8ce9b40309d8728640eade5b19e8a0d
352 connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
@@ -308,24 +359,30 @@ SPEC CHECKSUMS:
359 DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
360 DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
361 dnssec_proof: d461cac7bd3301eb7447f87936745a0c1ae0a67e
311 - fast_scanner: 44c00940355a51258cd6c2085734193cd23d95bc
312 - file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
313 - Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
314 - flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4
315 - flutter_local_authentication: 1172a4dd88f6306dadce067454e2c4caf07977bb
316 - flutter_local_notifications: ff50f8405aaa0ccdc7dcfb9022ca192e8ad9688f
317 - flutter_mailer: 2ef5a67087bc8c6c4cefd04a178bf1ae2c94cd83
318 - flutter_native_splash: df59bb2e1421aa0282cb2e95618af4dcb0c56c29
319 - flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be
320 - fluttertoast: 21eecd6935e7064cc1fcb733a4c5a428f3f24f0f
321 - image_picker_ios: 4f2f91b01abdb52842a8e277617df877e40f905b
322 - in_app_review: 436034b18594851a7328d7f1c2ed5ec235b79cfc
323 - integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573
324 - local_auth_darwin: 63c73d6d28cc3e239be2b6aa460ea6e317cd5100
362 + fast_scanner: 2cb1ad3e69e645e9980fb4961396ce5804caa3e3
363 + file_picker: 9b3292d7c8bc68c8a7bf8eb78f730e49c8efc517
364 + Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
365 + flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
366 + flutter_local_authentication: 989278c681612f1ee0e36019e149137f114b9d7f
367 + flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb
368 + flutter_mailer: 3a8cd4f36c960fb04528d5471097270c19fec1c4
369 + flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
370 + flutter_secure_storage: 2c2ff13db9e0a5647389bff88b0ecac56e3f3418
371 + fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1
372 + google_sign_in_ios: b48bb9af78576358a168361173155596c845f0b9
373 + GoogleSignIn: ce8c89bb9b37fb624b92e7514cc67335d1e277e4
374 + GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
375 + GTMAppAuth: f69bd07d68cd3b766125f7e072c45d7340dea0de
376 + GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
377 + image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
378 + in_app_review: 7dd1ea365263f834b8464673f9df72c80c17c937
379 + integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
380 + local_auth_darwin: d2e8c53ef0c4f43c646462e3415432c4dab3ae19
381 OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
382 package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
383 payjoin_flutter: d9d4c8aa16bd5dfedb9b21d0edc8199e0187d96e
384 permission_handler_apple: 92d754bbaa7361d436db2d6c3c1c2a0fdcec462e
385 + PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
386 quick_actions_ios: 4b07fb49d8d8f3518d7565fbb7a91014067a7d82
387 reown_yttrium: cee334ade64725b1d83f7b34c706a6aae2696d58
388 rive_common: dd421daaf9ae69f0125aa761dd96abd278399952
@@ -344,7 +401,6 @@ SPEC CHECKSUMS:
401 universal_ble: 45519b2aeafe62761e2c6309f8927edb5288b914
402 url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
403 wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
347 - warp_api: 997d8fb3e777669bc6260d0c12e56cd40b9db1d1
404 YttriumWrapper: 31e937fe9fbe0f1314d2ca6be9ce9b379a059966
405
406 PODFILE CHECKSUM: 5296465b1c6d14d506230356756826012f65d97a
ios/Runner.xcodeproj/project.pbxproj
-4
@@ -26,7 +26,6 @@
26 A1B4A70C9CFA13AB71662216 /* LnurlPay.swift in Resources */ = {isa = PBXBuildFile; fileRef = 7D3364C03978A8A74B6D586E /* LnurlPay.swift */; settings = {ASSET_TAGS = (BreezSDK, ); }; };
27 A3D5E17CC53DF13FA740DEFA /* RedeemSwap.swift in Resources */ = {isa = PBXBuildFile; fileRef = 9D2F2C9F2555316C95EE7EA3 /* RedeemSwap.swift */; settings = {ASSET_TAGS = (BreezSDK, ); }; };
28 B6C6E59403ACDE44724C12F4 /* ServiceConfig.swift in Resources */ = {isa = PBXBuildFile; fileRef = B3D5E78267F5F18D882FDC3B /* ServiceConfig.swift */; settings = {ASSET_TAGS = (BreezSDK, ); }; };
29 - CE3D5B792F142305004D767D /* WarpApiFFI.xcframework in CopyFiles */ = {isa = PBXBuildFile; fileRef = CE3D5B782F142305004D767D /* WarpApiFFI.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
29 CE918BF82D533ECE007F186E /* MoneroWallet.xcframework in CopyFiles */ = {isa = PBXBuildFile; fileRef = CE918BF72D533ECE007F186E /* MoneroWallet.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
30 CE918BFA2D533ED4007F186E /* WowneroWallet.xcframework in CopyFiles */ = {isa = PBXBuildFile; fileRef = CE918BF92D533ED4007F186E /* WowneroWallet.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
31 CE918BFC2D533ED8007F186E /* ZanoWallet.xcframework in CopyFiles */ = {isa = PBXBuildFile; fileRef = CE918BFB2D533ED8007F186E /* ZanoWallet.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
@@ -48,7 +47,6 @@
47 CE918BF82D533ECE007F186E /* MoneroWallet.xcframework in CopyFiles */,
48 CE918BFA2D533ED4007F186E /* WowneroWallet.xcframework in CopyFiles */,
49 CE918BFC2D533ED8007F186E /* ZanoWallet.xcframework in CopyFiles */,
51 - CE3D5B792F142305004D767D /* WarpApiFFI.xcframework in CopyFiles */,
50 );
51 runOnlyForDeploymentPostprocessing = 0;
52 };
@@ -89,7 +87,6 @@
87 B3D5E78267F5F18D882FDC3B /* ServiceConfig.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ServiceConfig.swift; path = "../.symlinks/plugins/breez_sdk/ios/bindings-swift/Sources/BreezSDK/ServiceConfig.swift"; sourceTree = "<group>"; };
88 B478EFCC3C0760ED97FFE561 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
89 C58D93382C00FAC6004BCF69 /* libresolv.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libresolv.tbd; path = usr/lib/libresolv.tbd; sourceTree = SDKROOT; };
92 - CE3D5B782F142305004D767D /* WarpApiFFI.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = WarpApiFFI.xcframework; path = ../scripts/zcash_lib/WarpApiFFI.xcframework; sourceTree = "<group>"; };
90 CE918BF72D533ECE007F186E /* MoneroWallet.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = MoneroWallet.xcframework; sourceTree = "<group>"; };
91 CE918BF92D533ED4007F186E /* WowneroWallet.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = WowneroWallet.xcframework; sourceTree = "<group>"; };
92 CE918BFB2D533ED8007F186E /* ZanoWallet.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = ZanoWallet.xcframework; sourceTree = "<group>"; };
@@ -170,7 +167,6 @@
167 97C146E51CF9000F007C117D = {
168 isa = PBXGroup;
169 children = (
173 - CE3D5B782F142305004D767D /* WarpApiFFI.xcframework */,
170 0C44A7182518EF4A00B570ED /* CakeWallet */,
171 9740EEB11CF90186004384FC /* Flutter */,
172 97C146F01CF9000F007C117D /* Runner */,
lib/core/background_sync.dart
+5 -1
@@ -210,7 +210,11 @@ class BackgroundSync {
210 }
211 await sharedPreferences.setString(PreferencesKey.backgroundSyncLastTrigger(wallet.name),
212 tx.date.add(Duration(minutes: 1)).toIso8601String());
213 - final action = tx.direction == TransactionDirection.incoming ? "Received" : "Sent";
213 + final action = tx.additionalInfo['isAutoShield'] == true
214 + ? "Autoshield"
215 + : tx.direction == TransactionDirection.incoming
216 + ? "Received"
217 + : "Sent";
218 if (sharedPreferences.getBool(PreferencesKey.backgroundSyncNotificationsEnabled) ?? false) {
219 await showNotification(
220 "$action ${wallet.currency.fullName} in ${wallet.name}", "${tx.amount.toString()}");
lib/entities/default_settings_migration.dart
+1 -1
@@ -56,7 +56,7 @@ const dogecoinDefaultNodeUri = 'dogecoin.stackwallet.com:50022';
56 const baseDefaultNodeUri = 'base-rpc.publicnode.com';
57 const arbitrumDefaultNodeUri = 'arbitrum.nownodes.io';
58 const bscDefaultNodeUri = 'bsc-dataseed.bnbchain.org';
59 -const zcashDefaultNodeUri = 'zec-node.cakewallet.com:443';
59 +const zcashDefaultNodeUri = 'zec.rocks:443';
60
61 Future<void> defaultSettingsMigration(
62 {required int version,
lib/new-ui/widgets/coins_page/assets_history/transaction_details_modal.dart
+27 -6
@@ -222,16 +222,37 @@ class _TransactionDetailsModalState extends State<TransactionDetailsModal> {
222
223 Widget _buildBottomWidget(TransactionDetailsListItem item) {
224 return switch (item.runtimeType) {
225 - AddressListItem => AddressFormatter.buildSegmentedAddress(
226 - address: item.value,
227 - evenTextStyle: TextStyle(
228 - fontSize: 12,
229 - fontFamily: "IBM Plex Mono",
230 - color: Theme.of(context).colorScheme.onSurface)),
225 + AddressListItem => _segmentedAddressList(item.value),
226 _ => Text(
227 item.value,
228 style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant),
229 )
230 };
231 }
232 +
233 + Widget _segmentedAddressList(String value) {
234 + final style = TextStyle(
235 + fontSize: 12,
236 + fontFamily: "IBM Plex Mono",
237 + color: Theme.of(context).colorScheme.onSurface,
238 + );
239 + final lines = value
240 + .split(RegExp(r'[,\n]'))
241 + .map((final line) => line.trim())
242 + .where((final line) => line.isNotEmpty)
243 + .toList();
244 + if (lines.length <= 1) {
245 + return AddressFormatter.buildSegmentedAddress(
246 + address: lines.isNotEmpty ? lines.first : value.trim(),
247 + evenTextStyle: style,
248 + );
249 + }
250 + return Column(
251 + crossAxisAlignment: CrossAxisAlignment.start,
252 + children: [
253 + for (final line in lines)
254 + AddressFormatter.buildSegmentedAddress(address: line, evenTextStyle: style),
255 + ],
256 + );
257 + }
258 }
lib/src/screens/dashboard/pages/balance/crypto_balance_widget.dart
-60
@@ -334,37 +334,6 @@ class CryptoBalanceWidget extends StatelessWidget {
334 ),
335 ),
336 ],
337 - if (dashboardViewModel.showZcashMissingFundsCard) ...[
338 - SizedBox(height: 10),
339 - Padding(
340 - padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
341 - child: InfoCard(
342 - title: S.of(context).zcash_card_missing_funds,
343 - description: S.of(context).zcash_card_description,
344 - leftButtonTitle: S.of(context).zcash_card_dismiss,
345 - rightButtonTitle: S.of(context).zcash_card_scan,
346 - image: 'assets/new-ui/crypto_full_icons/zcash.svg',
347 - leftButtonAction: () => _dismissZcash(context),
348 - rightButtonAction: () => _rescanInternalChangeZcash(context),
349 - hintWidget: GestureDetector(
350 - behavior: HitTestBehavior.opaque,
351 - onTap: () => launchUrl(
352 - Uri.parse(
353 - "https://docs.cakewallet.com/tutorials/zashi/#creating-a-new-zcash-wallet-on-cake"),
354 - mode: LaunchMode.externalApplication,
355 - ),
356 - child: Text(
357 - S.of(context).learn_more,
358 - style: Theme.of(context).textTheme.bodySmall?.copyWith(
359 - color: Theme.of(context).colorScheme.onSurfaceVariant,
360 - height: 1,
361 - ),
362 - softWrap: true,
363 - ),
364 - ),
365 - ),
366 - ),
367 - ],
337 if (dashboardViewModel.showDecredInfoCard) ...[
338 SizedBox(height: 10),
339 Padding(
@@ -494,21 +463,6 @@ class CryptoBalanceWidget extends StatelessWidget {
463 dashboardViewModel.setMwebEnabled();
464 }
465
497 - Future<void> _rescanInternalChangeZcash(BuildContext context) async {
498 - dashboardViewModel.rescanInternalChangeZcash();
499 - await showPopUp<void>(
500 - context: context,
501 - builder: (BuildContext context) => AlertWithOneAction(
502 - alertTitle: S.of(context).alert_notice,
503 - alertContent: S.of(context).zcash_card_warning,
504 - buttonText: S.of(context).understand,
505 - buttonAction: () {
506 - Navigator.of(context).pop();
507 - },
508 - ),
509 - );
510 - }
511 -
466 Future<void> _dismissMweb(BuildContext context) async {
467 await showPopUp<void>(
468 context: context,
@@ -522,18 +476,4 @@ class CryptoBalanceWidget extends StatelessWidget {
476 ));
477 dashboardViewModel.dismissMweb();
478 }
525 -
526 - Future<void> _dismissZcash(BuildContext context) async {
527 - await showPopUp<void>(
528 - context: context,
529 - builder: (BuildContext context) => AlertWithOneAction(
530 - alertTitle: S.of(context).alert_notice,
531 - alertContent: S.of(context).zcash_card_enable_later,
532 - buttonText: S.of(context).understand,
533 - buttonAction: () {
534 - Navigator.of(context).pop();
535 - },
536 - ));
537 - dashboardViewModel.dismissZcash();
538 - }
479 }
lib/src/screens/dashboard/pages/transactions_page.dart
+1 -1
@@ -111,7 +111,7 @@ class TransactionsPage extends StatelessWidget {
111 key: item.key,
112 onTap: () => Navigator.of(context)
113 .pushNamed(Routes.transactionDetails, arguments: transaction),
114 - isShield: transaction.additionalInfo['autoShield'] == true,
114 + isShield: transaction.additionalInfo['isAutoShield'] == true,
115 direction: transaction.direction,
116 formattedDate: DateFormat('HH:mm').format(transaction.date),
117 formattedAmount: item.formattedCryptoAmount,
lib/src/screens/settings/display_settings_page.dart
-9
@@ -115,21 +115,12 @@ class DisplaySettingsPage extends StatelessWidget {
115 onChanged: (val) {
116 _displaySettingsViewModel.setDisableTradeOption(val);
117 }),
118 - if (_displaySettingsViewModel.showZcashCardSetting)
119 - ListItemToggle(
120 - keyValue: "display_settings_show_zcashcard",
121 - label: S.of(context).show_zcash_card,
122 - value: _displaySettingsViewModel.showZcashCard,
123 - onChanged: (val) {
124 - _displaySettingsViewModel.setShowZcashCard(val);
125 - }),
118 ListItemSelector(
119 keyValue: "display_settings_sync_status_display",
120 label: S.of(context).sync_status_display_mode,
121 options: [_displaySettingsViewModel.syncStatusDisplayMode.title],
122 onTap: () async {
123 final items = SyncStatusDisplayMode.values.toList();
132 -
124 final selectedAtIndex =
125 items.indexOf(_displaySettingsViewModel.syncStatusDisplayMode);
126
lib/view_model/dashboard/transaction_list_item.dart
+7 -2
@@ -60,8 +60,13 @@ class TransactionListItem extends ActionListItem with Keyable {
60 return 'Transaction has missing data';
61 }
62
63 - if (transaction.additionalInfo['autoShield'] == true) {
64 - return "Autoshield";
63 + if (transaction.additionalInfo['isAutoShield'] == true) {
64 + if (transaction.isPending) {
65 + final status = formattedStatus;
66 + final baseString = S.current.shielding;
67 + return status.isNotEmpty ? "$baseString $status" : "$baseString...";
68 + }
69 + return S.current.shielding;
70 }
71 if (transaction.isPending) {
72 final status = formattedStatus;
lib/view_model/send/output.dart
+4 -2
@@ -21,6 +21,7 @@ import 'package:cw_core/balance.dart';
21 import 'package:cw_core/crypto_amount_format.dart';
22 import 'package:cw_core/crypto_currency.dart';
23 import 'package:cw_core/currency_for_wallet_type.dart';
24 +import 'package:cw_core/sync_status.dart';
25 import 'package:cw_core/transaction_history.dart';
26 import 'package:cw_core/transaction_info.dart';
27 import 'package:cw_core/utils/print_verbose.dart';
@@ -50,8 +51,9 @@ abstract class OutputBase with Store {
51 parsedAddress = ParsedAddress(parsedAddressByCurrencyMap: {}) {
52 autorun((_) {
53 final status = _wallet.syncStatus;
53 - printV("Sync status changed to $status. Recalculating fees");
54 -
54 + if (status is! SyncedSyncStatus) {
55 + return;
56 + }
57 calculateEstimatedFee();
58 });
59 }
lib/view_model/transaction_details_view_model.dart
+2 -2
@@ -391,8 +391,8 @@ abstract class TransactionDetailsViewModelBase with Store {
391 }
392
393 String get formattedTitle {
394 - if (transactionInfo.additionalInfo['autoShield'] == true) {
395 - return "Autoshield";
394 + if (transactionInfo.additionalInfo['isAutoShield'] == true) {
395 + return S.current.shielding;
396 }
397 if (transactionInfo.direction == TransactionDirection.incoming) {
398 return S.current.received;
lib/zcash/cw_zcash.dart
+2 -8
@@ -209,16 +209,10 @@ class CWZcash extends Zcash {
209 }
210
211 @override
212 - bool showMissingFundsCard(WalletBase wallet) {
213 - final zcashWallet = wallet as ZcashWallet;
214 - return zcashWallet.couldBeZashiWallet();
215 - }
212 + bool showMissingFundsCard(WalletBase wallet) => false;
213
214 @override
218 - Future<void> rescanInternalChange(WalletBase wallet) {
219 - final zcashWallet = wallet as ZcashWallet;
220 - return zcashWallet.rescanInternalChange();
221 - }
215 + Future<void> rescanInternalChange(WalletBase wallet) async {}
216 }
217
218 const wordList = [
pubspec_base.yaml
-2
@@ -268,8 +268,6 @@ flutter:
268 - assets/text/
269 - assets/faq/
270 - assets/animation/
271 - - scripts/zcash_lib/assets/sapling-spend.params
272 - - scripts/zcash_lib/assets/sapling-output.params
271 - assets/new-ui/
272 - assets/new-ui/icons/
273 - assets/new-ui/hero/
res/values/strings_en.arb
+2
@@ -1071,6 +1071,8 @@
1071 "share_this_node": "Share this node",
1072 "shared_seed_wallet_groups": "Shared Seed Wallet Groups",
1073 "shared_seed_wallets": "Shared Seed Wallets",
1074 + "share_this_node": "Share this node",
1075 + "shielding": "Autoshield",
1076 "show": "Show",
1077 "show_address_book_popup": "Show Address Book popup",
1078 "show_balance": "Long Press to Show Balance",