What changed, and why it matters
This is a large feature commit that replaces Cake Wallet's older Zcash wallet backend (warp_api) with a new library called zkool2. It touches address generation, transaction creation, balance scanning, transparent-address rotation, auto-shielding, and migration of old wallets. The commit message lists many bug fixes, but the code change is broad and partially complete (for example, the rescan method is mostly commented out). There is no direct evidence in the commit of a deliberate security vulnerability, but the size and incomplete state create a higher-than-normal risk of bugs that could lose funds, leak privacy, or produce incorrect balances.
Treat this as a high-risk refactor requiring focused review and QA before release. Specifically: verify zkool2 dependency version and supply-chain integrity; audit the new transaction commit/broadcast error path; complete and review the commented-out rescan logic; test transparent-address rotation, auto-shielding, and migration on mainnet-like data; and run integration tests for send/receive/rescan across wallet types. No immediate CVE or advisory action is warranted from the diff alone.
Security signals we found
Large backend replacement for a cryptocurrency wallet (high blast radius)
Transaction broadcast failure detection relies on txId length check (64 hex chars) rather than explicit success signal
Auto-shielding and t-address rotation logic rewritten with new concurrency and rate-limiting assumptions
Rescan implementation is partially stubbed/commented out
New dependency on external zkool2 Rust library whose code is not shown in the diff
Migration code reads old SQLite seed material and restores wallets with derived birth heights
Mempool parsing added with account-Id matching on notes/amounts
Evidence from the diff
The commit swaps the Zcash implementation from warp_api to zkool2 across cw_zcash. Key changes include: new migration path from old SQLite ywallet-style accounts to zkool2; rewritten sync loop using zkool_sync.synchronize; new transaction preparation/signing/broadcast flow using zkool_pay; transparent-address rotation rewritten around zkool account slots and sweep logic; mempool monitoring added; fee estimation now derived from the PCZT plan with a fallback; and a hardcoded checkpoint list for Zcash block-height/date conversion. Several areas are left in a transitional state: rescan() is reduced to a single rewind call with the full account-restore implementation commented out; error handling in commit() only checks txId length != 64 to decide failure; and the rotation sweep uses a 120-second rate limit and 30 000 zatoshi threshold without much explanation. The commit also removes the old sapling parameter workaround in CI.
Changed components
cw_zcash Zcash wallet backendZcash transaction creation and signingZcash transparent address rotation and auto-shieldingZcash synchronization and balance scanningZcash wallet migration from legacy ywallet formatZcash mempool monitoringLinux PR build workflow (removed sapling parameter workaround)Inspect captured patch +2814 / −1622
diff --git a/.github/workflows/pr_test_build_linux.yml b/.github/workflows/pr_test_build_linux.yml
index 879ea24..4677430 100644
--- a/.github/workflows/pr_test_build_linux.yml
+++ b/.github/workflows/pr_test_build_linux.yml
@@ -159,11 +159,6 @@ jobs:
- name: Generate localization
run: |
dart run tool/generate_localization.dart
- - name: Workaround zcash
- run: |
- mkdir -p scripts/zcash_lib/assets/
- touch scripts/zcash_lib/assets/sapling-spend.params
- touch scripts/zcash_lib/assets/sapling-output.params
- name: Build linux
run: |
flutter build linux --dart-define=hasDevOptions=true --release
diff --git a/.gitignore b/.gitignore
index 268167b..cd064fc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -226,4 +226,5 @@ scripts/android/yttrium
scripts/bitbox_flutter
scripts/reown_flutter
-*.salive
\ No newline at end of file
+*.salive
+android/.kotlin
diff --git a/assets/zcash_node_list.yml b/assets/zcash_node_list.yml
index afdd495..148e785 100644
--- a/assets/zcash_node_list.yml
+++ b/assets/zcash_node_list.yml
@@ -5,8 +5,8 @@
useSSL: true
isEnabledForAutoSwitching: true
label: "Cake Wallet"
--
- uri: zec.rocks:443
+- uri: zec.rocks:443
+ is_default: true
useSSL: true
isEnabledForAutoSwitching: true
label: "Zec.rocks"
diff --git a/cw_core/lib/get_height_by_date_zec.dart b/cw_core/lib/get_height_by_date_zec.dart
new file mode 100644
index 0000000..2176e8b
--- /dev/null
+++ b/cw_core/lib/get_height_by_date_zec.dart
@@ -0,0 +1,416 @@
+class ZcashHeight {
+ // 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
+ static final List<_Checkpoint> _points = [
+ _Checkpoint(1479114667, 10000),
+ _Checkpoint(1480618460, 20000),
+ _Checkpoint(1482129143, 30000),
+ _Checkpoint(1483635610, 40000),
+ _Checkpoint(1485145514, 50000),
+ _Checkpoint(1486655742, 60000),
+ _Checkpoint(1488163642, 70000),
+ _Checkpoint(1489668244, 80000),
+ _Checkpoint(1491175107, 90000),
+ _Checkpoint(1492681094, 100000),
+ _Checkpoint(1494185626, 110000),
+ _Checkpoint(1495691260, 120000),
+ _Checkpoint(1497196465, 130000),
+ _Checkpoint(1498702428, 140000),
+ _Checkpoint(1500209116, 150000),
+ _Checkpoint(1501716823, 160000),
+ _Checkpoint(1503222893, 170000),
+ _Checkpoint(1504730866, 180000),
+ _Checkpoint(1506236963, 190000),
+ _Checkpoint(1507744233, 200000),
+ _Checkpoint(1509254578, 210000),
+ _Checkpoint(1510768449, 220000),
+ _Checkpoint(1512275502, 230000),
+ _Checkpoint(1513783951, 240000),
+ _Checkpoint(1515292290, 250000),
+ _Checkpoint(1516799219, 260000),
+ _Checkpoint(1518306180, 270000),
+ _Checkpoint(1519813817, 280000),
+ _Checkpoint(1521319150, 290000),
+ _Checkpoint(1522828107, 300000),
+ _Checkpoint(1524336223, 310000),
+ _Checkpoint(1525844954, 320000),
+ _Checkpoint(1527354342, 330000),
+ _Checkpoint(1528860301, 340000),
+ _Checkpoint(1530365206, 350000),
+ _Checkpoint(1531873693, 360000),
+ _Checkpoint(1533380392, 370000),
+ _Checkpoint(1534888873, 380000),
+ _Checkpoint(1536393381, 390000),
+ _Checkpoint(1537899117, 400000),
+ _Checkpoint(1539406330, 410000),
+ _Checkpoint(1540912214, 420000),
+ _Checkpoint(1542422891, 430000),
+ _Checkpoint(1543931111, 440000),
+ _Checkpoint(1545437758, 450000),
+ _Checkpoint(1546945529, 460000),
+ _Checkpoint(1548450107, 470000),
+ _Checkpoint(1549957767, 480000),
+ _Checkpoint(1551466960, 490000),
+ _Checkpoint(1552969806, 500000),
+ _Checkpoint(1554477648, 510000),
+ _Checkpoint(1555981451, 520000),
+ _Checkpoint(1557489241, 530000),
+ _Checkpoint(1558997282, 540000),
+ _Checkpoint(1560503840, 550000),
+ _Checkpoint(1562011717, 560000),
+ _Checkpoint(1563518091, 570000),
+ _Checkpoint(1565027136, 580000),
+ _Checkpoint(1566532760, 590000),
+ _Checkpoint(1568039395, 600000),
+ _Checkpoint(1569547911, 610000),
+ _Checkpoint(1571051853, 620000),
+ _Checkpoint(1572559831, 630000),
+ _Checkpoint(1574069743, 640000),
+ _Checkpoint(1575576895, 650000),
+ _Checkpoint(1576603014, 660000),
+ _Checkpoint(1577356346, 670000),
+ _Checkpoint(1578109375, 680000),
+ _Checkpoint(1578863180, 690000),
+ _Checkpoint(1579616443, 700000),
+ _Checkpoint(1580369806, 710000),
+ _Checkpoint(1581122786, 720000),
+ _Checkpoint(1581876634, 730000),
+ _Checkpoint(1582630471, 740000),
+ _Checkpoint(1583383678, 750000),
+ _Checkpoint(1584133944, 760000),
+ _Checkpoint(1584887940, 770000),
+ _Checkpoint(1585641057, 780000),
+ _Checkpoint(1586395543, 790000),
+ _Checkpoint(1587148500, 800000),
+ _Checkpoint(1587901493, 810000),
+ _Checkpoint(1588655183, 820000),
+ _Checkpoint(1589408476, 830000),
+ _Checkpoint(1590163464, 840000),
+ _Checkpoint(1590916379, 850000),
+ _Checkpoint(1591669786, 860000),
+ _Checkpoint(1592423887, 870000),
+ _Checkpoint(1593176771, 880000),
+ _Checkpoint(1593930930, 890000),
+ _Checkpoint(1594684380, 900000),
+ _Checkpoint(1595439209, 910000),
+ _Checkpoint(1596192463, 920000),
+ _Checkpoint(1596946009, 930000),
+ _Checkpoint(1597700256, 940000),
+ _Checkpoint(1598453538, 950000),
+ _Checkpoint(1599207483, 960000),
+ _Checkpoint(1599960598, 970000),
+ _Checkpoint(1600714563, 980000),
+ _Checkpoint(1601467884, 990000),
+ _Checkpoint(1602220941, 1000000),
+ _Checkpoint(1602974722, 1010000),
+ _Checkpoint(1603728848, 1020000),
+ _Checkpoint(1604485637, 1030000),
+ _Checkpoint(1605238381, 1040000),
+ _Checkpoint(1605992807, 1050000),
+ _Checkpoint(1606747750, 1060000),
+ _Checkpoint(1607500606, 1070000),
+ _Checkpoint(1608255278, 1080000),
+ _Checkpoint(1609008684, 1090000),
+ _Checkpoint(1609763782, 1100000),
+ _Checkpoint(1610517871, 1110000),
+ _Checkpoint(1611271575, 1120000),
+ _Checkpoint(1612025546, 1130000),
+ _Checkpoint(1612779940, 1140000),
+ _Checkpoint(1613533993, 1150000),
+ _Checkpoint(1614287669, 1160000),
+ _Checkpoint(1615042297, 1170000),
+ _Checkpoint(1615792993, 1180000),
+ _Checkpoint(1616546145, 1190000),
+ _Checkpoint(1617299867, 1200000),
+ _Checkpoint(1618054038, 1210000),
+ _Checkpoint(1618808513, 1220000),
+ _Checkpoint(1619563243, 1230000),
+ _Checkpoint(1620318159, 1240000),
+ _Checkpoint(1621071298, 1250000),
+ _Checkpoint(1621826156, 1260000),
+ _Checkpoint(1622580237, 1270000),
+ _Checkpoint(1623335553, 1280000),
+ _Checkpoint(1624090141, 1290000),
+ _Checkpoint(1624844712, 1300000),
+ _Checkpoint(1625597720, 1310000),
+ _Checkpoint(1626351652, 1320000),
+ _Checkpoint(1627106054, 1330000),
+ _Checkpoint(1627860648, 1340000),
+ _Checkpoint(1628614015, 1350000),
+ _Checkpoint(1629368398, 1360000),
+ _Checkpoint(1630122679, 1370000),
+ _Checkpoint(1630877190, 1380000),
+ _Checkpoint(1631631884, 1390000),
+ _Checkpoint(1632385851, 1400000),
+ _Checkpoint(1633141006, 1410000),
+ _Checkpoint(1633895592, 1420000),
+ _Checkpoint(1634649550, 1430000),
+ _Checkpoint(1635403121, 1440000),
+ _Checkpoint(1636157669, 1450000),
+ _Checkpoint(1636915133, 1460000),
+ _Checkpoint(1637670036, 1470000),
+ _Checkpoint(1638424121, 1480000),
+ _Checkpoint(1639177413, 1490000),
+ _Checkpoint(1639931234, 1500000),
+ _Checkpoint(1640684476, 1510000),
+ _Checkpoint(1641438827, 1520000),
+ _Checkpoint(1642192402, 1530000),
+ _Checkpoint(1642946383, 1540000),
+ _Checkpoint(1643700901, 1550000),
+ _Checkpoint(1644455522, 1560000),
+ _Checkpoint(1645208504, 1570000),
+ _Checkpoint(1645962081, 1580000),
+ _Checkpoint(1646715150, 1590000),
+ _Checkpoint(1647465727, 1600000),
+ _Checkpoint(1648219045, 1610000),
+ _Checkpoint(1648973131, 1620000),
+ _Checkpoint(1649727422, 1630000),
+ _Checkpoint(1650481408, 1640000),
+ _Checkpoint(1651234287, 1650000),
+ _Checkpoint(1651989865, 1660000),
+ _Checkpoint(1652743327, 1670000),
+ _Checkpoint(1653497455, 1680000),
+ _Checkpoint(1654252094, 1690000),
+ _Checkpoint(1655005944, 1700000),
+ _Checkpoint(1655759708, 1710000),
+ _Checkpoint(1656513765, 1720000),
+ _Checkpoint(1657268358, 1730000),
+ _Checkpoint(1658022453, 1740000),
+ _Checkpoint(1658777019, 1750000),
+ _Checkpoint(1659531133, 1760000),
+ _Checkpoint(1660283907, 1770000),
+ _Checkpoint(1661037645, 1780000),
+ _Checkpoint(1661791977, 1790000),
+ _Checkpoint(1662544394, 1800000),
+ _Checkpoint(1663298018, 1810000),
+ _Checkpoint(1664051450, 1820000),
+ _Checkpoint(1664805547, 1830000),
+ _Checkpoint(1665558994, 1840000),
+ _Checkpoint(1666312456, 1850000),
+ _Checkpoint(1667066473, 1860000),
+ _Checkpoint(1667823393, 1870000),
+ _Checkpoint(1668576623, 1880000),
+ _Checkpoint(1669330657, 1890000),
+ _Checkpoint(1670084156, 1900000),
+ _Checkpoint(1670837831, 1910000),
+ _Checkpoint(1671591094, 1920000),
+ _Checkpoint(1672343975, 1930000),
+ _Checkpoint(1673097703, 1940000),
+ _Checkpoint(1673852016, 1950000),
+ _Checkpoint(1674605810, 1960000),
+ _Checkpoint(1675360050, 1970000),
+ _Checkpoint(1676113710, 1980000),
+ _Checkpoint(1676867174, 1990000),
+ _Checkpoint(1677620242, 2000000),
+ _Checkpoint(1678374852, 2010000),
+ _Checkpoint(1679125063, 2020000),
+ _Checkpoint(1679878121, 2030000),
+ _Checkpoint(1680632254, 2040000),
+ _Checkpoint(1681386518, 2050000),
+ _Checkpoint(1682140381, 2060000),
+ _Checkpoint(1682894564, 2070000),
+ _Checkpoint(1683648178, 2080000),
+ _Checkpoint(1684401932, 2090000),
+ _Checkpoint(1685156464, 2100000),
+ _Checkpoint(1685910040, 2110000),
+ _Checkpoint(1686663406, 2120000),
+ _Checkpoint(1687417875, 2130000),
+ _Checkpoint(1688172069, 2140000),
+ _Checkpoint(1688925610, 2150000),
+ _Checkpoint(1689679500, 2160000),
+ _Checkpoint(1690432691, 2170000),
+ _Checkpoint(1691186349, 2180000),
+ _Checkpoint(1691940901, 2190000),
+ _Checkpoint(1692694780, 2200000),
+ _Checkpoint(1693449743, 2210000),
+ _Checkpoint(1694203665, 2220000),
+ _Checkpoint(1694957643, 2230000),
+ _Checkpoint(1695710712, 2240000),
+ _Checkpoint(1696465787, 2250000),
+ _Checkpoint(1697218701, 2260000),
+ _Checkpoint(1697973031, 2270000),
+ _Checkpoint(1698726864, 2280000),
+ _Checkpoint(1699483348, 2290000),
+ _Checkpoint(1700237650, 2300000),
+ _Checkpoint(1700991210, 2310000),
+ _Checkpoint(1701745742, 2320000),
+ _Checkpoint(1702500723, 2330000),
+ _Checkpoint(1703253121, 2340000),
+ _Checkpoint(1704007238, 2350000),
+ _Checkpoint(1704760935, 2360000),
+ _Checkpoint(1705514669, 2370000),
+ _Checkpoint(1706268087, 2380000),
+ _Checkpoint(1707021423, 2390000),
+ _Checkpoint(1707776257, 2400000),
+ _Checkpoint(1708529542, 2410000),
+ _Checkpoint(1709283065, 2420000),
+ _Checkpoint(1710037165, 2430000),
+ _Checkpoint(1710787780, 2440000),
+ _Checkpoint(1711541488, 2450000),
+ _Checkpoint(1712294996, 2460000),
+ _Checkpoint(1713048843, 2470000),
+ _Checkpoint(1713803486, 2480000),
+ _Checkpoint(1714557127, 2490000),
+ _Checkpoint(1715311181, 2500000),
+ _Checkpoint(1716064271, 2510000),
+ _Checkpoint(1716818792, 2520000),
+ _Checkpoint(1717572765, 2530000),
+ _Checkpoint(1718326333, 2540000),
+ _Checkpoint(1719080275, 2550000),
+ _Checkpoint(1719834297, 2560000),
+ _Checkpoint(1720588042, 2570000),
+ _Checkpoint(1721341832, 2580000),
+ _Checkpoint(1722095122, 2590000),
+ _Checkpoint(1722848604, 2600000),
+ _Checkpoint(1723602357, 2610000),
+ _Checkpoint(1724355843, 2620000),
+ _Checkpoint(1725110085, 2630000),
+ _Checkpoint(1725863681, 2640000),
+ _Checkpoint(1726616892, 2650000),
+ _Checkpoint(1727371443, 2660000),
+ _Checkpoint(1728124893, 2670000),
+ _Checkpoint(1728878141, 2680000),
+ _Checkpoint(1729631572, 2690000),
+ _Checkpoint(1730385888, 2700000),
+ _Checkpoint(1731142437, 2710000),
+ _Checkpoint(1731896266, 2720000),
+ _Checkpoint(1732650193, 2730000),
+ _Checkpoint(1733403626, 2740000),
+ _Checkpoint(1734157390, 2750000),
+ _Checkpoint(1734911442, 2760000),
+ _Checkpoint(1735665720, 2770000),
+ _Checkpoint(1736419845, 2780000),
+ _Checkpoint(1737173511, 2790000),
+ _Checkpoint(1737926795, 2800000),
+ _Checkpoint(1738681511, 2810000),
+ _Checkpoint(1739434236, 2820000),
+ _Checkpoint(1740188214, 2830000),
+ _Checkpoint(1740941352, 2840000),
+ _Checkpoint(1741692338, 2850000),
+ _Checkpoint(1742445247, 2860000),
+ _Checkpoint(1743198927, 2870000),
+ _Checkpoint(1743952947, 2880000),
+ _Checkpoint(1744706370, 2890000),
+ _Checkpoint(1745461377, 2900000),
+ _Checkpoint(1746214735, 2910000),
+ _Checkpoint(1746967908, 2920000),
+ _Checkpoint(1747722271, 2930000),
+ _Checkpoint(1748474582, 2940000),
+ _Checkpoint(1749228582, 2950000),
+ _Checkpoint(1749983372, 2960000),
+ _Checkpoint(1750737278, 2970000),
+ _Checkpoint(1751490762, 2980000),
+ _Checkpoint(1752243931, 2990000),
+ _Checkpoint(1752997873, 3000000),
+ _Checkpoint(1753751756, 3010000),
+ _Checkpoint(1754504935, 3020000),
+ _Checkpoint(1755259449, 3030000),
+ _Checkpoint(1756012904, 3040000),
+ _Checkpoint(1756765335, 3050000),
+ _Checkpoint(1757519206, 3060000),
+ _Checkpoint(1758272484, 3070000),
+ _Checkpoint(1759025597, 3080000),
+ _Checkpoint(1759778957, 3090000),
+ _Checkpoint(1760532780, 3100000),
+ _Checkpoint(1761286210, 3110000),
+ _Checkpoint(1762039783, 3120000),
+ _Checkpoint(1762796627, 3130000),
+ _Checkpoint(1763550532, 3140000),
+ _Checkpoint(1764303860, 3150000),
+ _Checkpoint(1765057381, 3160000),
+ _Checkpoint(1765811222, 3170000),
+ _Checkpoint(1766564356, 3180000),
+ _Checkpoint(1767318326, 3190000),
+ _Checkpoint(1768071956, 3200000),
+ _Checkpoint(1768824957, 3210000),
+ _Checkpoint(1769578781, 3220000),
+ _Checkpoint(1770332206, 3230000),
+ _Checkpoint(1771086265, 3240000),
+ _Checkpoint(1771840180, 3250000),
+ _Checkpoint(1772592783, 3260000),
+ _Checkpoint(1773343947, 3270000),
+ _Checkpoint(1774097085, 3280000),
+ _Checkpoint(1774850161, 3290000),
+ _Checkpoint(1775603525, 3300000),
+ _Checkpoint(1776358117, 3310000),
+ ];
+
+ static int getBlockHeightByTime(DateTime time) {
+ final t = time.toUtc().millisecondsSinceEpoch;
+
+ if (_points.isEmpty) return 0;
+
+ if (t <= _points.first.timeMs) {
+ return 1;
+ }
+
+ for (int i = 0; i < _points.length - 1; i++) {
+ final p0 = _points[i];
+ final p1 = _points[i + 1];
+
+ if (t >= p0.timeMs && t < p1.timeMs) {
+ return _interpolate(
+ p0.height,
+ p1.height,
+ p0.timeMs,
+ p1.timeMs,
+ t,
+ );
+ }
+ }
+
+ final last = _points[_points.length - 1];
+ final prev = _points[_points.length - 2];
+
+ final avgBlockTimeMs = (last.timeMs - prev.timeMs) / (last.height - prev.height);
+
+ final blocksSinceLast = ((t - last.timeMs) / avgBlockTimeMs).floor();
+
+ return last.height + blocksSinceLast;
+ }
+
+ static DateTime getTimeByBlockHeight(int height) {
+ if (_points.isEmpty) return DateTime.fromMillisecondsSinceEpoch(0, isUtc: true);
+ if (height <= 1) {
+ return DateTime.fromMillisecondsSinceEpoch(_points.first.timeMs, isUtc: true);
+ }
+
+ for (int i = 0; i < _points.length - 1; i++) {
+ final p0 = _points[i];
+ final p1 = _points[i + 1];
+ if (height >= p0.height && height < p1.height) {
+ final t = _interpolate(
+ p0.timeMs,
+ p1.timeMs,
+ p0.height,
+ p1.height,
+ height,
+ );
+ return DateTime.fromMillisecondsSinceEpoch(t, isUtc: true);
+ }
+ }
+
+ final last = _points[_points.length - 1];
+ final prev = _points[_points.length - 2];
+ final avgBlockTimeMs = (last.timeMs - prev.timeMs) / (last.height - prev.height);
+ final msSinceLast = ((height - last.height) * avgBlockTimeMs).floor();
+ return DateTime.fromMillisecondsSinceEpoch(last.timeMs + msSinceLast, isUtc: true);
+ }
+
+ static int _interpolate(
+ int h0,
+ int h1,
+ int t0,
+ int t1,
+ int t,
+ ) {
+ final ratio = (t - t0) / (t1 - t0);
+ return h0 + ((h1 - h0) * ratio).floor();
+ }
+}
+
+class _Checkpoint {
+ final int timeMs;
+ final int height;
+
+ _Checkpoint(int timeSec, this.height) : timeMs = timeSec * 1000;
+}
diff --git a/cw_zcash/lib/cw_zcash.dart b/cw_zcash/lib/cw_zcash.dart
index c312ddb..92af285 100644
--- a/cw_zcash/lib/cw_zcash.dart
+++ b/cw_zcash/lib/cw_zcash.dart
@@ -4,7 +4,6 @@
library;
export 'src/zcash_balance.dart';
-export 'src/zcash_mnemonic_is_incorrect_exception.dart';
export 'src/zcash_transaction_info.dart';
export 'src/zcash_transactions.dart';
export 'src/zcash_wallet.dart';
diff --git a/cw_zcash/lib/src/legacy/zkool_sweep.dart b/cw_zcash/lib/src/legacy/zkool_sweep.dart
deleted file mode 100644
index 20f04da..0000000
--- a/cw_zcash/lib/src/legacy/zkool_sweep.dart
+++ /dev/null
@@ -1,196 +0,0 @@
-import 'dart:io';
-
-import 'package:cw_core/utils/print_verbose.dart';
-import 'package:path/path.dart' as p;
-import 'package:zkool/src/rust/api/account.dart' as zkool_account;
-import 'package:zkool/src/rust/api/coin.dart' as zkool_coin;
-import 'package:zkool/src/rust/api/sync.dart' as zkool_sync;
-import 'package:zkool/src/rust/api/pay.dart' as zkool_pay;
-import 'package:zkool/src/rust/api/key.dart' as zkool_key;
-import 'package:zkool/src/rust/api/network.dart' as zkool_network;
-import 'package:zkool/src/rust/pay.dart' as zkool_paydart;
-import 'package:zkool/src/rust/frb_generated.dart' as zkool_frb;
-
-class ZkoolSweep {
- ZkoolSweep({
- required this.currentBalance,
- required this.cacheDir,
- required this.seed,
- required this.passphrase,
- required this.address,
- required this.url,
- required this.height,
- });
- final int currentBalance;
- final String seed;
- final String cacheDir;
- final String passphrase;
- final String address;
- final String url;
- final int height;
- static SweepUpdate msg = SweepUpdate(message: 'Idling', currentHeight: 0, networkHeight: 0);
- Future<void> start() async {
- try {
- await _init();
- if (!zkool_key.isValidAddress(address: address)) {
- msg.message = "$address is not valid";
- return;
- }
-
- msg = SweepUpdate(message: 'Starting import', currentHeight: 0, networkHeight: 0);
- await _newAccount();
- msg.message = "Starting sync";
- await warpSync();
- msg.message = "Waiting";
- while (msg.blocksLeft != 0 && msg.networkHeight != 0) {
- await Future.delayed(Duration(seconds: 1));
- }
- await Future.delayed(Duration(seconds: 5));
- final balance = await getBalance();
- if (balance.toInt() == balance) {
- msg.message = "No need to sweep, balance is the same";
- return;
- }
- msg.message = "Sweeping";
- await sweep(balance);
- } catch (e) {
- msg.message = e.toString();
- printV("zkool_sweep: $e");
- rethrow;
- }
- }
-
- Future<BigInt> getBalance() async {
- final b = await zkool_sync.balance(c: c);
- return b.field0.reduce((final a, final b) => a + b);
- }
-
- Future<void> sweep(final BigInt amount) async {
- msg.message = "prepare";
- final tx = await zkool_pay.prepare(
- recipients: [zkool_paydart.Recipient(address: address, amount: amount)],
- options: zkool_pay.PaymentOptions(
- srcPools: 7,
- recipientPaysFee: true,
- smartTransparent: false,
- ),
- c: c,
- );
- msg.message = "getLatestHeight";
- final height = await getLatestHeight();
- msg.message = "signTransaction";
- final signTx = await zkool_pay.signTransaction(pczt: tx, c: c);
- msg.message = "extractTransaction";
- final txBytes = await zkool_pay.extractTransaction(package: signTx);
- msg.message = "broadcastTransaction";
- final result = await zkool_pay.broadcastTransaction(height: height, txBytes: txBytes, c: c);
- msg.message = "result";
- if (result.isEmpty) {
- msg.message = "Unknown error";
- throw Exception(result);
- }
- }
-
- static var c = zkool_coin.Coin();
-
- static bool _isInit = false;
-
- File get dbFile => File(p.join(cacheDir, "zkool-tmp-mirror.db"));
-
- Future<int> warpSync() async {
- final currentHeight = await getLatestHeight();
- msg.networkHeight = currentHeight;
- final sync = zkool_sync.synchronize(
- accounts: [currentAccount],
- currentHeight: currentHeight,
- actionsPerSync: 10000,
- transparentLimit: 100,
- checkpointAge: 200,
- c: c,
- );
- bool success = false;
- await sync.listen(
- (final syncProgress) {
- msg.currentHeight = syncProgress.height;
- msg.message = "Sync ${syncProgress.height} / ${syncProgress.time}";
- },
- onError: (final e) {
- msg.message = e.toString();
- },
- onDone: () {
- msg.message = "Sync done!";
- success = true;
- },
- );
-
- if (msg.currentHeight != currentHeight && msg.currentHeight != 0) {
- msg.message = "Canceling sync";
- await zkool_sync.cancelSync();
- await Future.delayed(Duration(seconds: 1));
- return warpSync();
- }
- return 0;
- }
-
- Future<int> getLatestHeight() async {
- try {
- return await zkool_network.getCurrentHeight(c: c);
- } catch (e) {
- printV("getLatestHeight: $e");
- }
- return 0;
- }
-
- Future<void> _newAccount() async {
- final id = await zkool_account.newAccount(
- na: zkool_account.NewAccount(
- name: 'restoretmp' + DateTime.now().microsecondsSinceEpoch.toString(),
- restore: true,
- passphrase: "",
- key: seed,
- aindex: 0,
- birth: height,
- folder: '',
- useInternal: true,
- internal: false,
- ledger: false,
- ),
- c: c,
- );
- currentAccount = id;
- c = await c.setAccount(account: id);
- }
-
- int currentAccount = 0;
-
- Future<void> _init() async {
- if (_isInit) return;
- _isInit = true;
- printV(".init()");
- await zkool_frb.RustLib.init();
- await zkool_network.initDatadir(directory: dbFile.parent.path);
- c = await c.openDatabase(dbFilepath: dbFile.path, password: 'cw_zcash_migration');
- c = c.setLwd(url: url, serverType: 0);
- printV(".init(): Done");
- }
-}
-
-class SweepUpdate {
- SweepUpdate({required this.message, required this.currentHeight, required this.networkHeight});
- String message;
- int currentHeight;
- int networkHeight;
- double get progress {
- if (networkHeight == 0) return 0.01;
- return currentHeight / networkHeight;
- }
-
- int get blocksLeft => networkHeight - currentHeight;
-
- Map<String, dynamic> toJson() => {
- "message": message,
- "currentHeight": currentHeight,
- "networkHeight": networkHeight,
- "progress": progress,
- };
-}
diff --git a/cw_zcash/lib/src/migrate.dart b/cw_zcash/lib/src/migrate.dart
new file mode 100644
index 0000000..e76e4d9
--- /dev/null
+++ b/cw_zcash/lib/src/migrate.dart
@@ -0,0 +1,96 @@
+import 'dart:math';
+
+import 'package:cw_core/get_height_by_date_zec.dart';
+import 'package:cw_core/pathForWallet.dart';
+import 'package:cw_core/utils/print_verbose.dart';
+import 'package:cw_core/wallet_type.dart';
+import 'package:cw_zcash/cw_zcash.dart';
+import 'package:sqflite_common_ffi/sqflite_ffi.dart';
+import 'package:path/path.dart' as p;
+
+class ZkoolSeed {
+ ZkoolSeed({required this.seed, required this.passphrase});
+
+ factory ZkoolSeed.fromYwalletSeed({required final String ywalletSeed}) {
+ final splSeed = ywalletSeed.split(" ");
+ String seed = "";
+ ;
+ String passphrase = "";
+ if ([13, 25].contains(splSeed.length)) {
+ seed = splSeed.take(splSeed.length - 1).join(" ");
+ passphrase = ywalletSeed.replaceFirst("${seed} ", "");
+ } else {
+ seed = ywalletSeed;
+ }
+ return ZkoolSeed(seed: seed, passphrase: passphrase);
+ }
+
+ String seed;
+ String passphrase;
+}
+
+class YwalletAccountInfo {
+ YwalletAccountInfo({
+ required this.idAccount,
+ required this.name,
+ required this.seed,
+ required this.aindex,
+ required this.sk,
+ required this.ivk,
+ required this.address,
+ });
+ final int idAccount;
+ final String name;
+ final String seed;
+ late final zkoolSeed = ZkoolSeed.fromYwalletSeed(ywalletSeed: seed);
+ final int aindex;
+ final String sk;
+ final String ivk;
+ final String address;
+ static YwalletAccountInfo fromJson(final Map<String, dynamic> j) => YwalletAccountInfo(
+ idAccount: j['id_account'],
+ name: j['name'],
+ seed: j['seed'],
+ aindex: j['aindex'],
+ sk: j['sk'],
+ ivk: j['ivk'],
+ address: j['address'],
+ );
+}
+
+Future<void> migrateOldSqliteToZkool2({required final String walletName}) async {
+ final dbFile = p.join(await pathForWalletTypeDir(type: WalletType.zcash), "zec.db");
+ final migrateDb = await openDatabase(dbFile);
+ final outputRaw = await migrateDb.rawQuery(
+ "SELECT `id_account`, `name`, `seed`, `aindex`, `sk`, `ivk`, `address` FROM `accounts`",
+ );
+ final output = outputRaw.map(YwalletAccountInfo.fromJson);
+
+ final walletId = await ZcashWalletBase.getLegacyZcashAccountIdForName(walletName);
+ for (final ya in output) {
+ if (ya.idAccount != walletId) {
+ continue;
+ }
+ printV("migrating account: ${ya.name}");
+ int birthHeight = await ZcashHeight.getBlockHeightByTime(DateTime(2026, 1, 1));
+
+ final accTxs = await migrateDb.rawQuery(
+ "SELECT height FROM transactions WHERE account = ${ya.idAccount}",
+ );
+ for (final tx in accTxs) {
+ final txHeight = int.tryParse(tx['height'].toString()) ?? birthHeight;
+ birthHeight = min(birthHeight, txHeight);
+ }
+
+ final accountId = await ZcashWalletBase.restoreZcashWalletFromSeed(
+ name: ya.name,
+ seed: ya.zkoolSeed.seed,
+ passphrase: ya.zkoolSeed.passphrase,
+ birthHeight: birthHeight,
+ );
+ await ZcashWalletBase.saveAccountId(ya.name, accountId);
+ return;
+ }
+
+ throw Exception("migration not finished (wallet name: $walletName not found)");
+}
diff --git a/cw_zcash/lib/src/pending_zcash_transaction.dart b/cw_zcash/lib/src/pending_zcash_transaction.dart
index db4f59f..3f6762d 100644
--- a/cw_zcash/lib/src/pending_zcash_transaction.dart
+++ b/cw_zcash/lib/src/pending_zcash_transaction.dart
@@ -1,14 +1,10 @@
-import 'dart:convert';
-import 'dart:math';
-
import 'package:cw_core/amount/money.dart';
-import 'package:cw_core/crypto_currency.dart';
-import 'package:cw_core/output_info.dart';
+import 'package:cw_core/exceptions.dart';
import 'package:cw_core/pending_transaction.dart';
+import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_zcash/cw_zcash.dart';
-import 'package:cw_zcash/src/zcash_taddress_rotation.dart';
-import 'package:warp_api/data_fb_generated.dart';
-import 'package:warp_api/warp_api.dart';
+import 'package:zkool/src/rust/api/pay.dart' as zkool_pay;
+import 'package:zkool/src/rust/api/network.dart' as zkool_network;
class PendingZcashTransaction with PendingTransaction {
PendingZcashTransaction({
@@ -21,7 +17,7 @@ class PendingZcashTransaction with PendingTransaction {
final ZcashWallet zcashWallet;
final ZcashTransactionCredentials credentials;
- final String txPlan;
+ final zkool_pay.PcztPackage txPlan;
String? _txId;
final Money availableBalance;
@@ -32,56 +28,42 @@ class PendingZcashTransaction with PendingTransaction {
String get hex => '';
@override
- Money get amount => Money.fromInt(_totalAmount, CryptoCurrency.zec);
-
- @override
- String get amountFormatted => amount.toString();
-
- int get _totalAmount {
+ Money get amount {
final isAll = credentials.outputs.fold<bool>(false, (final a, final b) => a || (b.sendAll));
if (isAll) {
- return availableBalance.amount.toInt();
+ return availableBalance - fee;
}
- return credentials.outputs.fold<int>(
- 0,
- (final a, final b) => a + b.cryptoAmount.amount.toInt(),
- );
+ return credentials.outputs
+ .map((final output) => output.cryptoAmount)
+ .reduce((final a, final b) => a + b);
}
+ @override
+ String get amountFormatted => amount.toString();
+
@override
final Money fee;
@override
Future<void> commit() async {
- _txId = await ZcashWalletService.runInDbMutex(
- () => WarpApi.signAndBroadcast(ZcashWalletBase.coin, zcashWallet.accountId, txPlan),
- );
- ZcashWalletBase.temporarySentTx[zcashWallet.accountId] ??= [];
- ZcashWalletBase.temporarySentTx[zcashWallet.accountId]?.add(
- ShieldedTx(
- base64.decode(
- ZcashTaddressRotation.flatBuffersPack(
- ShieldedTxT(
- id: Random().nextInt(pow(2, 32).toInt()),
- txId: _txId,
- height: 0,
- timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000,
- value: -_totalAmount,
- ).pack,
- ),
- ),
- ),
- );
- await ZcashTransactionInfo.addCachedDestinationAddress(
- _txId ?? '',
- credentials.outputs.reduce((final o1, final o2) {
- return OutputInfo(
- cryptoAmount: Money.zero(CryptoCurrency.zec),
- address: "${o1.address},${o2.address}",
- sendAll: false,
- isParsedAddress: false,
+ await ZcashWalletBase.runWithCoin(
+ accountId: zcashWallet.accountId,
+ func: (coin) async {
+ final signTx = await zkool_pay.signTransaction(pczt: txPlan, c: coin);
+ final txBytes = await zkool_pay.extractTransaction(package: signTx);
+ final currentHeight = await zkool_network.getCurrentHeight(c: coin);
+ final result = await zkool_pay.broadcastTransaction(
+ height: currentHeight,
+ txBytes: txBytes,
+ c: coin,
);
- }).address,
+ printV("result: $result");
+ final txId = ZcashWalletService.normalizeTxId(result);
+ if (txId.length != 64) {
+ throw TransactionCommitFailed(errorMessage: result);
+ }
+ _txId = txId;
+ },
);
await zcashWallet.updateTransactions();
await zcashWallet.updateBalance();
diff --git a/cw_zcash/lib/src/util/hex.dart b/cw_zcash/lib/src/util/hex.dart
new file mode 100644
index 0000000..1cd6a12
--- /dev/null
+++ b/cw_zcash/lib/src/util/hex.dart
@@ -0,0 +1,5 @@
+import 'package:flutter/services.dart';
+
+String uint8ListToHex(final Uint8List bytes) {
+ return bytes.map((final b) => b.toRadixString(16).padLeft(2, '0')).join();
+}
diff --git a/cw_zcash/lib/src/zcash_mempool.dart b/cw_zcash/lib/src/zcash_mempool.dart
new file mode 100644
index 0000000..33ba40f
--- /dev/null
+++ b/cw_zcash/lib/src/zcash_mempool.dart
@@ -0,0 +1,118 @@
+import 'dart:async';
+
+import 'package:cw_core/utils/print_verbose.dart';
+import 'package:cw_zcash/src/zcash_wallet_service.dart';
+import 'package:zkool/src/rust/api/coin.dart' as zkool_coin;
+import 'package:zkool/src/rust/api/mempool.dart' as zkool_mempool;
+
+typedef ZcashMempoolRefreshCallback = void Function(Set<int> accountIds);
+
+class ZcashMempoolService {
+ ZcashMempoolService._();
+
+ static final ZcashMempoolService instance = ZcashMempoolService._();
+
+ zkool_mempool.Mempool? _mempool;
+ final Map<String, zkool_mempool.MempoolTx> _txsById = {};
+
+ zkool_mempool.Mempool get _mempoolInstance => _mempool ??= zkool_mempool.Mempool();
+
+ ZcashMempoolRefreshCallback? onAccountsUpdated;
+
+ bool _loopRunning = false;
+ String? _nodeUrl;
+ StreamSubscription<zkool_mempool.MempoolMsg>? _subscription;
+
+ List<zkool_mempool.MempoolTx> txsForAccount(final int accountId) => _txsById.values
+ .where(
+ (final tx) =>
+ tx.notes.any((final n) => n.account == accountId) ||
+ tx.amounts.any((final a) => a.account == accountId),
+ )
+ .toList();
+
+ Future<void> ensureRunning(final zkool_coin.Coin c) async {
+ if (c.url.isEmpty) {
+ return;
+ }
+ if (_loopRunning && _nodeUrl == c.url) {
+ return;
+ }
+ await stop();
+ _nodeUrl = c.url;
+ _loopRunning = true;
+ unawaited(_runLoop(c));
+ }
+
+ Future<void> stop() async {
+ _loopRunning = false;
+ await _subscription?.cancel();
+ _subscription = null;
+ _txsById.clear();
+ try {
+ await _mempool?.cancel();
+ } catch (e) {
+ printV("mempool cancel: $e");
+ }
+ _mempool = null;
+ }
+
+ Future<void> _runLoop(final zkool_coin.Coin c) async {
+ while (_loopRunning && _nodeUrl == c.url) {
+ try {
+ final completer = Completer<void>();
+ _subscription = _mempoolInstance
+ .run(c: c)
+ .listen(
+ (final msg) {
+ if (!_loopRunning) {
+ return;
+ }
+ switch (msg) {
+ case zkool_mempool.MempoolMsg_BlockHeight():
+ _onBlockMined();
+ case zkool_mempool.MempoolMsg_TxId(:final field0):
+ _onMempoolTx(field0);
+ }
+ },
+ onDone: () {
+ if (!completer.isCompleted) {
+ completer.complete();
+ }
+ },
+ onError: (final e) {
+ printV("mempool stream error: $e");
+ if (!completer.isCompleted) {
+ completer.complete();
+ }
+ },
+ );
+ await completer.future;
+ } catch (e) {
+ printV("mempool loop error: $e");
+ }
+ if (_loopRunning) {
+ await Future.delayed(const Duration(seconds: 5));
+ }
+ }
+ }
+
+ void _onBlockMined() {
+ final affected = _allAffectedAccounts();
+ _txsById.clear();
+ onAccountsUpdated?.call(affected);
+ }
+
+ void _onMempoolTx(final zkool_mempool.MempoolTx tx) {
+ final normalized = ZcashWalletService.normalizeTxId(tx.txid);
+ _txsById[normalized] = tx;
+ onAccountsUpdated?.call(_affectedAccountsForTx(tx));
+ }
+
+ Set<int> _allAffectedAccounts() => _txsById.values.expand(_affectedAccountsForTx).toSet();
+
+ Set<int> _affectedAccountsForTx(final zkool_mempool.MempoolTx tx) => {
+ ...tx.notes.map((final n) => n.account),
+ ...tx.amounts.map((final a) => a.account),
+ };
+}
diff --git a/cw_zcash/lib/src/zcash_mnemonic_is_incorrect_exception.dart b/cw_zcash/lib/src/zcash_mnemonic_is_incorrect_exception.dart
deleted file mode 100644
index 4986837..0000000
--- a/cw_zcash/lib/src/zcash_mnemonic_is_incorrect_exception.dart
+++ /dev/null
@@ -1,5 +0,0 @@
-class ZcashMnemonicIsIncorrectException implements Exception {
- @override
- String toString() =>
- 'Zcash mnemonic has incorrect format. Mnemonic should contain 12 or 24 words separated by space.';
-}
diff --git a/cw_zcash/lib/src/zcash_taddress_rotation.dart b/cw_zcash/lib/src/zcash_taddress_rotation.dart
index 1c2ef1f..8e89403 100644
--- a/cw_zcash/lib/src/zcash_taddress_rotation.dart
+++ b/cw_zcash/lib/src/zcash_taddress_rotation.dart
@@ -12,368 +12,484 @@
// turned off and replaced with proper solution.
import 'dart:async';
-import 'dart:convert';
-import 'dart:io';
+import 'dart:math' show max;
-import 'package:cw_core/pathForWallet.dart';
+import 'package:cw_core/sync_status.dart';
+import 'package:cw_core/transaction_direction.dart';
import 'package:cw_core/utils/print_verbose.dart';
-import 'package:cw_core/wallet_info.dart';
-import 'package:cw_core/wallet_type.dart';
import 'package:cw_zcash/src/util/crc32.dart';
import 'package:cw_zcash/src/zcash_wallet.dart';
import 'package:cw_zcash/src/zcash_wallet_service.dart';
-import 'package:flutter/foundation.dart';
-import 'package:warp_api/data_fb_generated.dart';
-import 'package:warp_api/warp_api.dart';
-import 'package:flat_buffers/flat_buffers.dart' as fb;
-import 'package:path/path.dart' as p;
+import 'package:cw_zcash/src/zkool_compat.dart';
+import 'package:cw_zcash/src/zkooltx.dart';
+import 'package:zkool/src/rust/api/account.dart' as zkool_account;
+import 'package:zkool/src/rust/api/coin.dart' as zkool_coin;
+import 'package:zkool/src/rust/api/sync.dart' as zkool_sync;
+import 'package:zkool/src/rust/api/pay.dart' as zkool_pay;
+import 'package:zkool/src/rust/api/network.dart' as zkool_network;
+import 'package:zkool/src/rust/api/sweep.dart' as zkool_sweep;
+import 'package:zkool/src/rust/pay.dart' as zkool_paydart;
class ZcashTaddressRotation {
- ZcashTaddressRotation(this.accountIndex);
- final int accountIndex;
-
static bool _isStarted = false;
+ static zkool_coin.Coin get c => ZcashWalletBase.c;
+ static const int _sweepThreshold = 30000;
+ static const int _lookahead = 5;
+ // Matches transparentLimit in ZcashWalletBase._oneshotSync.
+ static const int _transparentSyncLimit = 100;
- static const coin = ZcashWalletBase.coin;
- static String seedForOffset(final String seed) {
- final seedWords = seed.split(" ");
+ static zkool_account.Seed seedForOffset(final zkool_account.Seed seed) {
+ final seedStr = "${seed.mnemonic} ${seed.phrase.replaceAll(" ", "_")}".trim();
+ final seedWords = seedStr.split(" ");
if ([12, 24].contains(seedWords.length)) {
seedWords.add("");
}
final lastI = seedWords.length - 1;
- final crc = CRC32.compute(seed);
- seedWords[lastI] += ":tgen:${crc}";
- return seedWords.join(" ");
+ final crc = CRC32.compute(seedStr);
+ seedWords[lastI] += ":tgen:$crc";
+ final phrase = seedWords.removeLast();
+ return zkool_account.Seed(mnemonic: seedWords.join(" "), phrase: phrase, aindex: 0);
+ }
+
+ static bool zkoolAccountSeedIsEqual(final zkool_account.Seed a, final zkool_account.Seed b) {
+ return a.mnemonic == b.mnemonic && a.phrase == b.phrase;
}
- static bool isSeedForWallet(final String? mainWallet, final String? subWallet) {
+ static bool isSeedForWallet(
+ final zkool_account.Seed? mainWallet,
+ final zkool_account.Seed? subWallet,
+ ) {
if (mainWallet == null || subWallet == null) return false;
- return seedForOffset(mainWallet.trim()) == subWallet;
+ return zkoolAccountSeedIsEqual(seedForOffset(mainWallet), subWallet);
}
- static Map<int, List<Account>> rotationAccounts = {};
- static Map<int, List<Account>> rotationAccountsUsable = {};
- static Map<int, List<ShieldedTx>> shieldedAccountsTx = {};
- static Future<void> init() async {
- printV("Deserializing previous state");
- if (_isStarted) {
+ static Map<int, List<ZkoolTx>> shieldedAccountsTx = {};
+ static final Map<int, int> _rotationAccountByMain = {};
+ static final Set<int> _resyncedRotationAccounts = {};
+ static final Set<String> _poolRescanAttempted = {};
+ static bool _sweepJobRunning = false;
+ static final Map<int, DateTime> _lastSweepBroadcastAt = {};
+
+ static bool _poolNeedsRescan(final List<zkool_account.TAddressTxCount> slots) {
+ if (slots.isEmpty) {
+ return false;
+ }
+ final unused = slots.where((final e) => e.txCount == 0);
+ if (unused.isEmpty) {
+ return false;
+ }
+ final head = unused.first;
+ final tail = slots.last.dindex;
+ if (slots.any((final e) => e.dindex > head.dindex && e.txCount > 0)) {
+ return true;
+ }
+ return head.dindex + _transparentSyncLimit <= tail;
+ }
+
+ static Future<void> _scanTransparentChain(final int rotationAccountId) async {
+ await ZcashWalletBase.runWithCoin(
+ accountId: rotationAccountId,
+ func: (final coin) async {
+ final all = await zkool_account.fetchAddressTxCount(
+ c: coin,
+ aggregate: false,
+ poolFilter: 1,
+ );
+ final external = all.where((final e) => e.scope == 0).toList();
+ if (external.isEmpty) {
+ return;
+ }
+ final maxD = external.map((final e) => e.dindex).reduce(max);
+ final scanner = await zkool_sweep.TransparentScanner.newInstance();
+ final height = await zkool_network.getCurrentHeight(c: coin);
+ final completer = Completer<void>();
+ final sub = scanner
+ .run(endHeight: height, gapLimit: maxD + _lookahead + 1, c: coin)
+ .listen(
+ (_) {},
+ onError: (final e) {
+ printV("transparent chain scan: $e");
+ if (!completer.isCompleted) {
+ completer.complete();
+ }
+ },
+ onDone: () {
+ if (!completer.isCompleted) {
+ completer.complete();
+ }
+ },
+ );
+ await Future.any([
+ completer.future,
+ Future.delayed(Duration(seconds: (maxD + 2).clamp(8, 45))),
+ ]);
+ await sub.cancel();
+ },
+ );
+ }
+
+ static Future<void> _resyncRotationPool(
+ final int rotationAccountId,
+ final int transparentLimit,
+ ) async {
+ await ZcashWalletBase.runWithCoin(
+ accountId: rotationAccountId,
+ func: (final coin) async {
+ await zkool_sync.cancelSync();
+ await zkool_account.resetSync(id: rotationAccountId, c: coin);
+ final height = await zkool_network.getCurrentHeight(c: coin);
+ final sync = zkool_sync.synchronize(
+ accounts: [rotationAccountId],
+ currentHeight: height,
+ actionsPerSync: 10000,
+ transparentLimit: transparentLimit,
+ checkpointAge: 200,
+ c: coin,
+ fast: false,
+ );
+ final completer = Completer<void>();
+ sync.listen(
+ (_) {},
+ onError: (final e) {
+ printV("rotation pool resync error: $e");
+ if (!completer.isCompleted) {
+ completer.complete();
+ }
+ },
+ onDone: () {
+ if (!completer.isCompleted) {
+ completer.complete();
+ }
+ },
+ );
+ await completer.future;
+ },
+ );
+ }
+
+ static Future<void> _ensurePoolScanned(final int rotationAccountId) async {
+ var slots = await _transparentSlots(rotationAccountId);
+ final firstSession = _resyncedRotationAccounts.add(rotationAccountId);
+ if (!firstSession && !_poolNeedsRescan(slots)) {
return;
}
- _isStarted = true;
- final pfw = await pathForWalletTypeDir(type: WalletType.zcash);
- try {
- final f = File(p.join(pfw, "zec-taddr.json"));
- if (!f.existsSync()) {
- f.writeAsBytesSync(serialize());
+ if (!firstSession) {
+ final head = slots.where((final e) => e.txCount == 0).firstOrNull?.dindex;
+ final key = '$rotationAccountId:$head';
+ if (_poolRescanAttempted.contains(key)) {
+ return;
}
- deserialize(f.readAsBytesSync());
- } catch (e) {
- printV("Failed to deserialize T address DB for zcash, it doesn't really matter: $e");
+ _poolRescanAttempted.add(key);
}
+ await _ensureLookahead(rotationAccountId);
+ await _scanTransparentChain(rotationAccountId);
+ slots = await _transparentSlots(rotationAccountId);
+ final limit = slots.isEmpty ? _transparentSyncLimit : slots.last.dindex + 1;
+ printV("rotation pool resync account $rotationAccountId limit $limit");
+ await _resyncRotationPool(rotationAccountId, limit);
+ }
+ static Future<void> init() async {
+ if (_isStarted) {
+ return;
+ }
+ _isStarted = true;
unawaited(
(() async {
- await Future.delayed(Duration(seconds: 5));
+ await Future.delayed(const Duration(seconds: 2));
return _jobRunner();
})(),
);
}
- static Future<void> serializeToFile() async {
- final pfw = await pathForWalletTypeDir(type: WalletType.zcash);
- final f = File(p.join(pfw, "zec-taddr.json"));
- f.writeAsBytesSync(serialize());
- }
+ static Future<int?> getRotationAccountForMainAccount(final int mainAccountId) async {
+ final cached = _rotationAccountByMain[mainAccountId];
+ if (cached != null) {
+ return cached;
+ }
+ try {
+ await ZcashWalletBase.runWithCoinMutex.acquire();
+ final wSeed = await zkool_account.getAccountSeed(c: c, account: mainAccountId);
+ if (wSeed == null) {
+ printV("Not running Taddr rotation - seed not found");
+ return null;
+ }
+ final seed = seedForOffset(wSeed);
+ final accs = await zkool_account.listAccounts(c: c);
+ for (int i = 0; i < accs.length; i++) {
+ final accSeed = await zkool_account.getAccountSeed(c: c, account: accs[i].id);
+ if (accSeed?.mnemonic == seed.mnemonic &&
+ accSeed?.phrase == seed.phrase &&
+ accSeed?.aindex == seed.aindex) {
+ _rotationAccountByMain[mainAccountId] = accs[i].id;
+ return accs[i].id;
+ }
+ }
- static Uint8List serialize() {
- final data = {
- "rotationAccounts": rotationAccounts.map(
- (final k, final v) => MapEntry(
- k.toRadixString(16),
- v.map((final a) => flatBuffersPack(a.unpack().pack)).toList(),
- ),
- ),
- if (kDebugMode)
- "_rotationAccounts": rotationAccounts.map(
- (final k, final v) =>
- MapEntry(k.toRadixString(16), v.map((final a) => a.toString()).toList()),
- ),
- "rotationAccountsUsable": rotationAccountsUsable.map(
- (final k, final v) => MapEntry(
- k.toRadixString(16),
- v.map((final a) => flatBuffersPack(a.unpack().pack)).toList(),
- ),
- ),
- if (kDebugMode)
- "_rotationAccountsUsable": rotationAccountsUsable.map(
- (final k, final v) =>
- MapEntry(k.toRadixString(16), v.map((final a) => a.toString()).toList()),
- ),
- "shieldedAccountsTx": shieldedAccountsTx.map(
- (final k, final v) => MapEntry(
- k.toRadixString(16),
- v.map((final a) => flatBuffersPack(a.unpack().pack)).toList(),
- ),
- ),
- if (kDebugMode)
- "_shieldedAccountsTx": shieldedAccountsTx.map(
- (final k, final v) =>
- MapEntry(k.toRadixString(16), v.map((final a) => a.toString()).toList()),
- ),
- };
+ zkool_account.Account? acc;
+ for (final a in accs) {
+ if (a.id == mainAccountId) {
+ acc = a;
+ break;
+ }
+ }
+ if (acc == null) {
+ printV("Main account $mainAccountId not found for Taddr rotation");
+ return null;
+ }
- if (kDebugMode) {
- return utf8.encode(JsonEncoder.withIndent(" ").convert(data));
+ final name = CRC32.compute(wSeed.mnemonic).toString();
+ final id = await newAccount(
+ name: name,
+ seed: seed.mnemonic,
+ passphrase: seed.phrase,
+ height: acc.birth,
+ aindex: seed.aindex,
+ );
+ _rotationAccountByMain[mainAccountId] = id;
+ return id;
+ } finally {
+ ZcashWalletBase.runWithCoinMutex.release();
}
- return utf8.encode(jsonEncode(data));
}
- static String flatBuffersPack(final int Function(fb.Builder fbBuilder) pack) {
- final fbBuilder = fb.Builder();
- final offset = pack(fbBuilder);
- fbBuilder.finish(offset);
- return base64.encode(fbBuilder.buffer);
+ // External (scope 0) transparent slots for the rotation account, ordered by
+ // dindex. zkool derives every slot in 0..=dindex, so unused ones (txCount==0)
+ // form the disposable look-ahead pool.
+ static Future<List<zkool_account.TAddressTxCount>> _transparentSlots(
+ final int rotationAccountId,
+ ) async {
+ final all = await ZcashWalletBase.runWithCoin(
+ accountId: rotationAccountId,
+ func: (final coin) =>
+ zkool_account.fetchAddressTxCount(c: coin, aggregate: false, poolFilter: 1),
+ );
+ return all.where((final e) => e.scope == 0).toList()
+ ..sort((final a, final b) => a.dindex.compareTo(b.dindex));
}
- static void deserialize(final Uint8List bytes) {
- // todo: replace keys with sums and reverse in serialize
- // final List<String> seeds = [];
- // for (int i = 0; i < accounts.length; i++) {
- // final acc = accounts[i];
- // final backup = WarpApi.getBackup(coin, acc.id);
- // await WarpApi.transparentSync(coin, acc.id, syncHeight);
- // seeds.add(backup.seed!);
- // }
- try {
- final Map<dynamic, dynamic> data = jsonDecode(utf8.decode(bytes));
- rotationAccounts = (data["rotationAccounts"] as Map<String, dynamic>).map(
- (final k, final v) => MapEntry(
- int.parse(k, radix: 16),
- (v as List).map((final a) => Account(atob(a))).toList(),
- ),
+ // Keep _lookahead unused disposable addresses ready, generating more whenever
+ // the pool runs low. Newly generated addresses are scanned by ongoing sync
+ // (the latest addresses are always in the scan window), so no resync needed.
+ static Future<void> _ensureLookahead(final int rotationAccountId) async {
+ final slots = await _transparentSlots(rotationAccountId);
+ final unused = slots.where((final e) => e.txCount == 0).length;
+ final toGenerate = _lookahead - unused;
+ for (var i = 0; i < toGenerate; i++) {
+ await ZcashWalletBase.runWithCoin(
+ accountId: rotationAccountId,
+ func: (final coin) => zkool_account.generateNextDindex(c: coin),
);
- rotationAccountsUsable = (data["rotationAccountsUsable"] as Map<String, dynamic>).map(
- (final k, final v) => MapEntry(
- int.parse(k, radix: 16),
- (v as List).map((final a) => Account(atob(a))).toList(),
- ),
- );
- shieldedAccountsTx = (data["shieldedAccountsTx"] as Map<String, dynamic>).map(
- (final k, final v) => MapEntry(
- int.parse(k, radix: 16),
- (v as List).map((final a) => ShieldedTx(atob(a))).toList(),
- ),
- );
- } catch (e) {
- rethrow;
+ }
+ if (toGenerate > 0) {
+ printV("topped up disposable taddr pool by $toGenerate (had $unused)");
}
}
- static Uint8List atob(final String value) =>
- Uint8List.fromList(List<int>.from(base64.decode(value)));
+ static bool _isWalletSynced(final int mainAccountId) {
+ final wallet = ZcashWalletBase.walletsByAccountId[mainAccountId];
+ return wallet != null && wallet.syncStatus is SyncedSyncStatus;
+ }
+
+ static Future<BigInt> _rotationTransparentBalance(final int rotationAccount) async {
+ return ZcashWalletBase.runWithCoin(
+ accountId: rotationAccount,
+ func: (final coin) async {
+ final b = await zkool_sync.balance(c: coin);
+ return b.field0.elementAt(0);
+ },
+ );
+ }
+
+ static Future<List<ZkoolTx>> _loadTxHistory(final int accountId) async {
+ return ZcashWalletBase.runWithCoin(
+ accountId: accountId,
+ func: (final coin) async {
+ final txsI = await zkool_account.listTxHistory(c: coin);
+ final txs = <ZkoolTx>[];
+ for (int i = 0; i < txsI.length; i++) {
+ final tx = txsI[i];
+ try {
+ final details = await zkool_account.getTxDetails(idTx: tx.id, c: coin);
+ txs.add(ZkoolTx(tx, details));
+ } catch (e) {
+ printV("getTxDetails skipped for tx ${tx.id}: $e");
+ }
+ }
+ return txs;
+ },
+ );
+ }
+
+ static bool _canBroadcastSweep(final int mainAccountId) {
+ final lastSweep = _lastSweepBroadcastAt[mainAccountId];
+ if (lastSweep == null) {
+ return true;
+ }
+ return !lastSweep.isAfter(DateTime.now().subtract(const Duration(seconds: 120)));
+ }
static Future<void> createAndSweepTAddresses() async {
- int chainHeight = 0;
+ if (_sweepJobRunning) {
+ return;
+ }
+ _sweepJobRunning = true;
try {
- chainHeight = await WarpApi.getLatestHeight(coin);
- } catch (e) {
- printV("Error getting latest height: $e");
- }
- final dbHeight = WarpApi.getDbHeight(coin);
- final height = dbHeight.unpack();
- final syncHeight = height.height;
- if (chainHeight != syncHeight) {
- printV(
- "Waiting for sync to finish: chainHeight(${chainHeight}) != syncHeight(${syncHeight})",
- );
+ final mainAccountIds = ZcashWalletBase.walletsByAccountId.keys.toList();
+ if (mainAccountIds.isEmpty) {
+ await createAndSweepTAddressesForAccount(c.account);
+ return;
+ }
+ for (final mainAccountId in mainAccountIds) {
+ try {
+ await createAndSweepTAddressesForAccount(mainAccountId);
+ } catch (e, s) {
+ printV("createAndSweepTAddresses for $mainAccountId error: $e");
+ s.toString().split("\n").forEach(printV);
+ }
+ }
+ } finally {
+ _sweepJobRunning = false;
+ }
+ }
+
+ static Future<void> createAndSweepTAddressesForAccount(final int cId) async {
+ if (!_isWalletSynced(cId)) {
+ printV("rotation sweep waiting for wallet sync (account $cId)");
return;
}
- final Map<int, List<ShieldedTx>> newShieldedAccountsTx = {};
- final accounts = WarpApi.getAccountList(coin);
- if (accounts.isEmpty) return;
- final Map<int, String> seeds = {};
- for (int i = 0; i < accounts.length; i++) {
- final acc = accounts[i];
- final backup = WarpApi.getBackup(coin, acc.id);
- await WarpApi.transparentSync(coin, acc.id, syncHeight);
- if (backup.seed == null) continue;
- seeds[acc.id] = backup.seed!;
+
+ printV("createAndSweepTAddresses for account $cId");
+
+ final rotationAccount = await getRotationAccountForMainAccount(cId);
+ if (rotationAccount == null) {
+ printV("rotation account is null, bailing out");
+ return;
}
- for (int i = 0; i < accounts.length; i++) {
- final seed = seeds[accounts[i].id]!;
- if ([12, 13, 24, 25].contains(seed.split(" ").length)) {
- if (seed.split(" ").last.contains(":tgen:")) continue;
- rotationAccounts[accounts[i].id] = [];
- }
+
+ await _ensureLookahead(rotationAccount);
+ await _ensurePoolScanned(rotationAccount);
+ await _refreshWalletAddresses(cId);
+
+ BigInt transparentBal = BigInt.zero;
+ try {
+ transparentBal = await _rotationTransparentBalance(rotationAccount);
+ printV("rotation account $rotationAccount transparent balance: $transparentBal");
+ } catch (e) {
+ printV("getTBalance: $e");
+ return;
}
- final raKeys = rotationAccounts.keys.toList();
- seeds.removeWhere((_, final val) => raKeys.contains(val));
- printV("raKeys: ${raKeys.length}");
- for (int i = 0; i < raKeys.length; i++) {
- final accs = accounts.where((final a) {
- final seed = WarpApi.getBackup(coin, a.id).seed;
- return isSeedForWallet(seeds[raKeys[i]], seed);
- }).toList();
- rotationAccounts[raKeys[i]]!.addAll(accs);
- final acc = accountForSeed(seeds[raKeys[i]]!)!;
- final txs = WarpApi.getTxsSync(coin, acc.id);
- newShieldedAccountsTx[acc.id] ??= [];
- newShieldedAccountsTx[acc.id]!.addAll(txs);
- }
- rotationAccountsUsable = rotationAccounts.map((final k, final v) => MapEntry(k, v.toList()));
- printV("rotationAccounts: ${rotationAccounts.length}");
-
- for (int i = 0; i < raKeys.length; i++) {
- final acc = accountForSeed(seeds[raKeys[i]]!)!;
- newShieldedAccountsTx[acc.id] = [];
- for (int j = 0; j < (rotationAccounts[raKeys[i]]?.length ?? 0); j++) {
- final txs = WarpApi.getTxsSync(coin, rotationAccounts[raKeys[i]]![j].id);
- rotationAccounts[raKeys[i]]![j];
- newShieldedAccountsTx[acc.id]!.addAll(txs);
- }
- rotationAccountsUsable[raKeys[i]]!.removeWhere((final a) {
- final txs = WarpApi.getTxsSync(coin, a.id);
- return txs.isNotEmpty;
- });
-
- // remove hidden addresses
- final wis = await WalletInfo.selectList('type = ?', [WalletType.zcash.index]);
-
- final List<String> hiddenAddresses_ = [];
- for (int k = 0; k < wis.length; k++) {
- final addrs = await wis[k].getHiddenAddresses();
- final addr2 = await wis[k].getUsedAddresses();
- hiddenAddresses_.addAll(addrs);
- hiddenAddresses_.addAll(addr2);
- }
- final Set<String> hiddenAddresses = hiddenAddresses_.toSet();
- rotationAccountsUsable[raKeys[i]]!.removeWhere((final a) {
- return hiddenAddresses.contains(WarpApi.getTAddr(coin, a.id));
- });
- }
- printV("rotationAccountsUsable: ${rotationAccountsUsable.length}");
-
- bool didAddNewAccount = false;
- for (int i = 0; i < raKeys.length; i++) {
- if (rotationAccountsUsable[raKeys[i]]!.length < 5) {
- final seed = seedForOffset(seeds[raKeys[i]]!);
- final name = CRC32.compute(raKeys[i].toString()).toString();
- final id = await ZcashWalletService.runInDbMutex(
- () => WarpApi.newAccount(coin, name, seed, rotationAccounts[raKeys[i]]!.length),
- );
- printV("new id: $id / $seed");
- printV("${rotationAccounts[raKeys[i]]}");
- printV(raKeys[i]);
- rotationAccounts[raKeys[i]]!.forEach((final a) {
- final b = WarpApi.getBackup(coin, a.id);
- printV("${a.id}");
- });
- didAddNewAccount = true;
- }
+
+ if (transparentBal < BigInt.from(_sweepThreshold)) {
+ await updateCache(mainAccountId: cId);
+ await _refreshWalletAfterRotationChange(cId);
+ return;
}
- printV("didAddNewAccount: ${didAddNewAccount}");
- if (didAddNewAccount) {
- return createAndSweepTAddresses();
+
+ if (!_canBroadcastSweep(cId)) {
+ printV("rotation sweep rate-limited for account $cId");
+ await updateCache(mainAccountId: cId);
+ await _refreshWalletAfterRotationChange(cId);
+ return;
}
- for (var i = 0; i < raKeys.length; i++) {
- final accs = rotationAccounts[raKeys[i]]!;
- inner:
- for (var j = 0; j < accs.length; j++) {
- int bal = 0;
- try {
- bal += await WarpApi.getTBalance(coin, accs[j].id);
- } catch (e) {
- printV("getTBalance: $e");
- return;
+ final toAddress = await ZcashWalletBase.runWithCoin(
+ accountId: cId,
+ func: (final coin) async {
+ final addrs = await zkool_account.getAddresses(c: coin, uaPools: 7);
+ final orchard = addrs.oaddr;
+ if (orchard == null || orchard.isEmpty) {
+ throw Exception('Orchard address unavailable for rotation sweep');
}
- if (bal < 30000) continue inner;
- final to = accountForSeed(seeds[raKeys[i]]!)!;
- // final toAddress = WarpApi.getTAddr(coin, to.id);
- // final fromSeed = WarpApi.getBackup(coin, to.id);
- // final txId = await ZcashWalletService.runInDbMutex(
- // () => WarpApi.sweepTransparentSeed(
- // to.coin,
- // to.id,
- // chainHeight,
- // fromSeed.seed!,
- // 7,
- // toAddress,
- // 0,
- // 1,
- // FeeT(fee: 10000, minFee: 0, maxFee: 0, scheme: 0),
- // ),
- // );
- final recipientBuilder = RecipientObjectBuilder(
- address: WarpApi.getAddress(coin, to.id, 4),
- pools: 4,
- feeIncluded: true,
- amount: bal,
- );
-
- final recipient = Recipient(recipientBuilder.toBytes());
- final fee = FeeT(fee: 10000, minFee: 0, maxFee: 0, scheme: 0);
- await ZcashWalletBase.loadProver();
- final txPlan = await ZcashWalletService.runInDbMutex(
- () => WarpApi.prepareTx(
- coin,
- accs[j].id,
- [recipient],
- 3, // pools: (Transparent + Sapling)
- 1,
- 0, // anchorOffset
- fee,
+ return orchard;
+ },
+ );
+ final result = await ZcashWalletBase.runWithCoin(
+ accountId: rotationAccount,
+ func: (final coin) async {
+ final tx = await zkool_pay.prepare(
+ recipients: [
+ zkool_paydart.Recipient(assetBase: zecBase, address: toAddress, amount: transparentBal),
+ ],
+ options: zkool_pay.PaymentOptions(
+ srcPools: 1,
+ recipientPaysFee: true,
+ smartTransparent: false,
),
+ c: coin,
);
- final _txId = await ZcashWalletService.runInDbMutex(
- () => WarpApi.signAndBroadcast(ZcashWalletBase.coin, accs[j].id, txPlan),
- );
- await ZcashWalletService.addShieldedTx(_txId);
- printV("shielded: $_txId");
+ final height = await zkool_network.getCurrentHeight(c: coin);
+ final signTx = await zkool_pay.signTransaction(pczt: tx, c: coin);
+ final txBytes = await zkool_pay.extractTransaction(package: signTx);
+ return zkool_pay.broadcastTransaction(height: height, txBytes: txBytes, c: coin);
+ },
+ );
+ printV("rotation sweep broadcast: $result");
+ if (result.isEmpty) {
+ throw Exception("rotation sweep broadcast failed");
+ }
- await Future.delayed(Duration(seconds: 120)); // let it be
- return createAndSweepTAddresses();
- }
+ await ZcashWalletService.addShieldedTx(result);
+ _lastSweepBroadcastAt[cId] = DateTime.now();
+ await updateCache(mainAccountId: cId);
+ await _refreshWalletAfterRotationChange(cId);
+ }
+
+ static Future<void> _refreshWalletAfterRotationChange(final int mainAccountId) async {
+ final wallet = ZcashWalletBase.walletsByAccountId[mainAccountId];
+ if (wallet == null) {
+ return;
}
- final nsatKeys = newShieldedAccountsTx.keys.toList();
- for (int i = 0; i < nsatKeys.length; i++) {
- inner:
- for (int j = 0; j < newShieldedAccountsTx[nsatKeys[i]]!.length; j++) {
- final tx = newShieldedAccountsTx[nsatKeys[i]]?[j];
- if (tx == null) {
- continue inner;
- }
- final _txId = tx.txId;
- if (_txId == null) {
- continue inner;
- }
- if (tx.value < 0) {
- await ZcashWalletService.addShieldedTx(_txId);
- // continue inner;
- }
- if (ZcashWalletService.autoshieldTx.contains(_txId)) {
- continue inner;
- }
+ unawaited(wallet.updateTransactions());
+ unawaited(wallet.updateBalance());
+ }
+
+ static Future<void> updateCache({required final int mainAccountId}) async {
+ final rotationAccount = await getRotationAccountForMainAccount(mainAccountId);
+ if (rotationAccount == null) {
+ printV("rotationAccount is null");
+ return;
+ }
+ await _ensureLookahead(rotationAccount);
+ await _ensurePoolScanned(rotationAccount);
+ final txs = await _loadTxHistory(rotationAccount);
+ shieldedAccountsTx[mainAccountId] = txs;
+ if (mainAccountId != rotationAccount && shieldedAccountsTx.containsKey(rotationAccount)) {
+ shieldedAccountsTx.remove(rotationAccount);
+ }
+
+ for (final tx in shieldedAccountsTx[mainAccountId]!) {
+ if (tx.direction == TransactionDirection.outgoing) {
+ await ZcashWalletService.addShieldedTx(tx.txHash);
}
}
- shieldedAccountsTx = newShieldedAccountsTx.map((final k, final v) => MapEntry(k, v.toList()));
- await serializeToFile();
- return;
+ await _refreshWalletAddresses(mainAccountId);
+ await _refreshWalletAfterRotationChange(mainAccountId);
+ }
+
+ static Future<void> _refreshWalletAddresses(final int mainAccountId) async {
+ final wallet = ZcashWalletBase.walletsByAccountId[mainAccountId];
+ if (wallet == null) {
+ return;
+ }
+ try {
+ await wallet.walletAddresses.refreshRotationAddresses();
+ } catch (e) {
+ printV("rotation address refresh: $e");
+ }
}
- static ShieldedTx reverseAmount(final ShieldedTx t) {
- final tt = t.unpack();
- tt.value = -tt.value;
- return ShieldedTx(base64.decode(flatBuffersPack(tt.pack)));
+ static List<ZkoolTx> rotationTxsForMainAccount(final int mainAccountId) {
+ return shieldedAccountsTx[mainAccountId] ?? const [];
}
- static Account? accountForSeed(final String seed) {
- final accounts = WarpApi.getAccountList(coin);
+ static Future<zkool_account.Account?> accountForSeed(final zkool_account.Seed seed) async {
+ final accounts = await zkool_account.listAccounts(c: c);
if (accounts.isEmpty) return null;
for (int i = 0; i < accounts.length; i++) {
final acc = accounts[i];
- final backup = WarpApi.getBackup(coin, acc.id);
- if (backup.seed == seed) {
+ final backup = await zkool_account.getAccountSeed(c: c, account: acc.id);
+ if (backup?.mnemonic == seed.mnemonic && backup?.phrase == seed.phrase) {
return acc;
}
}
@@ -383,63 +499,76 @@ class ZcashTaddressRotation {
static Future<void> _jobRunner() async {
for (;;) {
try {
- await Future.delayed(Duration(seconds: 5));
+ await Future.delayed(const Duration(seconds: 5));
await createAndSweepTAddresses();
} catch (e, s) {
- printV(e);
+ printV("createAndSweepTAddresses error: $e");
s.toString().split("\n").forEach(printV);
- rethrow;
} finally {
- await Future.delayed(Duration(seconds: 30));
+ await Future.delayed(const Duration(seconds: 30));
}
}
}
- static String? addressForAccount(final int accountId, final Set<String>? hiddenAddresses) {
- final accs = rotationAccountsUsable[accountId] ?? [];
- for (int i = 0; i < accs.length; i++) {
- final addr = WarpApi.getTAddr(coin, accs[i].id);
- if (hiddenAddresses?.contains(addr) == true) {
- continue;
- }
- return addr;
- }
- return null;
- }
-
- static List<String>? allAddressesForAccount(final int accountId) {
- final acc = rotationAccounts[accountId];
- if (acc == null) {
- printV("Nothing found");
+ // Current disposable address = first unused slot (lowest dindex).
+ static Future<String?> addressForAccount(final int accountId) async {
+ final rotationAccount = await getRotationAccountForMainAccount(accountId);
+ if (rotationAccount == null) {
return null;
}
- for (int i = 0; i < acc.length; i++) {
- final b = WarpApi.getBackup(coin, acc[i].id);
- printV("$i. ${b.seed?.split(" ").last}, ${b.index}, ${WarpApi.getTAddr(coin, acc[i].id)}");
+ final slots = await _transparentSlots(rotationAccount);
+ final unused = slots.where((final e) => e.txCount == 0);
+ if (unused.isNotEmpty) {
+ return unused.first.address;
}
- return acc.map((final a) => WarpApi.getTAddr(coin, a.id)).toList();
+ return slots.isNotEmpty ? slots.last.address : null;
}
- static List<String>? allUsedAddressesForAccount(final int accountId) {
- final seed = WarpApi.getBackup(coin, accountId).seed;
- if (seed == null) return [];
- final acc = rotationAccounts[seed]?.toList();
- if (acc == null) {
- printV("Nothing found");
- return null;
+ static Future<List<String>> usedAddressesForAccount(final int accountId) async {
+ final rotationAccount = await getRotationAccountForMainAccount(accountId);
+ if (rotationAccount == null) {
+ return const [];
}
- acc.removeWhere((final a1) {
- for (int i = 0; i < (rotationAccountsUsable[seed]?.length ?? 0); i++) {
- if (rotationAccountsUsable[seed]?[i].id == a1.id) {
- return true;
- }
- }
- return false;
- });
- for (int i = 0; i < acc.length; i++) {
- final b = WarpApi.getBackup(coin, acc[i].id);
- printV("$i. ${b.seed?.split(" ").last}, ${b.index}, ${WarpApi.getTAddr(coin, acc[i].id)}");
+ final slots = await _transparentSlots(rotationAccount);
+ return [
+ for (final entry in slots)
+ if (entry.txCount > 0) entry.address,
+ ];
+ }
+
+ // The full disposable pool: used slots plus the unused look-ahead window.
+ static Future<List<String>> allAddressesForAccount(final int accountId) async {
+ final rotationAccount = await getRotationAccountForMainAccount(accountId);
+ if (rotationAccount == null) {
+ return const [];
}
- return acc.map((final a) => WarpApi.getTAddr(coin, a.id)).toList();
+ final slots = await _transparentSlots(rotationAccount);
+ return slots.map((final e) => e.address).toList();
+ }
+
+ static Future<int> newAccount({
+ required final String name,
+ required final String seed,
+ required final String passphrase,
+ required final int height,
+ required final int aindex,
+ }) async {
+ final id = await zkool_account.newAccount(
+ na: zkool_account.NewAccount(
+ name: name,
+ restore: true,
+ passphrase: passphrase,
+ key: seed,
+ aindex: aindex,
+ birth: height,
+ folder: '',
+ pools: 1,
+ useInternal: true,
+ internal: false,
+ ledger: false,
+ ),
+ c: c,
+ );
+ return id;
}
}
diff --git a/cw_zcash/lib/src/zcash_transaction_info.dart b/cw_zcash/lib/src/zcash_transaction_info.dart
index d307caf..9dd09bd 100644
--- a/cw_zcash/lib/src/zcash_transaction_info.dart
+++ b/cw_zcash/lib/src/zcash_transaction_info.dart
@@ -1,15 +1,9 @@
-import 'dart:convert';
-import 'dart:io';
-
import 'package:cw_core/amount/money.dart';
-import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/transaction_direction.dart';
import 'package:cw_core/transaction_info.dart';
import 'package:cw_core/format_amount.dart';
-import 'package:cw_core/utils/print_verbose.dart';
-import 'package:cw_core/wallet_type.dart';
import 'package:cw_zcash/cw_zcash.dart';
-import 'package:path/path.dart' as p;
+import 'package:cw_zcash/src/zkooltx.dart';
class ZcashTransactionInfo extends TransactionInfo {
ZcashTransactionInfo({
@@ -23,6 +17,9 @@ class ZcashTransactionInfo extends TransactionInfo {
required final int confirmations,
required final String to,
final String? memo,
+ final TxType? txType,
+ final bool isRotationReceive = false,
+ final bool isShieldAction = false,
}) {
this.id = id;
this.amount = amount;
@@ -32,23 +29,21 @@ class ZcashTransactionInfo extends TransactionInfo {
this.date = date;
this.isPending = isPending;
this.confirmations = confirmations;
- this.to = getCachedDestinationAddress(id);
+ this.to = to;
if (memo != null && memo.isNotEmpty) {
additionalInfo['memo'] = memo;
}
- additionalInfo['autoShield'] = false;
- // note: this won't work yet, fee is not in zcash_lib metadata,
- // leaving here so it can start working automagically in future
- if (amount == fee && to.startsWith("u")) {
- additionalInfo['autoShield'] = true;
+ if (txType != null) {
+ additionalInfo['txType'] = txType.name;
}
- // remove below when above starts working
- additionalInfo['autoShield'] = ZcashWalletService.autoshieldTx.contains(txHash);
- // --- == === cut here === == ---
- if (additionalInfo['autoShield'] == true) {
+ additionalInfo['isRotationReceive'] = isRotationReceive;
+ additionalInfo['isAutoShield'] = isShieldAction || ZcashWalletService.isAutoshieldTx(txHash);
+
+ if (additionalInfo['isAutoShield'] == true) {
additionalInfo['memo'] ??= '';
- additionalInfo['memo'] += '\This is an auto-shielding transaction. Enjoy default privacy!';
- additionalInfo['memo'] = additionalInfo['memo'].trim();
+ additionalInfo['memo'] =
+ '${additionalInfo['memo']}\nThis is an auto-shielding transaction. Enjoy default privacy!.'
+ .trim();
}
}
@@ -61,35 +56,4 @@ class ZcashTransactionInfo extends TransactionInfo {
void changeFiatAmount(final String amount) => _fiatAmount = formatAmount(amount);
String? get memo => additionalInfo['memo'] as String?;
-
- static final Map<String, String> _destinationAddressMap = {};
-
- static String? getCachedDestinationAddress(final String txId) {
- return _destinationAddressMap[txId] ??
- _destinationAddressMap['"$txId"'] ??
- _destinationAddressMap[txId.replaceAll('"', '')];
- }
-
- static Future<void> addCachedDestinationAddress(final String txId, final String address) async {
- _destinationAddressMap[txId] = address;
- final pfwt = await pathForWalletTypeDir(type: WalletType.zcash);
- final f = File(p.join(pfwt, "sent-tx-map.json"));
- f.writeAsStringSync(json.encode(_destinationAddressMap));
- }
-
- static Future<void> init() async {
- try {
- final pfwt = await pathForWalletTypeDir(type: WalletType.zcash);
- final f = File(p.join(pfwt, "sent-tx-map.json"));
- if (!f.existsSync()) {
- f.writeAsStringSync('{}');
- }
- final tmpMap = json.decode(f.readAsStringSync());
- tmpMap.forEach((final k, final v) {
- _destinationAddressMap[k.toString()] = v.toString();
- });
- } catch (e, s) {
- printV("failed to deserialize: $e, $s");
- }
- }
}
diff --git a/cw_zcash/lib/src/zcash_wallet.dart b/cw_zcash/lib/src/zcash_wallet.dart
index d255ae7..e1258e5 100644
--- a/cw_zcash/lib/src/zcash_wallet.dart
+++ b/cw_zcash/lib/src/zcash_wallet.dart
@@ -1,9 +1,10 @@
import 'dart:async';
-import 'dart:convert';
import 'dart:io';
+import 'dart:math';
import 'package:cw_core/amount/money.dart';
import 'package:cw_core/crypto_currency.dart';
+import 'package:cw_core/get_height_by_date_zec.dart';
import 'package:cw_core/monero_transaction_priority.dart';
import 'package:cw_core/node.dart';
import 'package:cw_core/pathForWallet.dart';
@@ -12,26 +13,28 @@ import 'package:cw_core/sync_status.dart';
import 'package:cw_core/transaction_direction.dart';
import 'package:cw_core/transaction_priority.dart';
import 'package:cw_core/utils/print_verbose.dart';
-import 'package:cw_core/utils/proxy_wrapper.dart';
-import 'package:cw_core/wallet_addresses.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_credentials.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:bip39/bip39.dart' as bip39;
import 'package:cw_zcash/cw_zcash.dart';
-import 'package:cw_zcash/src/legacy/zkool_sweep.dart';
import 'package:cw_zcash/src/util/crc32.dart';
+import 'package:cw_zcash/src/zcash_mempool.dart';
import 'package:cw_zcash/src/zcash_taddress_rotation.dart';
import 'package:cw_zcash/src/zcash_wallet_addresses.dart';
-import 'package:flutter/foundation.dart';
+import 'package:cw_zcash/src/zkool_compat.dart';
+import 'package:cw_zcash/src/zkooltx.dart';
import 'package:mobx/mobx.dart';
-import 'package:warp_api/warp_api.dart';
-import 'package:warp_api/data_fb_generated.dart';
-import 'package:flutter/services.dart';
import 'package:mutex/mutex.dart';
-import 'package:path/path.dart' as p;
-import 'package:path_provider/path_provider.dart';
+import 'package:zkool/src/rust/api/account.dart' as zkool_account;
+import 'package:zkool/src/rust/api/coin.dart' as zkool_coin;
+import 'package:zkool/src/rust/api/mempool.dart' as zkool_mempool;
+import 'package:zkool/src/rust/api/sync.dart' as zkool_sync;
+import 'package:zkool/src/rust/api/pay.dart' as zkool_pay;
+import 'package:zkool/src/rust/api/network.dart' as zkool_network;
+import 'package:zkool/src/rust/pay.dart' as zkool_paydart;
+import 'package:zkool/src/rust/frb_generated.dart' as zkool_frb;
part 'zcash_wallet.g.dart';
@@ -42,24 +45,51 @@ abstract class ZcashWalletBase
with Store {
ZcashWalletBase(super.walletInfo, super.derivationInfo, {required this.accountId}) {
transactionHistory = ZcashTransactionHistory();
+ walletsByAccountId[accountId] = this;
}
- final int accountId;
- static const int coin = 0; // Zcash mainnet coin ID in warp_api
+ static final Map<int, ZcashWalletBase> walletsByAccountId = {};
+
+ static Future<void> refreshWalletForAccount(final int accountId) async {
+ final wallet = walletsByAccountId[accountId];
+ if (wallet == null) {
+ return;
+ }
+ await wallet.updateTransactions();
+ await wallet.updateBalance();
+ }
+
+ int accountId;
+
@override
@observable
SyncStatus syncStatus = NotConnectedSyncStatus();
- Timer? _syncStatusTimer;
- Timer? _periodicSyncTimer;
- int _initialSyncHeight = 0;
- int _lastKnownBlockHeight = 0;
-
@override
ObservableMap<CryptoCurrency, ZcashBalance> balance = ObservableMap.of({
CryptoCurrency.zec: ZcashBalance.zero(),
});
+ static const int _autoShieldMinSweep = 30000;
+
+ Money _feeFromTxPlan(
+ final zkool_pay.PcztPackage txPlan,
+ final TransactionPriority priority,
+ final int tryReduceFeeAmount, {
+ final zkool_coin.Coin? coin,
+ }) {
+ try {
+ return Money(zkool_pay.toPlan(package: txPlan, c: coin ?? c).fee, currency);
+ } catch (_) {
+ return Money.fromInt(
+ tryReduceFeeAmount != 0
+ ? tryReduceFeeAmount
+ : internalCalculateEstimatedFee(priority, null),
+ currency,
+ );
+ }
+ }
+
static int internalCalculateEstimatedFee(final TransactionPriority priority, final int? amount) {
const baseFee = 10000;
switch (priority) {
@@ -96,9 +126,8 @@ abstract class ZcashWalletBase
@override
Future<void> close({final bool shouldCleanup = false}) async {
- _stopSyncStatusUpdates();
- _stopPeriodicSync();
- await ZcashWalletService.runInDbMutex(() async => WarpApi.cancelSync());
+ _syncLoopRunning = false;
+ walletsByAccountId.remove(accountId);
}
Node? lastNode;
@@ -115,14 +144,12 @@ abstract class ZcashWalletBase
lwdUrl = '$protocol$lwdUrl';
}
printV("Setting LWD URL to: $lwdUrl");
- WarpApi.updateLWD(coin, lwdUrl);
+ c = c.setLwd(url: lwdUrl, serverType: 0);
syncStatus = ConnectedSyncStatus();
- try {
- await updateBalance();
- await updateTransactions();
- } catch (e) {
- printV("Error updating balance/transactions after connect: $e");
- }
+ unawaited(ZcashMempoolService.instance.ensureRunning(c));
+ _ensureSyncLoopRunning();
+ unawaited(_refreshSyncStatus());
+ unawaited(_oneshotSync());
} catch (e) {
printV("Connection error: $e");
syncStatus = FailedSyncStatus(error: e.toString());
@@ -130,261 +157,844 @@ abstract class ZcashWalletBase
}
}
- @override
- Future<PendingTransaction> createTransaction(final Object credentials) async {
- final creds = credentials as ZcashTransactionCredentials;
- await updateBalance();
-
- final zcashBalance = balance[CryptoCurrency.zec];
- final availableBalance = zcashBalance?.available ?? Money.zero(currency);
+ bool _syncLoopRunning = false;
- final recipients = <Recipient>[];
- var totalAmount = Money.zero(currency);
+ void _ensureSyncLoopRunning() {
+ if (_syncLoopRunning) {
+ return;
+ }
+ _syncLoopRunning = true;
+ unawaited(_runSyncLoop());
+ }
- for (final output in creds.outputs) {
- Money amount;
- if (output.sendAll) {
- amount = availableBalance;
- } else {
- amount = output.cryptoAmount;
+ static const _syncedPollInterval = Duration(seconds: 5);
+ static const _activePollInterval = Duration(seconds: 1);
- if (amount <= Money.zero(currency)) {
- throw Exception(
- 'Invalid amount for output. Amount: ${output.cryptoAmount}, Formatted: ${output.cryptoAmount}',
- );
- }
+ Future<void> _runSyncLoop() async {
+ var pollInterval = _activePollInterval;
+ while (_syncLoopRunning) {
+ await Future.delayed(pollInterval);
+ try {
+ final alreadySynced = await _oneshotSync();
+ pollInterval = alreadySynced ? _syncedPollInterval : _activePollInterval;
+ } catch (e) {
+ printV("zcash sync failed: $e");
+ pollInterval = _activePollInterval;
}
+ }
+ }
- totalAmount += amount;
+ int _syncCheckpointHeight = 0;
+
+ bool get isSyncing {
+ return _isSyncing;
+ }
+
+ set isSyncing(final bool value) {
+ _isSyncing = value;
+ }
+
+ bool _isSyncing = false;
+
+ static int oneshotSyncCount = 0;
+
+ Future<int> _getLowestSyncHeight() async {
+ final accounts = await zkool_account.listAccounts(c: c);
+ var lowest = await _getWalletDbHeight();
+ for (final acc in accounts) {
+ if (acc.id == accountId) continue;
+ c = await c.setAccount(account: acc.id);
+ lowest = min(lowest, await _getWalletDbHeight());
+ }
+ c = await c.setAccount(account: accountId);
+ return lowest;
+ }
- var address = (output.isParsedAddress ? output.extractedAddress! : output.address).trim();
+ Future<bool> _anyAccountNeedsSync(final int currentHeight) =>
+ withSharedCoinLock(() => _anyAccountNeedsSyncUnlocked(currentHeight));
- if (address.isEmpty) {
- throw Exception('Empty address for output');
+ Future<bool> _anyAccountNeedsSyncUnlocked(final int currentHeight) async {
+ final accounts = await zkool_account.listAccounts(c: c);
+ for (final acc in accounts) {
+ c = await c.setAccount(account: acc.id);
+ if (currentHeight > await _getWalletDbHeight()) {
+ c = await c.setAccount(account: accountId);
+ return true;
}
+ }
+ c = await c.setAccount(account: accountId);
+ return false;
+ }
- final paymentUri = WarpApi.decodePaymentURI(coin, address);
- String memo = output.memo ?? '';
- // String memo = '';
- if (paymentUri != null && paymentUri.address != null) {
- address = paymentUri.address!;
- if (memo.isEmpty && paymentUri.memo != null) {
- memo = paymentUri.memo!;
- }
+ @action
+ void _applySyncProgress(final int currentHeight, final int walletHeight) {
+ final blocksLeft = (currentHeight - walletHeight).clamp(0, currentHeight);
+ _syncCheckpointHeight = walletHeight;
+ if (blocksLeft <= 0) {
+ syncStatus = _isSyncing ? SyncingSyncStatus(1, 0.999) : SyncedSyncStatus();
+ return;
+ }
+ final ptc = currentHeight > 0 ? (walletHeight / currentHeight).clamp(0.0, 1.0) : 0.0;
+ syncStatus = SyncingSyncStatus(blocksLeft, ptc);
+ }
+
+ void _broadcastSyncProgress(final int currentHeight, final int walletHeight) {
+ runInAction(() {
+ for (final wallet in walletsByAccountId.values) {
+ wallet._applySyncProgress(currentHeight, walletHeight);
}
+ });
+ }
- if (!WarpApi.validAddress(coin, address)) {
- throw Exception('Invalid Zcash address: $address');
+ Future<int> _getWalletDbHeight() async {
+ try {
+ return (await zkool_sync.getDbHeight(c: c)).height;
+ } catch (_) {
+ final accounts = await zkool_account.listAccounts(c: c);
+ final account = accounts.where((final a) => a.id == accountId).firstOrNull;
+ if (account != null) {
+ return account.height;
}
+ rethrow;
+ }
+ }
- int recipientPools = 7;
+ void _onSyncCheckpoint(final int currentHeight, final int checkpointHeight) {
+ final height = checkpointHeight > _syncCheckpointHeight
+ ? checkpointHeight
+ : _syncCheckpointHeight;
+ _broadcastSyncProgress(currentHeight, height);
+ }
+
+ @action
+ Future<void> _refreshSyncStatus() async {
+ try {
+ await withSharedCoinLock(() async {
+ c = await c.setAccount(account: accountId);
+ final currentHeight = await zkool_network.getCurrentHeight(c: c);
+ final walletDbHeight = await _getWalletDbHeight();
+ _broadcastSyncProgress(currentHeight, walletDbHeight);
+ });
+ } catch (e) {
+ printV("refresh sync status: $e");
+ }
+ }
- if (address.startsWith('t1') || address.startsWith('t3')) {
- recipientPools = 1; // Transparent only
- } else if (address.startsWith('zs')) {
- recipientPools = 2; // Sapling only
+ @action
+ Future<bool> _oneshotSync() async {
+ try {
+ if (isSyncing) {
+ return syncStatus is SyncedSyncStatus;
}
- // For unified addresses (u1...) and other types, use 7 (all pools)
-
- final builder = RecipientObjectBuilder(
- address: address,
- pools: recipientPools,
- amount: amount.amount.toInt(),
- feeIncluded: output.sendAll,
- replyTo: false,
- memo: memo.isNotEmpty ? memo : null,
+ isSyncing = true;
+ late final int currentHeight;
+ late final int walletDbHeight;
+ await withSharedCoinLock(() async {
+ c = await c.setAccount(account: accountId);
+ currentHeight = await zkool_network.getCurrentHeight(c: c);
+ walletDbHeight = await _getWalletDbHeight();
+ });
+ if (!await _anyAccountNeedsSync(currentHeight)) {
+ _syncCheckpointHeight = walletDbHeight;
+ if (syncStatus is! SyncedSyncStatus) {
+ syncStatus = SyncedSyncStatus();
+ }
+ isSyncing = false;
+ return true;
+ }
+ await zkool_sync.cancelSync();
+ late final List<int> accountList;
+ late final int lagHeight;
+ await withSharedCoinLock(() async {
+ final accounts = await zkool_account.listAccounts(c: c);
+ accountList = accounts.map((final a) => a.id).toList()
+ ..removeWhere((final a) => a == c.account);
+ c = await c.setAccount(account: accountId);
+ lagHeight = await _getLowestSyncHeight();
+ });
+ _broadcastSyncProgress(currentHeight, lagHeight);
+ final sync = zkool_sync.synchronize(
+ accounts: [c.account, ...accountList],
+ currentHeight: currentHeight,
+ actionsPerSync: 10000,
+ transparentLimit: 100,
+ checkpointAge: 200,
+ c: c,
+ fast: false,
);
-
- recipients.add(Recipient(builder.toBytes()));
+ await withSharedCoinLock(() async {
+ c = await c.setAccount(account: accountId);
+ });
+ final randInt = CRC32.compute("${DateTime.now().microsecondsSinceEpoch}").toRadixString(16);
+ oneshotSyncCount++;
+ final completer = Completer<void>();
+ var lastLoggedHeight = walletDbHeight;
+ var chainTip = currentHeight;
+ late final StreamSubscription<zkool_sync.SyncProgress> subscription;
+ subscription = sync.listen(
+ (final syncProgress) {
+ if (syncProgress.height > chainTip) {
+ chainTip = syncProgress.height;
+ }
+ if (syncProgress.height >= lastLoggedHeight + 5000) {
+ lastLoggedHeight = syncProgress.height;
+ printV(
+ "[${c.account} ($accountList)] [$oneshotSyncCount/$randInt] sync: ${syncProgress.height}",
+ );
+ unawaited(
+ zkool_network.getCurrentHeight(c: c).then((final tip) {
+ if (tip > chainTip) {
+ chainTip = tip;
+ _onSyncCheckpoint(chainTip, syncProgress.height);
+ }
+ }),
+ );
+ }
+ _onSyncCheckpoint(chainTip, syncProgress.height);
+ },
+ onError: (final e) {
+ printV("[${c.account} ($accountList)] [$oneshotSyncCount/$randInt] error syncing: $e");
+ runInAction(() {
+ syncStatus = FailedSyncStatus(
+ error:
+ e.toString().replaceAll("AnyhowException(", "").split("\n").firstOrNull ??
+ "Unknown error",
+ );
+ });
+ isSyncing = false;
+ if (!completer.isCompleted) {
+ completer.complete();
+ }
+ },
+ onDone: () async {
+ printV("[${c.account} ($accountList)] [$oneshotSyncCount/$randInt] synchronized");
+ oneshotSyncCount--;
+ isSyncing = false;
+ try {
+ await withSharedCoinLock(() async {
+ c = await c.setAccount(account: accountId);
+ if (await _anyAccountNeedsSyncUnlocked(currentHeight)) {
+ final lagHeight = await _getLowestSyncHeight();
+ _broadcastSyncProgress(currentHeight, lagHeight);
+ return;
+ }
+ runInAction(() {
+ for (final wallet in walletsByAccountId.values) {
+ wallet.syncStatus = SyncedSyncStatus();
+ }
+ });
+ for (final wallet in walletsByAccountId.values) {
+ unawaited(wallet.updateBalance());
+ unawaited(wallet.updateTransactions());
+ unawaited(
+ ZcashTaddressRotation.updateCache(mainAccountId: wallet.accountId)
+ .catchError((final e) {
+ printV("rotation cache refresh: $e");
+ }),
+ );
+ }
+ });
+ } catch (e) {
+ printV("sync done height refresh: $e");
+ }
+ if (!completer.isCompleted) {
+ completer.complete();
+ }
+ },
+ );
+ await completer.future;
+ await subscription.cancel();
+ return syncStatus is SyncedSyncStatus;
+ } catch (e) {
+ syncStatus = FailedSyncStatus(error: e.toString());
+ isSyncing = false;
+ printV("error syncing: $e");
+ return false;
}
+ }
+
+ @override
+ Future<PendingTransaction> createTransaction(final Object credentials) =>
+ _createTransaction(credentials);
+
+ Future<PendingTransaction> _createTransaction(
+ final Object credentials, {
+ final int tryReduceFeeAmount = 0,
+ }) async {
+ final creds = credentials as ZcashTransactionCredentials;
+ await updateBalance();
+
+ final zcashBalance = balance[CryptoCurrency.zec];
+ final availableBalance = zcashBalance?.available ?? Money.zero(currency);
- if (totalAmount > availableBalance) {
- throw Exception('Insufficient balance');
+ final recipients = <zkool_paydart.Recipient>[];
+
+ bool receipientPaysFee = false;
+
+ for (final output in creds.outputs) {
+ receipientPaysFee = receipientPaysFee || output.sendAll;
+ var amount = output.cryptoAmount;
+ if (output.sendAll) {
+ amount = availableBalance - Money.fromInt(tryReduceFeeAmount, currency);
+ }
+ final recipientAddress = output.isParsedAddress ? output.extractedAddress! : output.address;
+ recipients.add(
+ zkool_paydart.Recipient(
+ assetBase: zecBase,
+ address: recipientAddress,
+ amount: amount.amount,
+ userMemo: output.memo,
+ ),
+ );
}
- final fee = FeeT(
- fee: internalCalculateEstimatedFee(creds.priority, null),
- minFee: 0,
- maxFee: 0,
- scheme: 0, // Fixed fee scheme
- );
+ final sendAmount = creds.outputs
+ .map((final out) => out.cryptoAmount)
+ .reduce((final a, final b) => a + b);
+
+ if (availableBalance == sendAmount) {
+ receipientPaysFee = true;
+ }
// pools parameter: bitmask for which pools to use for sending
// 1=Transparent, 2=Sapling, 4=Orchard, 7=All pools
- await ZcashWalletBase.loadProver();
// Using 7 (all pools) allows spending from any pool type
- final txPlan = await ZcashWalletService.runInDbMutex(
- () => WarpApi.prepareTx(
- coin,
- accountId,
- recipients,
- 7, // pools: All pools (Transparent + Sapling + Orchard) - allows spending from any pool
- 1, // senderUAType: 0 = unified address
- 0, // anchorOffset
- fee,
- ),
+ try {
+ return await runWithCoin(
+ accountId: accountId,
+ func: (coin) async {
+ final txPlan = await zkool_pay.prepare(
+ recipients: recipients,
+ options: zkool_pay.PaymentOptions(
+ srcPools: 7,
+ recipientPaysFee: receipientPaysFee,
+ smartTransparent: false,
+ ),
+ c: coin,
+ );
+ final txFee = _feeFromTxPlan(txPlan, creds.priority, tryReduceFeeAmount, coin: coin);
+ return PendingZcashTransaction(
+ zcashWallet: this as ZcashWallet,
+ credentials: creds,
+ txPlan: txPlan,
+ fee: txFee,
+ availableBalance: availableBalance,
+ );
+ },
+ );
+ } catch (e) {
+ if (tryReduceFeeAmount != 0) rethrow;
+ final estr = e.toString();
+ const prefix = "Not enough funds, ";
+ const suffix = " more ZEC required";
+ if (estr.contains(prefix) && estr.contains(suffix)) {
+ final start = estr.indexOf(prefix) + prefix.length;
+ final end = estr.indexOf(suffix, start);
+ final amtStr = estr.substring(start, end);
+ final amt = double.tryParse(amtStr);
+ if (amt == null) rethrow;
+ final feeInt = (amt * 100000000).ceil();
+ return _createTransaction(credentials, tryReduceFeeAmount: feeInt);
+ }
+ rethrow;
+ }
+ }
+
+ static const _dispPhrase = "Received to disposable address";
+
+ ZcashTransactionInfo _zcashInfoFromMempoolTx(
+ final zkool_mempool.MempoolTx tx,
+ final int accountId,
+ ) {
+ final accountNotes = tx.notes.where((final n) => n.account == accountId);
+ final netValue = accountNotes.fold<BigInt>(
+ BigInt.zero,
+ (final sum, final note) => sum + BigInt.from(note.value),
+ );
+ final direction = netValue >= BigInt.zero
+ ? TransactionDirection.incoming
+ : TransactionDirection.outgoing;
+ final memo = accountNotes
+ .map((final n) => n.memo)
+ .whereType<String>()
+ .where((final m) => m.isNotEmpty)
+ .firstOrNull;
+ final recipientAddresses = _recipientAddresses(
+ accountNotes.map((final n) => n.address).whereType<String>(),
+ direction,
);
- return PendingZcashTransaction(
- zcashWallet: this as ZcashWallet,
- credentials: creds,
- txPlan: txPlan,
- fee: Money.fromInt(internalCalculateEstimatedFee(creds.priority, null), currency),
- availableBalance: availableBalance,
+ final info = ZcashTransactionInfo(
+ id: ZcashWalletService.normalizeTxId(tx.txid),
+ amount: Money(netValue.abs(), currency),
+ fee: Money.zero(currency),
+ direction: direction,
+ isPending: true,
+ date: DateTime.now(),
+ height: 0,
+ confirmations: 0,
+ to: recipientAddresses.isEmpty ? '' : recipientAddresses.first,
+ memo: memo,
);
+ if (recipientAddresses.isNotEmpty) {
+ info.outputAddresses = recipientAddresses;
+ }
+ return info;
}
- static const _dispPhrase = "Received to disposable address";
- Future<List<ShieldedTx>> getShieldTxForUi() async {
- final tx = (ZcashTaddressRotation.shieldedAccountsTx[accountId] ?? <ShieldedTx>[])
- .map((final v) {
- final unpacked = v.unpack();
- unpacked.memo ??= "";
- unpacked.memo = "${unpacked.memo}\n$_dispPhrase".trim();
- final List<int> buff = base64.decode(
- ZcashTaddressRotation.flatBuffersPack(unpacked.pack),
- );
- return ShieldedTx(buff);
- })
- .where((final t) => t.value > 0);
+ ZcashTransactionInfo _zcashInfoFromZkoolTx(
+ final ZkoolTx tx,
+ final int currentHeight, {
+ final String? extraMemo,
+ final bool isRotationReceive = false,
+ final bool isShieldAction = false,
+ final TransactionDirection? directionOverride,
+ final BigInt? amountOverride,
+ }) {
+ final confirmations = tx.height > 0 && currentHeight >= tx.height
+ ? currentHeight - tx.height + 1
+ : 0;
+ final memo = extraMemo != null ? "${tx.memo ?? ''}\n$extraMemo".trim() : tx.memo;
+ final direction = directionOverride ?? tx.direction;
+ final recipientAddresses = _recipientAddresses(_paymentOutputAddresses(tx), direction);
+ final info = ZcashTransactionInfo(
+ id: tx.txHash,
+ amount: Money(amountOverride ?? tx.value, currency),
+ fee: Money.zero(currency),
+ direction: direction,
+ isPending: tx.height == 0,
+ date: tx.time,
+ height: tx.height,
+ confirmations: confirmations,
+ to: recipientAddresses.isEmpty ? '' : recipientAddresses.first,
+ memo: memo?.isNotEmpty == true ? memo : null,
+ txType: tx.type,
+ isRotationReceive: isRotationReceive,
+ isShieldAction: isShieldAction,
+ );
+ if (recipientAddresses.isNotEmpty) {
+ info.outputAddresses = recipientAddresses;
+ }
+ return info;
+ }
- return tx.toList();
+ bool _isShieldActionTx(final ZkoolTx tx, {required final Set<String> rotationSweepHashes}) {
+ if (ZcashWalletService.isAutoshieldTx(tx.txHash)) {
+ return true;
+ }
+ if (rotationSweepHashes.contains(tx.txHash)) {
+ return true;
+ }
+ if (_isPayToSelfAutoshield(tx)) {
+ return true;
+ }
+ if (tx.direction == TransactionDirection.outgoing &&
+ (tx.type == TxType.shield || tx.type == TxType.transparentSelfTransfer)) {
+ return true;
+ }
+ return false;
}
- static Map<int, List<ShieldedTx>> temporarySentTx = {};
+ bool _isPayToSelfAutoshield(final ZkoolTx tx) {
+ if (tx.type != TxType.shield && tx.type != TxType.transparentSelfTransfer) {
+ return false;
+ }
+ if (tx.transparentOrSaplingSpent <= BigInt.zero) {
+ return false;
+ }
+ if (tx.orchardReceived <= BigInt.zero) {
+ return false;
+ }
+ for (final dest in tx.outputAddresses) {
+ if (_addressBelongsToWallet(dest)) {
+ return true;
+ }
+ }
+ return tx.orchardReceived > BigInt.zero;
+ }
- static String txChecksumKey(final ShieldedTx tx) {
- final direction = tx.value > 0 ? TransactionDirection.incoming : TransactionDirection.outgoing;
- return 'tx${direction}_${tx.id}_${tx.timestamp}_${CRC32.compute(tx.toString())}';
+ bool _shouldSplitAutoshieldTx(final ZkoolTx tx, {required final bool isShield}) {
+ if (!isShield) {
+ return false;
+ }
+ if (ZcashWalletService.isAutoshieldTx(tx.txHash) || _isPayToSelfAutoshield(tx)) {
+ return tx.transparentOrSaplingSpent > BigInt.zero && tx.orchardReceived > BigInt.zero;
+ }
+ return false;
}
- @override
- Future<Map<String, ZcashTransactionInfo>> fetchTransactions() async {
- await ZcashWalletService.loadShieldTxs();
- final txs = (await ZcashWalletService.runInDbMutex(
- () => WarpApi.getTxs(coin, accountId),
- )).toList();
- // ShieldedTx{id: 26, txId: 4d1be06ce2c2debec8d98ce4e9434c8aac27c980488b459017d423fdcab37f93, height: 3195705, shortTxId: 4d1be06c, timestamp: 1767730944, name: null, value: 1000000, address: null, memo: , messages: MemoVec{memos: null}}
+ static String _txResultKey(final String txHash, {final String suffix = ''}) =>
+ 'tx_$txHash$suffix';
- final shieldTx = await getShieldTxForUi();
+ static int _txDisplayPriority(final ZcashTransactionInfo info) {
+ if (info.additionalInfo['isAutoShield'] == true) {
+ return 3;
+ }
+ if (info.additionalInfo['isRotationReceive'] == true) {
+ return 2;
+ }
+ return 1;
+ }
- txs.addAll(shieldTx);
- final txIds = txs.map((final tx) => tx.txId!.replaceAll('"', '')).toSet();
- temporarySentTx[accountId]?.removeWhere(
- (final ttx) => txIds.contains(ttx.txId!.replaceAll('"', '')),
- );
- txs.addAll(temporarySentTx[accountId] ?? []);
+ void _offerTx(final Map<String, ZcashTransactionInfo> byHash, final ZcashTransactionInfo info) {
+ final hash = info.txHash;
+ final existing = byHash[hash];
+ if (existing == null) {
+ byHash[hash] = info;
+ return;
+ }
+ final infoPriority = _txDisplayPriority(info);
+ final existingPriority = _txDisplayPriority(existing);
+ if (infoPriority > existingPriority) {
+ byHash[hash] = info;
+ return;
+ }
+ if (infoPriority == existingPriority &&
+ info.additionalInfo['isAutoShield'] == true &&
+ info.direction == TransactionDirection.outgoing &&
+ existing.direction == TransactionDirection.incoming) {
+ byHash[hash] = info;
+ }
+ }
- txs.sort((final a, final b) => a.height.compareTo(b.height));
- final Map<String, ZcashTransactionInfo> result = {};
- int currentHeight = 0;
- try {
- currentHeight = await WarpApi.getLatestHeight(coin);
- } catch (e) {
- printV("Error getting latest height: $e");
+ bool _addressBelongsToWallet(final String addr) {
+ final addrs = walletAddresses;
+ if (addrs.containsAddress(addr) ||
+ addrs.hiddenAddresses.contains(addr) ||
+ addrs.usedAddresses.contains(addr)) {
+ return true;
+ }
+ for (final infos in addrs.addressInfos.values) {
+ for (final info in infos) {
+ if (info.address == addr) {
+ return true;
+ }
+ }
+ }
+ for (final own in [
+ addrs.orchardAddress,
+ addrs.unifiedAddress,
+ addrs.saplingAddress,
+ addrs.transparentAddress,
+ addrs.address,
+ ]) {
+ if (own == null || own.isEmpty || own.startsWith('unknown ')) {
+ continue;
+ }
+ if (addr == own || addr.startsWith(own) || own.startsWith(addr)) {
+ return true;
+ }
}
+ return false;
+ }
- for (final tx in txs) {
- final direction = tx.value > 0
- ? TransactionDirection.incoming
- : TransactionDirection.outgoing;
-
- final confirmations = tx.height > 0 && currentHeight > 0 ? currentHeight - tx.height + 1 : 0;
-
- final txChecksum = txChecksumKey(tx);
- final txId = tx.txId ?? tx.shortTxId ?? txChecksum;
-
- final txInfo = ZcashTransactionInfo(
- id: txId.trim().replaceAll('"', ''),
- amount: Money.fromInt(tx.value.abs(), currency),
- fee: Money.zero(currency),
- direction: direction,
- isPending: tx.height == 0,
- date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000),
- height: tx.height,
- confirmations: confirmations,
- to: tx.address ?? '',
- memo: tx.memo,
- );
- // if (txInfo.additionalInfo['autoShield'] == true) {
- // continue;
- // }
- result[txChecksum] = txInfo;
+ List<String> _paymentOutputAddresses(final ZkoolTx tx) {
+ final all = tx.outputsWithAddress.toList();
+ final external = all.where((final o) => !_addressBelongsToWallet(o.address)).toList();
+ final outputs = external.isNotEmpty ? external : all;
+ final transparent = outputs.where((final o) => o.pool == NotePool.transparent.index).toList();
+ if (transparent.length >= 2) {
+ return transparent.map((final o) => o.address).toList();
+ }
+ return outputs.map((final o) => o.address).toList();
+ }
+
+ List<String> _recipientAddresses(
+ final Iterable<String> raw,
+ final TransactionDirection direction,
+ ) {
+ final seen = <String>{};
+ final addresses = [
+ for (final address in raw)
+ if (address.trim().isNotEmpty && seen.add(address.trim())) address.trim(),
+ ];
+ if (addresses.isEmpty) {
+ return [];
}
+ final external = direction == TransactionDirection.incoming
+ ? addresses
+ : [
+ for (final address in addresses)
+ if (!_addressBelongsToWallet(address)) address,
+ ];
+ final list = external.isNotEmpty ? external : addresses;
+ if (list.isEmpty) {
+ return [];
+ }
+
+ final embedded = {
+ for (final address in list)
+ if (address.startsWith('u')) ..._uaReceivers(address),
+ };
+ var result = embedded.isEmpty
+ ? list
+ : [
+ for (final address in list)
+ if (address.startsWith('u') || !embedded.contains(address)) address,
+ ];
+ if (result.isEmpty) {
+ result = list;
+ }
+
+ final standalone = [
+ for (final address in result)
+ if (!address.startsWith('u') && !embedded.contains(address)) address,
+ ];
+ if (standalone.length >= 2) {
+ result = standalone;
+ }
+
+ if (direction == TransactionDirection.incoming) {
+ return [
+ result.firstWhere(
+ (final a) => a.startsWith('u'),
+ orElse: () =>
+ result.firstWhere((final a) => a.startsWith('z'), orElse: () => result.first),
+ ),
+ ];
+ }
return result;
}
+ Set<String> _uaReceivers(final String ua) {
+ try {
+ final receivers = zkool_account.receiversFromUa(ua: ua, c: ZcashWalletBase.c);
+ return {
+ for (final address in [receivers.taddr, receivers.saddr, receivers.oaddr])
+ if (address != null && address.isNotEmpty) address,
+ };
+ } catch (_) {
+ return {};
+ }
+ }
+
@override
- Object get keys {
- final backup = WarpApi.getBackup(coin, accountId);
+ Future<Map<String, ZcashTransactionInfo>> fetchTransactions() async {
+ await ZcashWalletService.loadShieldTxs();
+ final (txs, currentHeight) = await runWithCoin(
+ accountId: accountId,
+ func: (coin) async {
+ final txsI = await zkool_account.listTxHistory(c: coin);
+ final txsA = await Future.wait(
+ txsI.map((final tx) => zkool_account.getTxDetails(idTx: tx.id, c: coin)),
+ );
+ final txs = <ZkoolTx>[];
+ for (int i = 0; i < txsI.length; i++) {
+ txs.add(ZkoolTx(txsI[i], txsA[i]));
+ }
+ txs.sort((final a, final b) => a.height.compareTo(b.height));
+ var currentHeight = 1;
+ try {
+ currentHeight = await zkool_network.getCurrentHeight(c: coin);
+ } catch (e) {
+ printV("failed to get height: $e");
+ }
+ return (txs, currentHeight);
+ },
+ );
+ final Map<String, ZcashTransactionInfo> byHash = {};
+ final rotationTxs = ZcashTaddressRotation.rotationTxsForMainAccount(accountId);
+ final rotationSweepHashes = <String>{
+ for (final tx in rotationTxs)
+ if (tx.direction == TransactionDirection.outgoing) tx.txHash,
+ };
+
+ for (final tx in rotationTxs) {
+ if (tx.direction == TransactionDirection.incoming) {
+ _offerTx(
+ byHash,
+ _zcashInfoFromZkoolTx(tx, currentHeight, extraMemo: _dispPhrase, isRotationReceive: true),
+ );
+ continue;
+ }
+ _offerTx(byHash, _zcashInfoFromZkoolTx(tx, currentHeight, isShieldAction: true));
+ }
+
+ final Map<String, ZcashTransactionInfo> splitEntries = {};
+ for (final tx in txs) {
+ final isShield = _isShieldActionTx(tx, rotationSweepHashes: rotationSweepHashes);
+ if (_shouldSplitAutoshieldTx(tx, isShield: isShield)) {
+ byHash.remove(tx.txHash);
+ splitEntries[_txResultKey(tx.txHash, suffix: '_shield')] = _zcashInfoFromZkoolTx(
+ tx,
+ currentHeight,
+ isShieldAction: true,
+ directionOverride: TransactionDirection.outgoing,
+ amountOverride: tx.transparentOrSaplingSpent,
+ );
+ splitEntries[_txResultKey(tx.txHash, suffix: '_recv')] = _zcashInfoFromZkoolTx(
+ tx,
+ currentHeight,
+ directionOverride: TransactionDirection.incoming,
+ amountOverride: tx.orchardReceived,
+ );
+ continue;
+ }
+ _offerTx(byHash, _zcashInfoFromZkoolTx(tx, currentHeight, isShieldAction: isShield));
+ }
+
+ final knownHashes = {for (final tx in txs) tx.txHash, ...byHash.keys};
+ for (final mempoolTx in ZcashMempoolService.instance.txsForAccount(accountId)) {
+ final hash = ZcashWalletService.normalizeTxId(mempoolTx.txid);
+ if (knownHashes.contains(hash)) {
+ continue;
+ }
+ final info = _zcashInfoFromMempoolTx(mempoolTx, accountId);
+ if (info.amount.isZero) {
+ continue;
+ }
+ _offerTx(byHash, info);
+ }
+
return {
- // "seed": backup.seed,
- // "index": backup.index,
- "privateSpendKey": backup.sk,
- "privateViewKey": backup.fvk,
- "uvk": backup.uvk,
- "tsk": backup.tsk,
- if (lastKnownRestoreHeight != null) "restoreHeight": lastKnownRestoreHeight.toString(),
+ for (final entry in byHash.entries) _txResultKey(entry.key): entry.value,
+ ...splitEntries,
};
}
+ Future<void> _initKeys() async {
+ try {
+ c = await c.setAccount(account: accountId);
+ final ufvk = await zkool_account.getAccountUfvk(account: accountId, c: c, pools: 7);
+
+ keys = {
+ "privateViewKey": ufvk,
+ if (lastKnownRestoreHeight != null) "restoreHeight": lastKnownRestoreHeight.toString(),
+ };
+ } catch (e) {
+ keys = {"privateViewKey": e.toString()};
+ }
+ try {
+ c = await c.setAccount(account: accountId);
+ final s = (await zkool_account.getAccountSeed(account: accountId, c: c));
+
+ if (s == null) {
+ throw Exception("seed not found");
+ }
+ final seedPhrase = s.mnemonic.split(" ");
+ if ([13, 25].contains(seedPhrase.length)) {
+ passphrase = seedPhrase.removeLast();
+ } else {
+ passphrase = s.phrase;
+ }
+ seed = s.mnemonic.trim();
+ } catch (e) {
+ seed = e.toString();
+ }
+ }
+
+ @override
+ Object keys = {};
+
@override
String get password => _password!;
@override
Future<void> renameWalletFiles(final String newWalletName) async {
- final currentWalletPath = await pathForWallet(name: name, type: type);
- final currentCacheFile = File(currentWalletPath);
- final newWalletPath = await pathForWallet(name: newWalletName, type: type);
- if (currentCacheFile.existsSync()) {
- await currentCacheFile.copy(newWalletPath);
+ await renameWalletFilesForName(fromName: name, toName: newWalletName);
+ }
+
+ static Future<void> renameWalletFilesForName({
+ required final String fromName,
+ required final String toName,
+ }) async {
+ if (fromName == toName) {
+ return;
+ }
+ final currentWalletDir = Directory(await pathForWalletDir(name: fromName, type: _type));
+ if (!currentWalletDir.existsSync()) {
+ throw Exception('Wallet directory not found: $fromName');
+ }
+ final newWalletDirPath = '${await pathForWalletTypeDir(type: _type)}/$toName';
+ if (Directory(newWalletDirPath).existsSync()) {
+ throw Exception('Cannot rename wallet: "$toName" already exists');
+ }
+ await currentWalletDir.rename(newWalletDirPath);
+ for (final suffix in const ['', '.v2']) {
+ final oldFile = File('$newWalletDirPath/$fromName$suffix');
+ if (oldFile.existsSync()) {
+ await oldFile.rename('$newWalletDirPath/$toName$suffix');
+ }
}
- Directory(currentWalletPath).deleteSync(recursive: true);
}
@override
bool get hasRescan => true;
- static Future<void> storeZcashHeight(final int height) async {
- lastKnownRestoreHeight = height;
- final zcashDir = await pathForWalletTypeDir(type: WalletType.zcash);
- final zcashInitialSync = File(p.join(zcashDir, ".initial-sync-marker"));
- zcashInitialSync.writeAsBytesSync([0x00]);
- zcashInitialSync.writeAsStringSync(height.toString(), mode: FileMode.writeOnlyAppend);
+ static int? lastKnownRestoreHeight = null;
+
+ static int zashiAnnouncedBlockHeight = 2419420;
+
+ Future<dynamic> _getAddressesForAccount(final int id) async {
+ return runWithCoin(
+ accountId: id,
+ func: (final coin) => zkool_account.getAddresses(c: coin, uaPools: 7),
+ );
}
- static int? lastKnownRestoreHeight = null;
- static Future<int?> loadZcashHeight() async {
- final zcashDir = await pathForWalletTypeDir(type: WalletType.zcash);
- final zcashInitialSync = File(p.join(zcashDir, ".initial-sync-marker"));
- if (!await zcashInitialSync.exists()) {
- return null;
- }
- final bytes = await zcashInitialSync.readAsBytes();
- if (bytes.isEmpty) {
- return null;
- }
- final heightString = String.fromCharCodes(bytes.skip(1));
- lastKnownRestoreHeight = int.tryParse(heightString);
- return lastKnownRestoreHeight;
+ bool _addressesMatch(final dynamic old, final dynamic new_) {
+ return old.ua == new_.ua &&
+ old.oaddr == new_.oaddr &&
+ old.saddr == new_.saddr &&
+ old.taddr == new_.taddr;
}
- static int zashiAnnouncedBlockHeight = 2419420;
+ Future<void> _switchToAccount(final int newAccountId, final int height) async {
+ walletsByAccountId.remove(accountId);
+ accountId = newAccountId;
+ walletsByAccountId[newAccountId] = this;
+ walletAddresses.accountId = newAccountId;
+ c = await c.setAccount(account: newAccountId);
+ lastKnownRestoreHeight = height;
+ await walletAddresses.init();
+ await _initKeys();
+ }
@override
@action
Future<void> rescan({required final int height}) async {
- try {
- syncStatus = StartingScanSyncStatus(height);
- printV("rescanning from: $height");
- await storeZcashHeight(height);
- await ZcashWalletService.runInDbMutex(() async => WarpApi.rescanFrom(coin, height));
- await startSync();
- } catch (e) {
- printV("Rescan error: $e");
- syncStatus = FailedSyncStatus(error: e.toString());
- }
+ await zkool_sync.rewindSync(height: height, account: accountId, c: c);
+ // try {
+ // syncStatus = StartingScanSyncStatus(height);
+ // printV("rescanning from: $height");
+ // await zkool_sync.cancelSync();
+ // isSyncing = false;
+
+ // await runWithCoinMutex.acquire();
+ // try {
+ // final oldAddresses = await _getAddressesForAccount(accountId);
+
+ // c = await c.setAccount(account: accountId);
+ // final accountSeed = await zkool_account.getAccountSeed(account: accountId, c: c);
+ // if (accountSeed == null) {
+ // throw Exception('Cannot rescan: seed not available');
+ // }
+
+ // final newAccountId = await restoreZcashWalletFromSeed(
+ // name: name,
+ // seed: accountSeed.mnemonic,
+ // passphrase: accountSeed.phrase,
+ // birthHeight: height,
+ // );
+
+ // final newAddresses = await _getAddressesForAccount(newAccountId);
+ // if (!_addressesMatch(oldAddresses, newAddresses)) {
+ // throw Exception('Rescan address verification failed');
+ // }
+
+ // await saveAccountId(name, newAccountId);
+ // await _switchToAccount(newAccountId, height);
+ // } finally {
+ // runWithCoinMutex.release();
+ // }
+
+ // syncStatus = ConnectedSyncStatus();
+ // } catch (e) {
+ // printV("Rescan error: $e");
+ // syncStatus = FailedSyncStatus(error: e.toString());
+ // rethrow;
+ // }
}
bool _isTransactionUpdating = false;
@@ -417,56 +1027,31 @@ abstract class ZcashWalletBase
}
}
- Future<void> updateTransactionsHistory() => updateTransactions();
-
@override
Future<void> save() async {}
Future<void> init() async {
try {
+ await ZcashTaddressRotation.init();
await walletAddresses.init();
await updateBalance();
await updateTransactions();
+ unawaited(
+ ZcashTaddressRotation.updateCache(mainAccountId: accountId)
+ .catchError((final e) => printV("rotation cache refresh: $e")),
+ );
+ await _initKeys();
} catch (e) {
printV("Wallet init error: $e");
}
}
@override
- String? get seed {
- try {
- final backup = WarpApi.getBackup(coin, accountId);
- final seed = backup.seed?.split(" ");
- if (seed == null) {
- return null;
- }
- if ([13, 25].contains(seed.length)) {
- seed.removeLast();
- }
- return seed.join(" ").trim();
- } catch (e) {
- return null;
- }
- }
+ String? seed = "";
@override
- String? get passphrase {
- try {
- final backup = WarpApi.getBackup(coin, accountId);
- final seed = backup.seed?.split(" ");
- if (seed == null) {
- return null;
- }
- if ([13, 25].contains(seed.length)) {
- final passphrase = seed.removeLast();
- return passphrase;
- }
- return null;
- } catch (e) {
- return null;
- }
- }
+ String? passphrase = "";
@override
Future<String> signMessage(final String message, {final String? address = null}) {
@@ -476,328 +1061,129 @@ abstract class ZcashWalletBase
@override
@action
Future<void> startSync() async {
+ if (syncStatus is AttemptingSyncStatus ||
+ syncStatus is SyncronizingSyncStatus ||
+ syncStatus is SyncingSyncStatus) {
+ return;
+ }
try {
- syncStatus = AttemptingSyncStatus();
-
- _initialSyncHeight = 0;
- _lastKnownBlockHeight = 0;
-
- _startSyncStatusUpdates();
-
- syncStatus = SyncronizingSyncStatus();
-
- unawaited(
- _runWarpSync().catchError((final e) {
- isNodeWorking = false;
- printV("WarpSync error in startSync: $e");
- syncStatus = FailedSyncStatus(error: e.toString());
- _stopSyncStatusUpdates();
- }),
- );
+ _ensureSyncLoopRunning();
+ unawaited(_oneshotSync());
} catch (e) {
isNodeWorking = false;
printV("Sync error: $e");
syncStatus = FailedSyncStatus(error: e.toString());
- _stopSyncStatusUpdates();
rethrow;
}
}
static Mutex warpSyncMutex = Mutex();
- static Future<void> initialSyncCheck() async {
- final zcashDir = await pathForWalletTypeDir(type: WalletType.zcash);
- final zcashInitialSync = File(p.join(zcashDir, ".initial-sync-marker"));
- if (!zcashInitialSync.existsSync()) {
- int chainHeight = 3000000; // fallback if node is offline
- try {
- chainHeight = await WarpApi.getLatestHeight(coin);
- } catch (e) {
- printV("Error getting latest height: $e");
- }
- await ZcashWalletService.runInDbMutex(
- () async => await WarpApi.rescanFrom(coin, chainHeight - 150000),
- );
- await storeZcashHeight(chainHeight);
- }
- }
-
- @action
- Future<void> _runWarpSync() async {
- Timer? _t;
- try {
- await warpSyncMutex.acquire();
- await initialSyncCheck();
- isNodeWorking = true;
- printV("Starting warpSync for coin $coin, account $accountId");
- int? initialQueue = null;
- void _cancelSyncIfShould(final Timer t) {
- initialQueue ??= ZcashWalletService.dbMutexQueue + 2;
- if (ZcashWalletService.dbMutexQueue <= initialQueue!) {
- initialQueue = ZcashWalletService.dbMutexQueue;
- return;
- }
- printV(
- "Canceling sync! (ZcashWalletService.dbMutexQueue: ${ZcashWalletService.dbMutexQueue} > initialQueue: ${initialQueue})",
- );
- WarpApi.cancelSync();
- t.cancel();
- _t = null;
- }
-
- unawaited(
- Future.delayed(Duration(seconds: 2)).then((_) {
- _t = Timer.periodic(Duration(milliseconds: 100), _cancelSyncIfShould);
- }),
- );
- final result = await ZcashWalletService.runInDbMutex(
- () => WarpApi.warpSync(coin, accountId, true, 0, 1000000, 0),
- );
- printV("warpSync completed with result: $result");
-
- await _updateSyncStatus();
- } catch (e) {
- syncStatus = FailedSyncStatus(error: e.toString());
- unawaited(Future.delayed(Duration(seconds: 1)).then((_) => unawaited(_runWarpSync())));
- _stopSyncStatusUpdates();
- } finally {
- isNodeWorking = false;
- warpSyncMutex.release();
- _t?.cancel();
- }
- }
-
- void _startSyncStatusUpdates() {
- _stopSyncStatusUpdates();
- _updateSyncStatus();
- _syncStatusTimer = Timer.periodic(const Duration(milliseconds: 5000), (_) {
- _updateSyncStatus().catchError((final e) {
- printV("Error in sync status update timer: $e");
- });
- });
- }
-
- void _stopSyncStatusUpdates() {
- _syncStatusTimer?.cancel();
- _syncStatusTimer = null;
- }
-
- void _startPeriodicSync() {
- printV("_startPeriodicSync");
- _stopPeriodicSync();
- _periodicSyncTimer = Timer.periodic(const Duration(seconds: 5), (_) async {
- try {
- final chainHeight = await WarpApi.getLatestHeight(coin);
- final dbHeight = WarpApi.getDbHeight(coin);
- final height = dbHeight.unpack();
- final syncHeight = height.height;
-
- if (syncHeight < chainHeight) {
- printV("Periodic sync: chainHeight=$chainHeight, syncHeight=$syncHeight, starting sync");
- await _runWarpSync();
- } else {
- await updateBalance();
- await updateTransactions();
- }
- } catch (e) {
- printV("Periodic sync error: $e");
- }
- });
- }
-
- void _stopPeriodicSync() {
- _periodicSyncTimer?.cancel();
- _periodicSyncTimer = null;
- }
-
- @action
- Future<void> _updateSyncStatus() async {
- try {
- final dbHeight = WarpApi.getDbHeight(coin);
- final height = dbHeight.unpack();
- final syncHeight = height.height;
-
- final chainHeight = await WarpApi.getLatestHeight(coin);
-
- if (_initialSyncHeight <= 0 && syncHeight > 0) {
- _initialSyncHeight = syncHeight;
- printV("Initialized sync height to: $_initialSyncHeight");
- if (syncHeight - 10 > dbHeight.height) {}
- }
-
- if (chainHeight <= 0) {
- if (syncStatus is! ConnectedSyncStatus && syncStatus is! ConnectingSyncStatus) {
- syncStatus = ConnectedSyncStatus();
- }
- try {
- await updateBalance();
- await updateTransactions();
- } catch (e) {
- printV("Error updating balance/transactions: $e");
- }
- return;
- }
-
- if (syncHeight <= 0) {
- if (syncStatus is! ConnectedSyncStatus &&
- syncStatus is! ConnectingSyncStatus &&
- syncStatus is! AttemptingSyncStatus) {
- syncStatus = ConnectedSyncStatus();
- }
- try {
- await updateBalance();
- await updateTransactions();
- } catch (e) {
- printV("Error updating balance/transactions: $e");
- }
- return;
- }
-
- if (syncHeight >= chainHeight && syncHeight > 0) {
- syncStatus = SyncedSyncStatus();
- _stopSyncStatusUpdates();
- await updateBalance();
- await updateTransactions();
- _startPeriodicSync();
- return;
- }
-
- if (_lastKnownBlockHeight != syncHeight) {
- _lastKnownBlockHeight = syncHeight;
- }
-
- if (syncHeight < 0 || chainHeight < syncHeight) {
- return;
- }
-
- final blocksLeft = chainHeight - syncHeight;
- if (blocksLeft <= 0) {
- syncStatus = SyncedSyncStatus();
- _stopSyncStatusUpdates();
- await updateBalance();
- await updateTransactions();
- _startPeriodicSync();
- return;
- }
-
- double ptc = 0.0;
- if (_initialSyncHeight > 0) {
- final track = chainHeight - _initialSyncHeight;
- final diff = track > 0 ? track - (chainHeight - syncHeight) : 0;
- ptc = track > 0 && diff >= 0 ? diff / track : 0.0;
- } else {
- ptc = syncHeight / chainHeight;
- }
-
- syncStatus = SyncingSyncStatus(blocksLeft, ptc.clamp(0.0, 1.0));
-
- await updateBalance();
- await updateTransactions();
- } catch (e) {
- printV("Sync status update error: $e");
- }
- }
-
- String getDiversifiedAddress(final int uaType, {final DateTime? time}) {
- try {
- final timestamp = (time ?? DateTime.now()).millisecondsSinceEpoch ~/ 1000;
- return WarpApi.getDiversifiedAddress(coin, accountId, uaType, timestamp);
- } catch (e) {
- printV("Error getting diversified address: $e");
- return "";
- }
- }
-
static final autoShieldMutex = Mutex();
+ static DateTime? _lastAutoShieldAt;
Future<void> _autoShield() async {
+ if (_lastAutoShieldAt != null &&
+ _lastAutoShieldAt!.isAfter(DateTime.now().subtract(const Duration(seconds: 75)))) {
+ return;
+ }
try {
await autoShieldMutex.acquire();
await _$autoShield();
- } catch (e) {
+ } catch (e, s) {
printV("shielding failed: $e");
- await Future.delayed(Duration(seconds: 30));
+ s.toString().split("\n").forEach(printV);
} finally {
autoShieldMutex.release();
}
}
Future<void> _$autoShield() async {
- final chainHeight = await WarpApi.getLatestHeight(coin);
- final dbHeight = WarpApi.getDbHeight(coin);
- final height = dbHeight.unpack();
- final syncHeight = height.height;
- if (chainHeight != syncHeight) {
- printV("Not autoshielding: chainHeight(${chainHeight}) != syncHeight(${syncHeight})");
- return;
- }
- final bpConfirmed = WarpApi.getPoolBalances(coin, accountId, 0, false);
- if (bpConfirmed.transparent + bpConfirmed.sapling <= 20000) {
+ if (syncStatus is! SyncedSyncStatus) {
return;
}
+ final txId = await runWithCoin(
+ accountId: accountId,
+ func: (coin) async {
+ final _notes = await zkool_account.listNotes(c: coin);
+ final List<zkool_account.TxNote> txNotes = [];
+ for (int i = 0; i < _notes.length; i++) {
+ final note = _notes[i];
+ final noteType = NotePool.values[note.pool];
+ if ([NotePool.sapling, NotePool.transparent].contains(noteType)) {
+ txNotes.add(note);
+ }
+ }
- final recipientBuilder = RecipientObjectBuilder(
- address: (walletAddresses as ZcashWalletAddresses).orchardAddress,
- pools: 4,
- feeIncluded: true,
- amount: bpConfirmed.transparent + bpConfirmed.sapling,
- );
+ final sweepable = txNotes.isEmpty
+ ? BigInt.from(0)
+ : txNotes.map((final txn) => txn.value).reduce((final a, final b) => a + b);
- final recipient = Recipient(recipientBuilder.toBytes());
- final fee = FeeT(fee: 10000, minFee: 0, maxFee: 0, scheme: 0);
- await ZcashWalletBase.loadProver();
- final txPlan = await ZcashWalletService.runInDbMutex(
- () => WarpApi.prepareTx(
- coin,
- accountId,
- [recipient],
- 3, // pools: (Transparent + Sapling)
- 1,
- 0, // anchorOffset
- fee,
- ),
- );
- final _txId = await ZcashWalletService.runInDbMutex(
- () => WarpApi.signAndBroadcast(ZcashWalletBase.coin, accountId, txPlan),
+ if (sweepable <= BigInt.from(_autoShieldMinSweep)) {
+ return null;
+ }
+
+ final txPlan = await zkool_pay.prepare(
+ recipients: [
+ zkool_paydart.Recipient(
+ assetBase: zecBase,
+ address: walletAddresses.orchardAddress!,
+ amount: sweepable,
+ ),
+ ],
+ options: zkool_pay.PaymentOptions(
+ srcPools: 3,
+ recipientPaysFee: true,
+ smartTransparent: false,
+ ),
+ c: coin,
+ );
+
+ final signTx = await zkool_pay.signTransaction(pczt: txPlan, c: coin);
+ final txBytes = await zkool_pay.extractTransaction(package: signTx);
+ final currentHeight = await zkool_network.getCurrentHeight(c: coin);
+ return await zkool_pay.broadcastTransaction(
+ height: currentHeight,
+ txBytes: txBytes,
+ c: coin,
+ );
+ },
);
- await ZcashWalletService.addShieldedTx(_txId);
- printV("shielded: $_txId");
+ if (txId == null) {
+ return;
+ }
+
+ await ZcashWalletService.addShieldedTx(txId);
+ _lastAutoShieldAt = DateTime.now();
+ printV("shielded: $txId");
await updateTransactions();
- await updateBalance();
- await Future.delayed(Duration(seconds: 75 * 5)); // do not re-try doing that
+ await _refreshBalance(runAutoShield: false);
}
- @override
- @action
- Future<void> updateBalance() async {
+ Future<void> _refreshBalance({required final bool runAutoShield}) async {
try {
- final poolBalances = WarpApi.getPoolBalances(coin, accountId, 0, true);
- final balances = poolBalances.unpack();
- // final notes = WarpApi.getNotesSync(coin, accountId);
- // int frozenBalance = 0;
- // for (final note in notes) {
- // if (note.excluded) {
- // frozenBalance += note.value;
- // }
- // }
- final total = balances.orchard + balances.sapling + balances.transparent;
- final spendable = total - balances.transparent;
-
- final confirmedPoolBalances = WarpApi.getPoolBalances(coin, accountId, 3, true);
- final confirmedBalances = confirmedPoolBalances.unpack();
- final confirmedTotal =
- confirmedBalances.orchard + confirmedBalances.sapling + confirmedBalances.transparent;
-
- int knownOutPending = 0;
- ZcashWalletBase.temporarySentTx[accountId]?.forEach((final sTx) {
- knownOutPending += sTx.value; // it's negative
- });
- final confirmedSpendable = confirmedTotal - balances.transparent + knownOutPending;
+ final bal = await runWithCoin(
+ accountId: accountId,
+ func: (coin) => zkool_sync.balance(c: coin),
+ );
- unawaited(_autoShield());
+ // 0 - transparent
+ // 1 - sapling
+ // 2 - orchard
+ final confirmedTotal = bal.field0.reduce((final a, final b) => a + b);
+
+ // int knownOutPending = 0;
+ // ZcashWalletBase.temporarySentTx[accountId]?.forEach((final sTx) {
+ // knownOutPending += sTx.value; // it's negative
+ // });
+ final confirmedSpendable = confirmedTotal - bal.field0[0];
+
+ if (runAutoShield) {
+ await _autoShield();
+ }
balance[CryptoCurrency.zec] = ZcashBalance(
- Money.fromInt(confirmedSpendable, currency),
- Money.fromInt(spendable - confirmedSpendable, currency),
+ Money(confirmedSpendable, currency),
+ Money(confirmedTotal - confirmedSpendable, currency),
frozen: Money.zero(currency),
);
} catch (e, stackTrace) {
@@ -806,6 +1192,12 @@ abstract class ZcashWalletBase
}
}
+ @override
+ @action
+ Future<void> updateBalance() async {
+ await _refreshBalance(runAutoShield: true);
+ }
+
@override
Future<bool> verifyMessage(
final String message,
@@ -815,11 +1207,11 @@ abstract class ZcashWalletBase
throw UnimplementedError();
}
- @observable
- late WalletAddresses walletAddresses = ZcashWalletAddresses(accountId, walletInfo);
+ @override
+ late ZcashWalletAddresses walletAddresses = ZcashWalletAddresses(accountId, walletInfo);
static Future<ZcashWallet> create(final WalletCredentials credentials) async {
- await _init();
+ await $init();
final newWalletCredentials = credentials as ZcashNewWalletCredentials;
String mnemonic;
@@ -830,57 +1222,50 @@ abstract class ZcashWalletBase
mnemonic = bip39.generateMnemonic(strength: strength);
}
- final accountId = await _restoreZcashWalletFromSeed(
+ final birthHeight = await ZcashHeight.getBlockHeightByTime(DateTime.now());
+
+ final accountId = await restoreZcashWalletFromSeed(
name: credentials.name,
seed: mnemonic,
passphrase: newWalletCredentials.passphrase,
+ birthHeight: birthHeight,
);
- await _saveAccountId(credentials.name, accountId);
+ await saveAccountId(credentials.name, accountId);
final wallet = await open(
name: credentials.name,
password: credentials.password!,
walletInfo: credentials.walletInfo!,
);
- await wallet.walletAddresses.saveAddressesInBox();
+ await wallet.init();
return wallet;
}
static Future<ZcashWallet> restore(final WalletCredentials credentials) async {
- await _init();
+ await $init();
final fromSeedCredentials = credentials as ZcashFromSeedWalletCredentials;
final String? seed = fromSeedCredentials.seed;
if (seed == null || seed.isEmpty) {
throw Exception('Seed phrase is required for wallet restoration');
}
- final accountId = await _restoreZcashWalletFromSeed(
+ final accountId = await restoreZcashWalletFromSeed(
name: credentials.name,
seed: seed,
passphrase: fromSeedCredentials.passphrase,
+ birthHeight: credentials.height!,
);
- await _saveAccountId(credentials.name, accountId);
+ await saveAccountId(credentials.name, accountId);
final wallet = await open(
name: credentials.name,
password: credentials.password!,
walletInfo: credentials.walletInfo!,
);
- await wallet.walletAddresses.saveAddressesInBox();
- printV("height: ${credentials.height}");
- if (credentials.height != null) {
- await storeZcashHeight(credentials.height!);
- unawaited(
- Future.delayed(Duration(seconds: 2)).then(
- (_) => ZcashWalletService.runInDbMutex(
- () async => await WarpApi.rescanFrom(coin, credentials.height ?? 0),
- ),
- ),
- );
- }
+ await wallet.init();
return wallet;
}
static Future<ZcashWallet> restoreKeys(final WalletCredentials credentials) async {
- await _init();
+ await $init();
final fromKeysCredentials = credentials as ZcashFromKeysWalletCredentials;
final String? keys = fromKeysCredentials.privateKey;
if (keys == null || keys.isEmpty) {
@@ -892,29 +1277,20 @@ abstract class ZcashWalletBase
throw Exception('Key is not in secret-extended-key-main1 format');
}
- final accountId = await _restoreZcashWalletFromSeed(
+ final accountId = await restoreZcashWalletFromSeed(
name: credentials.name,
seed: keys,
passphrase: fromKeysCredentials.passphrase,
+ birthHeight: credentials.height!,
);
- await _saveAccountId(credentials.name, accountId);
+ await saveAccountId(credentials.name, accountId);
final wallet = await open(
name: credentials.name,
password: credentials.password!,
walletInfo: credentials.walletInfo!,
);
- await wallet.walletAddresses.saveAddressesInBox();
+ await wallet.init();
printV("height: ${credentials.height}");
- if (credentials.height != null) {
- await storeZcashHeight(credentials.height!);
- unawaited(
- Future.delayed(Duration(seconds: 2)).then(
- (_) => ZcashWalletService.runInDbMutex(
- () async => await WarpApi.rescanFrom(coin, credentials.height ?? 0),
- ),
- ),
- );
- }
return wallet;
}
@@ -923,56 +1299,80 @@ abstract class ZcashWalletBase
required final String password,
required final WalletInfo walletInfo,
}) async {
- await _init();
- if (password.isNotEmpty) {
- WarpApi.setDbPasswd(coin, password);
- }
+ await $init();
+ // if (password.isNotEmpty) {
+ // setDbPasswd(coin, password);
+ // }
final accountId = await getZcashAccountIdForName(name);
if (accountId == null) {
- throw Exception("Wallet account not found for name: $name");
+ throw Exception("accountId is null");
}
+ c = await c.setAccount(account: accountId);
final wallet = ZcashWallet(
walletInfo,
await walletInfo.getDerivationInfo(),
accountId: accountId,
);
- await wallet.walletAddresses.init();
+ await wallet._initKeys();
return wallet;
}
- static Future<int> _restoreZcashWalletFromSeed({
+ static Future<int> restoreZcashWalletFromSeed({
required final String name,
- required String seed,
- required String? passphrase,
+ required final String seed,
+ required final String? passphrase,
+ required final int birthHeight,
}) async {
- if (passphrase?.isNotEmpty == true) {
- passphrase = passphrase!.replaceAll(" ", "_");
- seed = "${seed} ${passphrase}";
- }
- final accountId = await ZcashWalletService.runInDbMutex(
- () => WarpApi.newAccount(coin, name, seed, 0),
+ // if (passphrase?.isNotEmpty == true) {
+ // passphrase = passphrase!.replaceAll(" ", "_");
+ // seed = "${seed} ${passphrase}";
+ // }
+
+ final accountId = await newAccount(
+ name: name,
+ height: birthHeight,
+ seed: seed,
+ passphrase: passphrase ?? '',
);
return accountId;
}
+ static Future<int?> getLegacyZcashAccountIdForName(final String name) async {
+ final wPath = (await pathForWallet(name: name, type: _type));
+ final f = File(wPath);
+ if (!f.existsSync()) {
+ final accs = await zkool_account.listAccounts(c: c);
+ for (final acc in accs) {
+ if (acc.name == name) {
+ return acc.id;
+ }
+ }
+ }
+ final content = f.readAsStringSync();
+ return int.tryParse(content.trim());
+ }
+
static Future<int?> getZcashAccountIdForName(final String name) async {
- final wPath = await pathForWallet(name: name, type: _type);
+ final wPath = (await pathForWallet(name: name, type: _type)) + ".v2";
final f = File(wPath);
if (!f.existsSync()) {
- final accounts = WarpApi.getAccountList(coin);
- for (final account in accounts) {
- if (account.name == name) {
- return account.id;
+ final accs = await zkool_account.listAccounts(c: c);
+ for (final acc in accs) {
+ if (acc.name == name) {
+ return acc.id;
}
}
- return null;
}
final content = f.readAsStringSync();
return int.tryParse(content.trim());
}
- static Future<void> _saveAccountId(final String name, final int accountId) async {
- final wPath = await pathForWallet(name: name, type: _type);
+ static Future<void> saveAccountId(final String name, final int accountId) async {
+ final wPath = (await pathForWallet(name: name, type: _type)) + ".v2";
+ final dirName = Directory(wPath).parent.path;
+ if (!Directory(dirName).existsSync()) {
+ Directory(dirName).createSync(recursive: true);
+ }
final f = File(wPath);
f.writeAsStringSync(accountId.toString());
}
@@ -981,202 +1381,120 @@ abstract class ZcashWalletBase
static Future<String> getDbDataPath() async {
final pathForWalletType = await pathForWalletTypeDir(type: _type);
- final dbDataPath = "${pathForWalletType}/zec.db";
+ final dbDataPath = "${pathForWalletType}/zec.v2.db";
if (!Directory(pathForWalletType).existsSync()) {
Directory(pathForWalletType).createSync(recursive: true);
}
return dbDataPath;
}
- static Future<String> getTorDir() async {
- final pathForWalletType = await pathForWalletTypeDir(type: _type);
- final torPath = "${pathForWalletType}/tor";
- if (!Directory(torPath).existsSync()) {
- Directory(torPath).createSync(recursive: true);
- }
- return torPath;
- }
-
- static Future<String> getFsBlockCacheDir() async {
+ static Future<String> getDbDataPathLegacyYwallet() async {
final pathForWalletType = await pathForWalletTypeDir(type: _type);
- final fsBlockCacheDir = "${pathForWalletType}/blockCache";
+ final dbDataPath = "${pathForWalletType}/zec.db";
if (!Directory(pathForWalletType).existsSync()) {
Directory(pathForWalletType).createSync(recursive: true);
}
- return fsBlockCacheDir;
+ return dbDataPath;
}
- static String? dbDataPath;
static bool _initialized = false;
static void unlockDatabase(final String password) {
_password = password;
}
+ static var c = zkool_coin.Coin();
+
static String? _password;
- static Future<void> _init() async {
+ static Future<void> $init() async {
if (_initialized) return;
- dbDataPath = await getDbDataPath();
- printV("WarpApi.initWallet");
+ _initialized = true;
+ printV(r".$init()");
+ await zkool_frb.RustLib.init();
+ ZcashMempoolService.instance.onAccountsUpdated = (final accountIds) {
+ for (final accountId in accountIds) {
+ unawaited(refreshWalletForAccount(accountId));
+ }
+ };
+ final dbFile = File(await getDbDataPath());
+ final ywalletDbFile = File(await getDbDataPathLegacyYwallet());
+ await zkool_network.initDatadir(directory: dbFile.parent.path);
+ // c = await c.openDatabase(dbFilepath: dbFile.path, password: 'cw_zcash_migration');
+ c = await c.openDatabase(dbFilepath: dbFile.path, password: null);
+ printV("initWallet");
if (_password == null) {
throw Exception("Zcash wallet locked! Please contact support");
}
- if (!File(dbDataPath!).existsSync()) {
+ if (!dbFile.existsSync()) {
//TODO(mrcyjanek): copy-encrypt
}
- // coin+1 = ycash
- WarpApi.setDbPasswd(coin, '');
- WarpApi.setDbPasswd(coin + 1, '');
- WarpApi.initWallet(coin, dbDataPath!);
- WarpApi.initWallet(coin + 1, dbDataPath!);
- try {
- WarpApi.migrateData(coin);
- WarpApi.migrateData(coin + 1);
- } catch (e) {
- printV("zec init failed: $e");
- } // do not fail on network exception
- await loadZcashHeight();
-
- unawaited(loadProver());
+ if (!ywalletDbFile.existsSync()) {
+ //TODO(mrcyjanek): migrate to zkool
+ }
- await ZcashTaddressRotation.init();
- await ZcashTransactionInfo.init();
_initialized = true;
}
- static bool isProverLoaded = false;
- static Future<void> loadProver() async {
- Uint8List? spend;
- Uint8List? output;
- final cacheDir = await getApplicationCacheDirectory();
- try {
- final spendBundle = await rootBundle.load('scripts/zcash_lib/assets/sapling-spend.params');
- final outputBundle = await rootBundle.load('scripts/zcash_lib/assets/sapling-output.params');
- spend = spendBundle.buffer.asUint8List();
- output = outputBundle.buffer.asUint8List();
- if (spend.length == 0 || output.length == 0) {
- spend = await File(cacheDir.path + "/sapling-spend.params").readAsBytesSync();
- output = await File(cacheDir.path + "/sapling-output.params").readAsBytesSync();
- }
- if (spend.length == 0 || output.length == 0) throw Exception("NUH UH");
- } catch (e) {
- printV("$e. Fine, I'll download them.");
- final spendResponse = await ProxyWrapper().get(
- clearnetUri: Uri.parse("https://download.z.cash/downloads/sapling-spend.params"),
- );
- final outputResponse = await ProxyWrapper().get(
- clearnetUri: Uri.parse("https://download.z.cash/downloads/sapling-output.params"),
- );
- spend = spendResponse.bodyBytes;
- output = outputResponse.bodyBytes;
- await File(cacheDir.path + "/sapling-spend.params").writeAsBytes(spend);
- await File(cacheDir.path + "/sapling-output.params").writeAsBytes(output);
- }
- WarpApi.initProver(spend, output);
- isProverLoaded = true;
+ static Future<int> getHeightByDate(final DateTime date) async {
+ final height = await ZcashHeight.getBlockHeightByTime(date);
+ return height;
}
- static Future<int> getBlockHeightByTime(final DateTime time) async {
- final genesisTime = DateTime.utc(2016, 10, 28);
- const genesisHeight = 0;
-
- final firstHalvingTime = DateTime.utc(2020, 11, 18);
- const firstHalvingHeight = 1046400;
-
- final secondHalvingTime = DateTime.utc(2024, 11, 23);
- const secondHalvingHeight = 2726400;
-
- final t = time.toUtc().millisecondsSinceEpoch;
- final t0 = genesisTime.millisecondsSinceEpoch;
- final t1 = firstHalvingTime.millisecondsSinceEpoch;
- final t2 = secondHalvingTime.millisecondsSinceEpoch;
-
- if (t <= t0) return genesisHeight;
-
- if (t < t1) {
- return _interpolate(genesisHeight, firstHalvingHeight, t0, t1, t);
- }
-
- if (t < t2) {
- return _interpolate(firstHalvingHeight, secondHalvingHeight, t1, t2, t);
- }
-
- final secondsSince2 = (t - t2) / 1000.0;
- final blocksSince2 = (secondsSince2 / 76.0).floor();
- return secondHalvingHeight + blocksSince2;
+ static Future<int> newAccount({
+ required final String name,
+ required final int height,
+ required final String seed,
+ required final String passphrase,
+ }) async {
+ final id = await zkool_account.newAccount(
+ na: zkool_account.NewAccount(
+ name: name,
+ restore: true,
+ passphrase: passphrase,
+ key: seed,
+ aindex: 0,
+ birth: height,
+ folder: '',
+ useInternal: true,
+ internal: false,
+ ledger: false,
+ ),
+ c: c,
+ );
+ return id;
}
- static int _interpolate(
- final int hStart,
- final int hEnd,
- final int tStart,
- final int tEnd,
- final int t,
- ) {
- if (tEnd == tStart) return hStart;
- final ratio = (t - tStart) / (tEnd - tStart);
- return (hStart + (hEnd - hStart) * ratio).round();
- }
+ static final runWithCoinMutex = Mutex();
+ static int runWithCoinCount = 0;
- static Future<int> getHeightByDate(final DateTime date) async {
- int height = await getBlockHeightByTime(date);
+ static Future<T> withSharedCoinLock<T>(final FutureOr<T> Function() func) async {
+ await runWithCoinMutex.acquire();
try {
- final h2 = await WarpApi.getBlockHeightByTime(coin, date);
- height = h2;
- } catch (e) {
- printV("getHeightByDate: $e");
+ return await func();
+ } finally {
+ runWithCoinMutex.release();
}
- return height;
- }
-
- bool couldBeZashiWallet() {
- final backup = WarpApi.getBackup(coin, accountId);
- final seed = backup.seed?.split(" ");
- if (seed == null) return false;
- if (zkoolSweep != null) return false;
- if (!(syncStatus is SyncedSyncStatus)) return false;
- return seed.length == 24;
}
- static ZkoolSweep? zkoolSweep;
-
- static bool _didRunRescanInternalChange = false;
- Future<void> rescanInternalChange() async {
- if (_didRunRescanInternalChange) {
- return;
+ static FutureOr<T> runWithCoin<T>({
+ required final int accountId,
+ required final FutureOr<T> Function(zkool_coin.Coin c) func,
+ }) async {
+ var newC = zkool_coin.Coin();
+ newC = await newC.openDatabase(dbFilepath: c.dbFilepath);
+ newC = await newC.setAccount(account: accountId);
+ newC = await newC.setLwd(serverType: c.serverType, url: c.url);
+ newC = await newC.setUseTor(useTor: c.useTor);
+
+ runWithCoinCount++;
+ printV("run with coin: $runWithCoinCount");
+ await runWithCoinMutex.acquire();
+ try {
+ newC = await newC.setAccount(account: accountId);
+ return await func(newC);
+ } finally {
+ runWithCoinMutex.release();
+ runWithCoinCount--;
}
- _didRunRescanInternalChange = true;
- final bal =
- balance[CryptoCurrency.zec]!.available +
- balance[CryptoCurrency.zec]!.unavailable +
- (balance[CryptoCurrency.zec]!.frozen ?? Money.zero(CryptoCurrency.zec));
- final osCacheDir = await getApplicationCacheDirectory();
- final cacheDir = osCacheDir.createTempSync("zkool-import");
- zkoolSweep = ZkoolSweep(
- currentBalance: bal.amount.toInt(),
- cacheDir: cacheDir.path,
- seed: seed ?? '',
- passphrase: password,
- address: (walletAddresses as ZcashWalletAddresses).orchardAddress,
- url: (lastNode!.isSSL ? 'https://' : 'http://') + lastNode!.uriRaw,
- height: (await loadZcashHeight()) ?? zashiAnnouncedBlockHeight,
- );
- unawaited(zkoolSweep!.start());
- int count = 0;
- Timer.periodic(Duration(milliseconds: 1000 ~/ 120), (final Timer t) {
- final msg = ZkoolSweep.msg;
- if (kDebugMode && (++count % (120 ~/ 10)) == 0) {
- printV(msg.message);
- }
- if (msg.blocksLeft == 0 && msg.networkHeight != 0) {
- zkoolSweep = null;
- t.cancel();
- }
- if (msg.blocksLeft == 0) {
- syncStatus = ConnectedSyncStatus();
- return;
- }
- syncStatus = SyncingSyncStatus(msg.blocksLeft, msg.progress);
- });
}
}
diff --git a/cw_zcash/lib/src/zcash_wallet_addresses.dart b/cw_zcash/lib/src/zcash_wallet_addresses.dart
index a9fd207..31bdbbf 100644
--- a/cw_zcash/lib/src/zcash_wallet_addresses.dart
+++ b/cw_zcash/lib/src/zcash_wallet_addresses.dart
@@ -6,7 +6,7 @@ import 'package:cw_core/wallet_info.dart';
import 'package:cw_zcash/cw_zcash.dart';
import 'package:cw_zcash/src/zcash_taddress_rotation.dart';
import 'package:mobx/mobx.dart';
-import 'package:warp_api/warp_api.dart';
+import 'package:zkool/src/rust/api/account.dart' as zkool_account;
part 'zcash_wallet_addresses.g.dart';
@@ -17,24 +17,34 @@ abstract class ZcashWalletAddressesBase extends WalletAddresses with Store {
addressPageType = ZcashReceivePageOption.typeFromString(walletInfo.addressPageType ?? "");
}
- final int accountId;
+ int accountId;
static const int transparentType = 0;
static const int shieldedSaplingType = 1;
static const int shieldedOrchardType = 2;
static const int unifiedType = 3;
- String? _transparentAddress;
- String? _saplingAddress;
- String? _orchardAddress;
- String? _unifiedAddress;
+ @observable
+ String? transparentAddress;
+
+ @observable
+ String? _transparentObservableAddress;
+
+ @observable
+ String? saplingAddress;
+
+ @observable
+ String? orchardAddress;
+
+ @observable
+ String? unifiedAddress;
@override
@computed
String get latestAddress {
switch (addressPageType) {
case ZcashAddressType.transparent:
- return transparentAddress;
+ return transparentAddress ?? "unknown transparentAddress";
case ZcashAddressType.transparentRotated:
// just to display something, after the wallet is synced the address
// 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 {
// 3) display first address from rotated pool which is also static until synced
// 2 seems like an obvious winner simply because it offers little to no benefit over 3
// and is noticably less complex in implementation
- return transparentAddressRotated ?? transparentAddress;
+ return _transparentObservableAddress ??
+ transparentAddress ??
+ "unknown transparentAddressRotated";
case ZcashAddressType.shieldedSapling:
- return saplingAddress;
+ return saplingAddress ?? "unknown saplingAddress";
case ZcashAddressType.shieldedOrchard:
- return orchardAddress;
+ return orchardAddress ?? "unknown orchardAddress";
case ZcashAddressType.unifiedType:
- if (_unifiedAddress == null) {
- _unifiedAddress = WarpApi.getAddress(ZcashWalletBase.coin, accountId, 6);
- }
- return _unifiedAddress ?? "";
+ return unifiedAddress ?? "unknown unifiedAddress";
}
}
@@ -79,63 +88,21 @@ abstract class ZcashWalletAddressesBase extends WalletAddresses with Store {
await walletInfo.save();
}
- String get transparentAddress {
- try {
- if (_transparentAddress == null) {
- _transparentAddress = WarpApi.getTAddr(ZcashWalletBase.coin, accountId);
- }
- return _transparentAddress ?? "";
- } catch (e) {
- return "";
- }
- }
-
- @computed
- String? get transparentAddressRotated {
- try {
- return ZcashTaddressRotation.addressForAccount(accountId, hiddenAddresses);
- } catch (e) {
- return null;
- }
- }
-
- String get saplingAddress {
- try {
- if (_saplingAddress == null) {
- _saplingAddress = WarpApi.getAddress(ZcashWalletBase.coin, accountId, 2);
- }
- return _saplingAddress ?? "";
- } catch (e) {
- return "";
- }
- }
-
- String get orchardAddress {
- try {
- if (_orchardAddress == null) {
- _orchardAddress = WarpApi.getAddress(ZcashWalletBase.coin, accountId, 4);
- }
- return _orchardAddress ?? "";
- } catch (e) {
- return "";
- }
- }
-
@override
@computed
- String get addressForExchange => transparentAddressRotated ?? transparentAddress;
+ String get addressForExchange =>
+ _transparentObservableAddress ?? transparentAddress ?? "unknown addressForExchange";
@override
@computed
- String get addressForBuy => transparentAddressRotated ?? transparentAddress;
+ String get addressForBuy =>
+ _transparentObservableAddress ?? transparentAddress ?? "unknown addressForBuy";
@override
bool containsAddress(final String address) {
return this.address == address || addressesMap.values.contains(address);
}
- static int get coin => ZcashWalletBase.coin;
-
@override
@observable
Set<String> hiddenAddresses = {};
@@ -149,37 +116,105 @@ abstract class ZcashWalletAddressesBase extends WalletAddresses with Store {
address = latestAddress;
}
+ Future<void> _syncRotationHiddenAddresses() async {
+ final all = (await ZcashTaddressRotation.allAddressesForAccount(accountId)).toSet();
+ final used = (await ZcashTaddressRotation.usedAddressesForAccount(accountId)).toSet();
+ hiddenAddresses.removeWhere(all.contains);
+ hiddenAddresses.addAll(used);
+ }
+
@override
Future<void> init() async {
+ try {
+ await _init();
+ } catch (e) {
+ printV("init failed, retrying in 2 seconds");
+ printV(e);
+ await Future.delayed(Duration(seconds: 2));
+ await init();
+ }
+ }
+
+ static bool isPlaceholderAddress(final String? address) =>
+ address == null || address.isEmpty || address.startsWith('unknown ');
+
+ String _resolvedWalletInfoAddress() {
+ for (final candidate in [unifiedAddress, orchardAddress, saplingAddress, transparentAddress]) {
+ if (!isPlaceholderAddress(candidate)) {
+ return candidate!;
+ }
+ }
+ return latestAddress;
+ }
+
+ Future<void> _init() async {
await _initAddresses();
- await ZcashTaddressRotation.init();
- int accountIndex = 0;
+ final addr = await ZcashWalletBase.runWithCoin(
+ accountId: accountId,
+ func: (final coin) => zkool_account.getAddresses(c: coin, uaPools: 7),
+ );
+
+ transparentAddress = addr.taddr ?? 'unknown addr.taddr';
+ saplingAddress = addr.saddr ?? 'unknown addr.saddr';
+ orchardAddress = addr.oaddr ?? 'unknown addr.oaddr';
+ unifiedAddress = addr.ua ?? 'unknown addr.ua';
+ _transparentObservableAddress = await ZcashTaddressRotation.addressForAccount(accountId);
+ final rotationAddrs = await ZcashTaddressRotation.allAddressesForAccount(accountId);
addressInfos = {
- 0:
- ZcashTaddressRotation.allAddressesForAccount(accountId)?.map((final v) {
- return WalletInfoAddressInfo(
- walletInfoId: walletInfo.internalId,
- mapKey: ++accountIndex,
- accountIndex: 0,
- address: v,
- label: "",
- );
- }).toList() ??
- [],
+ 0: [
+ for (int i = 0; i < rotationAddrs.length; i++)
+ WalletInfoAddressInfo(
+ walletInfoId: walletInfo.internalId,
+ mapKey: i + 1,
+ accountIndex: 0,
+ address: rotationAddrs[i],
+ label: "",
+ ),
+ ],
};
- hiddenAddresses.addAll(
- ZcashTaddressRotation.allUsedAddressesForAccount(accountId)?.toSet() ?? {},
- );
+ await _syncRotationHiddenAddresses();
+ await _applyAddressForCurrentType();
+
+ if (isPlaceholderAddress(walletInfo.address)) {
+ final resolved = _resolvedWalletInfoAddress();
+ if (!isPlaceholderAddress(resolved)) {
+ walletInfo.address = resolved;
+ await walletInfo.save();
+ }
+ }
+
+ await saveAddressesInBox();
+ }
- // addressInfos[0]?.removeWhere((final test) => hiddenAddresses.contains(test.address));
+ Future<void> refreshRotationAddresses() async {
+ _transparentObservableAddress = await ZcashTaddressRotation.addressForAccount(accountId);
+ final rotationAddrs = await ZcashTaddressRotation.allAddressesForAccount(accountId);
+ addressInfos = {
+ 0: [
+ for (int i = 0; i < rotationAddrs.length; i++)
+ WalletInfoAddressInfo(
+ walletInfoId: walletInfo.internalId,
+ mapKey: i + 1,
+ accountIndex: 0,
+ address: rotationAddrs[i],
+ label: "",
+ ),
+ ],
+ };
+ await _syncRotationHiddenAddresses();
+ await _applyAddressForCurrentType();
+ }
+
+ Future<void> _applyAddressForCurrentType() async {
if (_addressPageType == ZcashAddressType.transparentRotated) {
- final addr = ZcashTaddressRotation.addressForAccount(accountId, hiddenAddresses);
+ final addr = await ZcashTaddressRotation.addressForAccount(accountId);
if (addr != null) {
address = addr;
}
+ return;
}
- await saveAddressesInBox();
+ address = latestAddress;
}
@observable
@@ -198,12 +233,17 @@ abstract class ZcashWalletAddressesBase extends WalletAddresses with Store {
@override
Future<void> saveAddressesInBox() async {
try {
+ if (!isPlaceholderAddress(address)) {
+ walletInfo.address = address;
+ }
await walletInfo.setAddresses(addressesMap);
await walletInfo.setAddressInfos(addressInfos);
await walletInfo.setUsedAddresses(usedAddresses.toList());
await walletInfo.setHiddenAddresses(hiddenAddresses.toList());
await walletInfo.setManualAddresses(manualAddresses.toList());
+ await walletInfo.save();
await _initAddresses();
+ await _syncRotationHiddenAddresses();
} catch (e) {
printV("Error saving addresses: $e");
}
@@ -211,10 +251,7 @@ abstract class ZcashWalletAddressesBase extends WalletAddresses with Store {
@override
Future<void> updateAddressesInBox() async {
- _transparentAddress = null;
- _saplingAddress = null;
- _orchardAddress = null;
- _unifiedAddress = null;
+ await _initAddresses();
}
@override
@@ -227,11 +264,7 @@ abstract class ZcashWalletAddressesBase extends WalletAddresses with Store {
if (addressPageType != ZcashAddressType.transparentRotated) {
return [];
}
- final List<WalletInfoAddressInfo> allInfos = [];
- for (final entry in addressInfos.entries) {
- allInfos.addAll(entry.value);
- }
- return allInfos;
+ return addressInfos[0] ?? [];
}
@override
diff --git a/cw_zcash/lib/src/zcash_wallet_service.dart b/cw_zcash/lib/src/zcash_wallet_service.dart
index 0d79e70..4f46047 100644
--- a/cw_zcash/lib/src/zcash_wallet_service.dart
+++ b/cw_zcash/lib/src/zcash_wallet_service.dart
@@ -9,6 +9,7 @@ import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_service.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cw_zcash/cw_zcash.dart';
+import 'package:cw_zcash/src/migrate.dart';
import 'package:mutex/mutex.dart';
import 'package:path/path.dart' as p;
@@ -25,9 +26,9 @@ class ZcashWalletService
static Mutex dbMutex = Mutex();
static int dbMutexQueue = 0;
static Future<T> runInDbMutex<T>(final Future<T> Function() call) async {
+ dbMutexQueue++;
+ await dbMutex.acquire();
try {
- printV("dbMutexQueue: ${dbMutexQueue++}");
- await dbMutex.acquire();
return await call();
} finally {
dbMutex.release();
@@ -37,10 +38,15 @@ class ZcashWalletService
static Set<String> autoshieldTx = {};
+ static String normalizeTxId(final String txId) =>
+ txId.replaceAll(RegExp(r'[^a-fA-F0-9]'), '').toLowerCase();
+
+ static bool isAutoshieldTx(final String txHash) => autoshieldTx.contains(normalizeTxId(txHash));
+
static Future<void> addShieldedTx(final String txId) async {
final pathForWalletType = await pathForWalletTypeDir(type: type);
- final shieldedListFile = p.join(pathForWalletType, "autoshield_list.txt");
- autoshieldTx.add(txId.replaceAll(RegExp(r'[^a-zA-Z0-9 ]'), ''));
+ final shieldedListFile = p.join(pathForWalletType, "autoshield_list.v2.txt");
+ autoshieldTx.add(normalizeTxId(txId));
final shieldedList = File(shieldedListFile);
shieldedList.writeAsStringSync(autoshieldTx.join("\n"));
}
@@ -48,12 +54,16 @@ class ZcashWalletService
static Future<void> loadShieldTxs() async {
try {
final pathForWalletType = await pathForWalletTypeDir(type: type);
- final shieldedListFile = p.join(pathForWalletType, "autoshield_list.txt");
+ final shieldedListFile = p.join(pathForWalletType, "autoshield_list.v2.txt");
final shieldedList = File(shieldedListFile);
if (!shieldedList.existsSync()) {
return;
}
- autoshieldTx = shieldedList.readAsLinesSync().toSet();
+ autoshieldTx = shieldedList
+ .readAsLinesSync()
+ .map(normalizeTxId)
+ .where((final line) => line.isNotEmpty)
+ .toSet();
} catch (e) {
printV("loadShieldTxs failed: $e");
}
@@ -74,12 +84,21 @@ class ZcashWalletService
@override
Future<bool> isWalletExit(final String name) async {
- final path = await pathForWallet(name: name, type: getType());
- return File(path).existsSync();
+ final oldPath = (await pathForWallet(name: name, type: getType()));
+ final path = (await pathForWallet(name: name, type: getType())) + ".v2";
+ return File(path).existsSync() || File(oldPath).existsSync();
}
@override
Future<ZcashWallet> openWallet(final String name, final String password) async {
+ await ZcashWalletBase.$init();
+ if (await isWalletExit(name)) {
+ final path = (await pathForWallet(name: name, type: getType())) + ".v2";
+ if (!File(path).existsSync()) {
+ await migrateOldSqliteToZkool2(walletName: name);
+ }
+ }
+
await loadShieldTxs();
final walletInfo = await WalletInfo.get(name, getType());
if (walletInfo == null) {
@@ -108,7 +127,7 @@ class ZcashWalletService
@override
Future<void> remove(final String wallet) async {
- final path = await pathForWalletDir(name: wallet, type: getType());
+ final path = (await pathForWalletDir(name: wallet, type: getType()));
final file = Directory(path);
final isExist = file.existsSync();
@@ -129,17 +148,11 @@ class ZcashWalletService
if (currentWalletInfo == null) {
throw Exception('Wallet not found');
}
- final accountId = await ZcashWalletBase.getZcashAccountIdForName(currentName);
- if (accountId == null) {
- throw Exception('Wallet account not found for name: $currentName');
+ if (!await isWalletExit(currentName)) {
+ throw Exception('Wallet not found');
}
- final currentWallet = ZcashWallet(
- currentWalletInfo,
- await currentWalletInfo.getDerivationInfo(),
- accountId: accountId,
- );
- await currentWallet.renameWalletFiles(newName);
+ await ZcashWalletBase.renameWalletFilesForName(fromName: currentName, toName: newName);
final newWalletInfo = currentWalletInfo;
newWalletInfo.id = WalletBase.idFor(newName, getType());
@@ -162,11 +175,11 @@ class ZcashWalletService
final bool? isTestnet,
}) async {
if (credentials.seed == null || credentials.seed!.isEmpty) {
- throw ZcashMnemonicIsIncorrectException();
+ throw Exception("Seed is missing");
}
if (!bip39.validateMnemonic(credentials.seed!)) {
- throw ZcashMnemonicIsIncorrectException();
+ throw Exception("Seed is not valid bip39");
}
return ZcashWalletBase.restore(credentials);
diff --git a/cw_zcash/lib/src/zkool_compat.dart b/cw_zcash/lib/src/zkool_compat.dart
new file mode 100644
index 0000000..c13ff29
--- /dev/null
+++ b/cw_zcash/lib/src/zkool_compat.dart
@@ -0,0 +1,3 @@
+import 'dart:typed_data';
+
+final zecBase = Uint8List(32);
diff --git a/cw_zcash/lib/src/zkooltx.dart b/cw_zcash/lib/src/zkooltx.dart
new file mode 100644
index 0000000..16fdc6f
--- /dev/null
+++ b/cw_zcash/lib/src/zkooltx.dart
@@ -0,0 +1,312 @@
+import 'dart:convert';
+import 'dart:math';
+import 'dart:typed_data';
+
+import 'package:cw_core/get_height_by_date_zec.dart';
+import 'package:cw_core/transaction_direction.dart';
+import 'package:cw_zcash/src/util/hex.dart';
+import 'package:zkool/src/rust/api/account.dart' as zkool_account;
+
+enum TxType {
+ unknown,
+ selfTransfer, // 0
+ receive, // 1
+ sent, // 2
+ unshield, // 4
+ shield, // 8
+ transparentSelfTransfer, // 12
+}
+
+enum NotePool { transparent, sapling, orchard, unknown }
+
+class ZkoolTx {
+ ZkoolTx(final zkool_account.Tx tx, final zkool_account.TxAccount txAccount)
+ : this._tx = tx,
+ this._txAccount = txAccount;
+ final zkool_account.Tx _tx;
+ final zkool_account.TxAccount _txAccount;
+
+ int get height => max(
+ _tx.height,
+ _txAccount.height,
+ ); // sometimes returns 0 - higher number would be correct in this case
+
+ DateTime get time {
+ final ts = max(_tx.time, _txAccount.time) * 1000;
+ if (ts == 0) {
+ return ZcashHeight.getTimeByBlockHeight(height);
+ }
+ return DateTime.fromMillisecondsSinceEpoch(ts);
+ }
+
+ String? get to => _txAccount.outputs.firstOrNull?.address;
+ Iterable<String> get outputAddresses =>
+ _txAccount.outputs.map((final o) => o.address).whereType<String>();
+ Iterable<({String address, int pool, BigInt value})> get outputsWithAddress => _txAccount.outputs
+ .where((final o) => o.address.isNotEmpty)
+ .map((final o) => (address: o.address, pool: o.pool, value: o.value));
+ Iterable<int> get spendPools => _txAccount.spends.map((final s) => s.pool);
+ Iterable<int> get notePools => _txAccount.notes.map((final n) => n.pool);
+ String? get memo => _txAccount.memos.firstOrNull?.memo;
+
+ BigInt get transparentOrSaplingSpent {
+ if (_txAccount.spends.isEmpty) {
+ return BigInt.zero;
+ }
+ return _txAccount.spends
+ .where(
+ (final s) => s.pool == NotePool.transparent.index || s.pool == NotePool.sapling.index,
+ )
+ .fold(BigInt.zero, (final a, final s) => a + s.value);
+ }
+
+ BigInt get orchardReceived {
+ final fromNotes = _txAccount.notes
+ .where((final n) => n.pool == NotePool.orchard.index)
+ .fold(BigInt.zero, (final a, final n) => a + n.value);
+ if (fromNotes > BigInt.zero) {
+ return fromNotes;
+ }
+ return _txAccount.outputs
+ .where((final o) => o.pool == NotePool.orchard.index)
+ .fold(BigInt.zero, (final a, final o) => a + o.value);
+ }
+
+ String get txHash {
+ final reversed = Uint8List.fromList(_txAccount.txid.reversed.toList());
+ final txId = uint8ListToHex(reversed);
+ return txId;
+ }
+
+ BigInt get value => _value.abs();
+
+ BigInt get fee => value - _calcValue;
+
+ BigInt get _value {
+ if (_tx.value != 0) {
+ return BigInt.from(_tx.value);
+ }
+
+ return _calcValue;
+ }
+
+ /////////// calc is short for calculator (calculated in this case)
+ BigInt get _calcValue {
+ final noteSum = _txAccount.notes.isEmpty
+ ? BigInt.from(0)
+ : _txAccount.notes.map((final note) => note.value).reduce((final a, final b) => a + b);
+ final outputSum = _txAccount.outputs.isEmpty
+ ? BigInt.from(0)
+ : _txAccount.outputs
+ .map((final output) => output.value)
+ .reduce((final a, final b) => a + b);
+ final spentSum = _txAccount.spends.isEmpty
+ ? BigInt.from(0)
+ : _txAccount.spends.map((final spent) => spent.value).reduce((final a, final b) => a + b);
+
+ return noteSum - outputSum - spentSum;
+ }
+
+ TransactionDirection get direction {
+ switch (type) {
+ case TxType.selfTransfer:
+ case TxType.receive:
+ case TxType.unshield:
+ case TxType.shield:
+ case TxType.transparentSelfTransfer:
+ return TransactionDirection.incoming;
+ case TxType.sent:
+ return TransactionDirection.outgoing;
+ case TxType.unknown:
+ return _value > BigInt.from(0)
+ ? TransactionDirection.incoming
+ : TransactionDirection.outgoing;
+ }
+ }
+
+ TxType get type {
+ switch (_tx.tpe) {
+ case 0:
+ return TxType.selfTransfer;
+ case 1:
+ return TxType.receive;
+ case 2:
+ return TxType.sent;
+ case 4:
+ return TxType.unshield;
+ case 8:
+ return TxType.shield;
+ case 12:
+ return TxType.transparentSelfTransfer;
+ default:
+ return TxType.unknown;
+ }
+ }
+
+ Map<String, dynamic> toJson() => {
+ "tx": {
+ "id": _tx.id,
+ "txid": base64.encode(_tx.txid),
+ "height": _tx.height,
+ "time": _tx.time,
+ "value": value.toInt(),
+ "tpe": _tx.tpe,
+ "zsaValue": _tx.zsaValue,
+ "assetDisplay": _tx.assetDisplay,
+ },
+ "txaccount": {
+ "id": _txAccount.id,
+ "account": _txAccount.account,
+ "txid": base64.encode(_txAccount.txid),
+ "height": _txAccount.height,
+ "time": _txAccount.time,
+ "notes": _txAccount.notes
+ .map(
+ (final note) => {
+ "id": note.id,
+ "pool": note.pool,
+ "height": note.height,
+ "tx": note.tx,
+ "scope": note.scope,
+ "value": note.value.toString(),
+ "locked": note.locked,
+ },
+ )
+ .toList(),
+ "spends": _txAccount.spends
+ .map(
+ (final spend) => {
+ "id": spend.id,
+ "pool": spend.pool,
+ "height": spend.height,
+ "value": spend.value.toString(),
+ "assetDisplay": spend.assetDisplay,
+ },
+ )
+ .toList(),
+ "outputs": _txAccount.outputs
+ .map(
+ (final output) => {
+ "id": output.id,
+ "pool": output.pool,
+ "height": output.height,
+ "value": output.value.toString(),
+ "address": output.address,
+ },
+ )
+ .toList(),
+ "memos": _txAccount.memos
+ .map(
+ (final memo) => {
+ "note": memo.note,
+ "pool": memo.pool,
+ "output": memo.output,
+ "memo": memo.memo,
+ },
+ )
+ .toList(),
+ },
+ };
+
+ static int _asInt(final dynamic value, {final int fallback = 0}) {
+ if (value == null) {
+ return fallback;
+ }
+ if (value is int) {
+ return value;
+ }
+ if (value is num) {
+ return value.toInt();
+ }
+ return int.parse(value.toString());
+ }
+
+ static Uint8List _txidFromJson(final dynamic raw) {
+ if (raw is Uint8List) {
+ return raw;
+ }
+ if (raw is List) {
+ return Uint8List.fromList(List<int>.from(raw));
+ }
+ if (raw is String) {
+ return base64.decode(raw);
+ }
+ throw FormatException("Invalid txid in ZkoolTx json: $raw");
+ }
+
+ static List<T> _listFromJson<T>(final dynamic raw, final T Function(Map<String, dynamic>) parse) {
+ if (raw is! List) {
+ return const [];
+ }
+ return raw.map((final entry) => parse(Map<String, dynamic>.from(entry as Map))).toList();
+ }
+
+ static ZkoolTx fromJson(final Map<String, dynamic> json) {
+ final txJson = Map<String, dynamic>.from((json["tx"] as Map?)?.cast<String, dynamic>() ?? json);
+ final txAccountJson = Map<String, dynamic>.from(
+ (json["txaccount"] as Map?)?.cast<String, dynamic>() ?? json,
+ );
+
+ return ZkoolTx(
+ zkool_account.Tx(
+ id: _asInt(txJson["id"]),
+ txid: _txidFromJson(txJson["txid"]),
+ height: _asInt(txJson["height"]),
+ time: _asInt(txJson["time"]),
+ value: _asInt(txJson["value"]),
+ tpe: txJson["tpe"] == null ? null : _asInt(txJson["tpe"]),
+ zsaValue: _asInt(txJson["zsaValue"]),
+ assetDisplay: txJson["assetDisplay"] as String? ?? "",
+ ),
+ zkool_account.TxAccount(
+ id: _asInt(txAccountJson["id"]),
+ account: _asInt(txAccountJson["account"]),
+ txid: _txidFromJson(txAccountJson["txid"]),
+ height: _asInt(txAccountJson["height"]),
+ time: _asInt(txAccountJson["time"]),
+ notes: _listFromJson(
+ txAccountJson["notes"],
+ (final a) => zkool_account.TxNote(
+ id: _asInt(a["id"]),
+ pool: _asInt(a["pool"]),
+ height: _asInt(a["height"]),
+ tx: _asInt(a["tx"]),
+ value: BigInt.parse(a["value"].toString()),
+ scope: _asInt(a["scope"]),
+ locked: a["locked"] as bool? ?? false,
+ assetDisplay: a["assetDisplay"] as String? ?? "",
+ ),
+ ),
+ spends: _listFromJson(
+ txAccountJson["spends"],
+ (final a) => zkool_account.TxSpend(
+ id: _asInt(a["id"]),
+ pool: _asInt(a["pool"]),
+ height: _asInt(a["height"]),
+ value: BigInt.parse(a["value"].toString()),
+ assetDisplay: a["assetDisplay"] as String? ?? "",
+ ),
+ ),
+ outputs: _listFromJson(
+ txAccountJson["outputs"],
+ (final a) => zkool_account.TxOutput(
+ id: _asInt(a["id"]),
+ pool: _asInt(a["pool"]),
+ height: _asInt(a["height"]),
+ value: BigInt.parse(a["value"].toString()),
+ address: a["address"] as String? ?? "",
+ ),
+ ),
+ memos: _listFromJson(
+ txAccountJson["memos"],
+ (final a) => zkool_account.TxMemo(
+ note: a["note"] == null ? null : _asInt(a["note"]),
+ pool: _asInt(a["pool"]),
+ output: a["output"] == null ? null : _asInt(a["output"]),
+ memo: a["memo"] as String?,
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/cw_zcash/pubspec.yaml b/cw_zcash/pubspec.yaml
index 72e6de2..84e0d27 100644
--- a/cw_zcash/pubspec.yaml
+++ b/cw_zcash/pubspec.yaml
@@ -9,8 +9,6 @@ environment:
dependencies:
flutter:
sdk: flutter
- warp_api:
- path: ../scripts/zcash_lib/packages/warp_api_ffi
cw_core:
path: ../cw_core
mobx: ^2.6.0
@@ -20,11 +18,13 @@ dependencies:
path: any
flat_buffers: any
zkool:
+ # path: ../../zkool2
git:
- url: https://github.com/cake-tech/zkool2
- ref: b1642f260da4fa00dd2f8d4dd689073d0b56960a
+ url: https://github.com/cake-tech/zkool2.git
+ ref: dbd3dde8d8485428326a58e55660b7404666dd7c
path_provider: any
+ sqflite_common_ffi: any
dependency_overrides:
decimal: ^2.3.3
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index 29a2ad9..d46513d 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -1,4 +1,14 @@
PODS:
+ - AppAuth (1.7.6):
+ - AppAuth/Core (= 1.7.6)
+ - AppAuth/ExternalUserAgent (= 1.7.6)
+ - AppAuth/Core (1.7.6)
+ - AppAuth/ExternalUserAgent (1.7.6):
+ - AppAuth/Core
+ - AppCheckCore (11.2.0):
+ - GoogleUtilities/Environment (~> 8.0)
+ - GoogleUtilities/UserDefaults (~> 8.0)
+ - PromisesObjC (~> 2.4)
- bitbox_flutter (0.0.1):
- Flutter
- breez_sdk_spark_flutter (0.14.0):
@@ -73,6 +83,34 @@ PODS:
- Flutter
- fluttertoast (0.0.2):
- Flutter
+ - google_sign_in_ios (0.0.1):
+ - AppAuth (>= 1.7.4)
+ - Flutter
+ - FlutterMacOS
+ - GoogleSignIn (~> 8.0)
+ - GTMSessionFetcher (>= 3.4.0)
+ - GoogleSignIn (8.0.0):
+ - AppAuth (< 2.0, >= 1.7.3)
+ - AppCheckCore (~> 11.0)
+ - GTMAppAuth (< 5.0, >= 4.1.1)
+ - GTMSessionFetcher/Core (~> 3.3)
+ - GoogleUtilities/Environment (8.1.0):
+ - GoogleUtilities/Privacy
+ - GoogleUtilities/Logger (8.1.0):
+ - GoogleUtilities/Environment
+ - GoogleUtilities/Privacy
+ - GoogleUtilities/Privacy (8.1.0)
+ - GoogleUtilities/UserDefaults (8.1.0):
+ - GoogleUtilities/Logger
+ - GoogleUtilities/Privacy
+ - GTMAppAuth (4.1.1):
+ - AppAuth/Core (~> 1.7)
+ - GTMSessionFetcher/Core (< 4.0, >= 3.3)
+ - GTMSessionFetcher (3.5.0):
+ - GTMSessionFetcher/Full (= 3.5.0)
+ - GTMSessionFetcher/Core (3.5.0)
+ - GTMSessionFetcher/Full (3.5.0):
+ - GTMSessionFetcher/Core
- image_picker_ios (0.0.1):
- Flutter
- in_app_review (2.0.0):
@@ -88,6 +126,7 @@ PODS:
- payjoin_flutter (0.23.0)
- permission_handler_apple (9.4.8):
- Flutter
+ - PromisesObjC (2.4.0)
- quick_actions_ios (0.0.1):
- Flutter
- reown_yttrium (0.0.1):
@@ -174,6 +213,7 @@ DEPENDENCIES:
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
+ - google_sign_in_ios (from `.symlinks/plugins/google_sign_in_ios/darwin`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- in_app_review (from `.symlinks/plugins/in_app_review/ios`)
- integration_test (from `.symlinks/plugins/integration_test/ios`)
@@ -200,10 +240,17 @@ DEPENDENCIES:
SPEC REPOS:
https://github.com/CocoaPods/Specs.git:
+ - AppAuth
+ - AppCheckCore
- CryptoSwift
- DKImagePickerController
- DKPhotoGallery
+ - GoogleSignIn
+ - GoogleUtilities
+ - GTMAppAuth
+ - GTMSessionFetcher
- OrderedSet
+ - PromisesObjC
- SDWebImage
- sqlite3
- SwiftyGif
@@ -248,6 +295,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/flutter_secure_storage/ios"
fluttertoast:
:path: ".symlinks/plugins/fluttertoast/ios"
+ google_sign_in_ios:
+ :path: ".symlinks/plugins/google_sign_in_ios/darwin"
image_picker_ios:
:path: ".symlinks/plugins/image_picker_ios/ios"
in_app_review:
@@ -296,6 +345,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/warp_api/ios"
SPEC CHECKSUMS:
+ AppAuth: d4f13a8fe0baf391b2108511793e4b479691fb73
+ AppCheckCore: cc8fd0a3a230ddd401f326489c99990b013f0c4f
bitbox_flutter: 9505732798041c413152669751beeaecc5fe400f
breez_sdk_spark_flutter: ed3b6709b8ce9b40309d8728640eade5b19e8a0d
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
@@ -308,24 +359,30 @@ SPEC CHECKSUMS:
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
dnssec_proof: d461cac7bd3301eb7447f87936745a0c1ae0a67e
- fast_scanner: 44c00940355a51258cd6c2085734193cd23d95bc
- file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
- Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
- flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4
- flutter_local_authentication: 1172a4dd88f6306dadce067454e2c4caf07977bb
- flutter_local_notifications: ff50f8405aaa0ccdc7dcfb9022ca192e8ad9688f
- flutter_mailer: 2ef5a67087bc8c6c4cefd04a178bf1ae2c94cd83
- flutter_native_splash: df59bb2e1421aa0282cb2e95618af4dcb0c56c29
- flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be
- fluttertoast: 21eecd6935e7064cc1fcb733a4c5a428f3f24f0f
- image_picker_ios: 4f2f91b01abdb52842a8e277617df877e40f905b
- in_app_review: 436034b18594851a7328d7f1c2ed5ec235b79cfc
- integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573
- local_auth_darwin: 63c73d6d28cc3e239be2b6aa460ea6e317cd5100
+ fast_scanner: 2cb1ad3e69e645e9980fb4961396ce5804caa3e3
+ file_picker: 9b3292d7c8bc68c8a7bf8eb78f730e49c8efc517
+ Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
+ flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
+ flutter_local_authentication: 989278c681612f1ee0e36019e149137f114b9d7f
+ flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb
+ flutter_mailer: 3a8cd4f36c960fb04528d5471097270c19fec1c4
+ flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
+ flutter_secure_storage: 2c2ff13db9e0a5647389bff88b0ecac56e3f3418
+ fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1
+ google_sign_in_ios: b48bb9af78576358a168361173155596c845f0b9
+ GoogleSignIn: ce8c89bb9b37fb624b92e7514cc67335d1e277e4
+ GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
+ GTMAppAuth: f69bd07d68cd3b766125f7e072c45d7340dea0de
+ GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
+ image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
+ in_app_review: 7dd1ea365263f834b8464673f9df72c80c17c937
+ integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
+ local_auth_darwin: d2e8c53ef0c4f43c646462e3415432c4dab3ae19
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
payjoin_flutter: d9d4c8aa16bd5dfedb9b21d0edc8199e0187d96e
permission_handler_apple: 92d754bbaa7361d436db2d6c3c1c2a0fdcec462e
+ PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
quick_actions_ios: 4b07fb49d8d8f3518d7565fbb7a91014067a7d82
reown_yttrium: cee334ade64725b1d83f7b34c706a6aae2696d58
rive_common: dd421daaf9ae69f0125aa761dd96abd278399952
@@ -344,7 +401,6 @@ SPEC CHECKSUMS:
universal_ble: 45519b2aeafe62761e2c6309f8927edb5288b914
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
- warp_api: 997d8fb3e777669bc6260d0c12e56cd40b9db1d1
YttriumWrapper: 31e937fe9fbe0f1314d2ca6be9ce9b379a059966
PODFILE CHECKSUM: 5296465b1c6d14d506230356756826012f65d97a
diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj
index 2c2584e..6b469cb 100644
--- a/ios/Runner.xcodeproj/project.pbxproj
+++ b/ios/Runner.xcodeproj/project.pbxproj
@@ -26,7 +26,6 @@
A1B4A70C9CFA13AB71662216 /* LnurlPay.swift in Resources */ = {isa = PBXBuildFile; fileRef = 7D3364C03978A8A74B6D586E /* LnurlPay.swift */; settings = {ASSET_TAGS = (BreezSDK, ); }; };
A3D5E17CC53DF13FA740DEFA /* RedeemSwap.swift in Resources */ = {isa = PBXBuildFile; fileRef = 9D2F2C9F2555316C95EE7EA3 /* RedeemSwap.swift */; settings = {ASSET_TAGS = (BreezSDK, ); }; };
B6C6E59403ACDE44724C12F4 /* ServiceConfig.swift in Resources */ = {isa = PBXBuildFile; fileRef = B3D5E78267F5F18D882FDC3B /* ServiceConfig.swift */; settings = {ASSET_TAGS = (BreezSDK, ); }; };
- CE3D5B792F142305004D767D /* WarpApiFFI.xcframework in CopyFiles */ = {isa = PBXBuildFile; fileRef = CE3D5B782F142305004D767D /* WarpApiFFI.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
CE918BF82D533ECE007F186E /* MoneroWallet.xcframework in CopyFiles */ = {isa = PBXBuildFile; fileRef = CE918BF72D533ECE007F186E /* MoneroWallet.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
CE918BFA2D533ED4007F186E /* WowneroWallet.xcframework in CopyFiles */ = {isa = PBXBuildFile; fileRef = CE918BF92D533ED4007F186E /* WowneroWallet.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
CE918BFC2D533ED8007F186E /* ZanoWallet.xcframework in CopyFiles */ = {isa = PBXBuildFile; fileRef = CE918BFB2D533ED8007F186E /* ZanoWallet.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
@@ -48,7 +47,6 @@
CE918BF82D533ECE007F186E /* MoneroWallet.xcframework in CopyFiles */,
CE918BFA2D533ED4007F186E /* WowneroWallet.xcframework in CopyFiles */,
CE918BFC2D533ED8007F186E /* ZanoWallet.xcframework in CopyFiles */,
- CE3D5B792F142305004D767D /* WarpApiFFI.xcframework in CopyFiles */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -89,7 +87,6 @@
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>"; };
B478EFCC3C0760ED97FFE561 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
C58D93382C00FAC6004BCF69 /* libresolv.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libresolv.tbd; path = usr/lib/libresolv.tbd; sourceTree = SDKROOT; };
- CE3D5B782F142305004D767D /* WarpApiFFI.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = WarpApiFFI.xcframework; path = ../scripts/zcash_lib/WarpApiFFI.xcframework; sourceTree = "<group>"; };
CE918BF72D533ECE007F186E /* MoneroWallet.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = MoneroWallet.xcframework; sourceTree = "<group>"; };
CE918BF92D533ED4007F186E /* WowneroWallet.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = WowneroWallet.xcframework; sourceTree = "<group>"; };
CE918BFB2D533ED8007F186E /* ZanoWallet.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = ZanoWallet.xcframework; sourceTree = "<group>"; };
@@ -170,7 +167,6 @@
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
- CE3D5B782F142305004D767D /* WarpApiFFI.xcframework */,
0C44A7182518EF4A00B570ED /* CakeWallet */,
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
diff --git a/lib/core/background_sync.dart b/lib/core/background_sync.dart
index a63618d..c2e2705 100644
--- a/lib/core/background_sync.dart
+++ b/lib/core/background_sync.dart
@@ -210,7 +210,11 @@ class BackgroundSync {
}
await sharedPreferences.setString(PreferencesKey.backgroundSyncLastTrigger(wallet.name),
tx.date.add(Duration(minutes: 1)).toIso8601String());
- final action = tx.direction == TransactionDirection.incoming ? "Received" : "Sent";
+ final action = tx.additionalInfo['isAutoShield'] == true
+ ? "Autoshield"
+ : tx.direction == TransactionDirection.incoming
+ ? "Received"
+ : "Sent";
if (sharedPreferences.getBool(PreferencesKey.backgroundSyncNotificationsEnabled) ?? false) {
await showNotification(
"$action ${wallet.currency.fullName} in ${wallet.name}", "${tx.amount.toString()}");
diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart
index 5a673b5..910ea06 100644
--- a/lib/entities/default_settings_migration.dart
+++ b/lib/entities/default_settings_migration.dart
@@ -56,7 +56,7 @@ const dogecoinDefaultNodeUri = 'dogecoin.stackwallet.com:50022';
const baseDefaultNodeUri = 'base-rpc.publicnode.com';
const arbitrumDefaultNodeUri = 'arbitrum.nownodes.io';
const bscDefaultNodeUri = 'bsc-dataseed.bnbchain.org';
-const zcashDefaultNodeUri = 'zec-node.cakewallet.com:443';
+const zcashDefaultNodeUri = 'zec.rocks:443';
Future<void> defaultSettingsMigration(
{required int version,
diff --git a/lib/new-ui/widgets/coins_page/assets_history/transaction_details_modal.dart b/lib/new-ui/widgets/coins_page/assets_history/transaction_details_modal.dart
index 0ac7e72..ff22480 100644
--- a/lib/new-ui/widgets/coins_page/assets_history/transaction_details_modal.dart
+++ b/lib/new-ui/widgets/coins_page/assets_history/transaction_details_modal.dart
@@ -222,16 +222,37 @@ class _TransactionDetailsModalState extends State<TransactionDetailsModal> {
Widget _buildBottomWidget(TransactionDetailsListItem item) {
return switch (item.runtimeType) {
- AddressListItem => AddressFormatter.buildSegmentedAddress(
- address: item.value,
- evenTextStyle: TextStyle(
- fontSize: 12,
- fontFamily: "IBM Plex Mono",
- color: Theme.of(context).colorScheme.onSurface)),
+ AddressListItem => _segmentedAddressList(item.value),
_ => Text(
item.value,
style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant),
)
};
}
+
+ Widget _segmentedAddressList(String value) {
+ final style = TextStyle(
+ fontSize: 12,
+ fontFamily: "IBM Plex Mono",
+ color: Theme.of(context).colorScheme.onSurface,
+ );
+ final lines = value
+ .split(RegExp(r'[,\n]'))
+ .map((final line) => line.trim())
+ .where((final line) => line.isNotEmpty)
+ .toList();
+ if (lines.length <= 1) {
+ return AddressFormatter.buildSegmentedAddress(
+ address: lines.isNotEmpty ? lines.first : value.trim(),
+ evenTextStyle: style,
+ );
+ }
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ for (final line in lines)
+ AddressFormatter.buildSegmentedAddress(address: line, evenTextStyle: style),
+ ],
+ );
+ }
}
diff --git a/lib/src/screens/dashboard/pages/balance/crypto_balance_widget.dart b/lib/src/screens/dashboard/pages/balance/crypto_balance_widget.dart
index 7c4a6ff..932a220 100644
--- a/lib/src/screens/dashboard/pages/balance/crypto_balance_widget.dart
+++ b/lib/src/screens/dashboard/pages/balance/crypto_balance_widget.dart
@@ -334,37 +334,6 @@ class CryptoBalanceWidget extends StatelessWidget {
),
),
],
- if (dashboardViewModel.showZcashMissingFundsCard) ...[
- SizedBox(height: 10),
- Padding(
- padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
- child: InfoCard(
- title: S.of(context).zcash_card_missing_funds,
- description: S.of(context).zcash_card_description,
- leftButtonTitle: S.of(context).zcash_card_dismiss,
- rightButtonTitle: S.of(context).zcash_card_scan,
- image: 'assets/new-ui/crypto_full_icons/zcash.svg',
- leftButtonAction: () => _dismissZcash(context),
- rightButtonAction: () => _rescanInternalChangeZcash(context),
- hintWidget: GestureDetector(
- behavior: HitTestBehavior.opaque,
- onTap: () => launchUrl(
- Uri.parse(
- "https://docs.cakewallet.com/tutorials/zashi/#creating-a-new-zcash-wallet-on-cake"),
- mode: LaunchMode.externalApplication,
- ),
- child: Text(
- S.of(context).learn_more,
- style: Theme.of(context).textTheme.bodySmall?.copyWith(
- color: Theme.of(context).colorScheme.onSurfaceVariant,
- height: 1,
- ),
- softWrap: true,
- ),
- ),
- ),
- ),
- ],
if (dashboardViewModel.showDecredInfoCard) ...[
SizedBox(height: 10),
Padding(
@@ -494,21 +463,6 @@ class CryptoBalanceWidget extends StatelessWidget {
dashboardViewModel.setMwebEnabled();
}
- Future<void> _rescanInternalChangeZcash(BuildContext context) async {
- dashboardViewModel.rescanInternalChangeZcash();
- await showPopUp<void>(
- context: context,
- builder: (BuildContext context) => AlertWithOneAction(
- alertTitle: S.of(context).alert_notice,
- alertContent: S.of(context).zcash_card_warning,
- buttonText: S.of(context).understand,
- buttonAction: () {
- Navigator.of(context).pop();
- },
- ),
- );
- }
-
Future<void> _dismissMweb(BuildContext context) async {
await showPopUp<void>(
context: context,
@@ -522,18 +476,4 @@ class CryptoBalanceWidget extends StatelessWidget {
));
dashboardViewModel.dismissMweb();
}
-
- Future<void> _dismissZcash(BuildContext context) async {
- await showPopUp<void>(
- context: context,
- builder: (BuildContext context) => AlertWithOneAction(
- alertTitle: S.of(context).alert_notice,
- alertContent: S.of(context).zcash_card_enable_later,
- buttonText: S.of(context).understand,
- buttonAction: () {
- Navigator.of(context).pop();
- },
- ));
- dashboardViewModel.dismissZcash();
- }
}
diff --git a/lib/src/screens/dashboard/pages/transactions_page.dart b/lib/src/screens/dashboard/pages/transactions_page.dart
index 9e623c0..6af5f75 100644
--- a/lib/src/screens/dashboard/pages/transactions_page.dart
+++ b/lib/src/screens/dashboard/pages/transactions_page.dart
@@ -111,7 +111,7 @@ class TransactionsPage extends StatelessWidget {
key: item.key,
onTap: () => Navigator.of(context)
.pushNamed(Routes.transactionDetails, arguments: transaction),
- isShield: transaction.additionalInfo['autoShield'] == true,
+ isShield: transaction.additionalInfo['isAutoShield'] == true,
direction: transaction.direction,
formattedDate: DateFormat('HH:mm').format(transaction.date),
formattedAmount: item.formattedCryptoAmount,
diff --git a/lib/src/screens/settings/display_settings_page.dart b/lib/src/screens/settings/display_settings_page.dart
index 5a04b86..6a3fa6f 100644
--- a/lib/src/screens/settings/display_settings_page.dart
+++ b/lib/src/screens/settings/display_settings_page.dart
@@ -115,21 +115,12 @@ class DisplaySettingsPage extends StatelessWidget {
onChanged: (val) {
_displaySettingsViewModel.setDisableTradeOption(val);
}),
- if (_displaySettingsViewModel.showZcashCardSetting)
- ListItemToggle(
- keyValue: "display_settings_show_zcashcard",
- label: S.of(context).show_zcash_card,
- value: _displaySettingsViewModel.showZcashCard,
- onChanged: (val) {
- _displaySettingsViewModel.setShowZcashCard(val);
- }),
ListItemSelector(
keyValue: "display_settings_sync_status_display",
label: S.of(context).sync_status_display_mode,
options: [_displaySettingsViewModel.syncStatusDisplayMode.title],
onTap: () async {
final items = SyncStatusDisplayMode.values.toList();
-
final selectedAtIndex =
items.indexOf(_displaySettingsViewModel.syncStatusDisplayMode);
diff --git a/lib/view_model/dashboard/transaction_list_item.dart b/lib/view_model/dashboard/transaction_list_item.dart
index 4d6a0d5..e2554b2 100644
--- a/lib/view_model/dashboard/transaction_list_item.dart
+++ b/lib/view_model/dashboard/transaction_list_item.dart
@@ -60,8 +60,13 @@ class TransactionListItem extends ActionListItem with Keyable {
return 'Transaction has missing data';
}
- if (transaction.additionalInfo['autoShield'] == true) {
- return "Autoshield";
+ if (transaction.additionalInfo['isAutoShield'] == true) {
+ if (transaction.isPending) {
+ final status = formattedStatus;
+ final baseString = S.current.shielding;
+ return status.isNotEmpty ? "$baseString $status" : "$baseString...";
+ }
+ return S.current.shielding;
}
if (transaction.isPending) {
final status = formattedStatus;
diff --git a/lib/view_model/send/output.dart b/lib/view_model/send/output.dart
index 2933fbe..ebd12a1 100644
--- a/lib/view_model/send/output.dart
+++ b/lib/view_model/send/output.dart
@@ -21,6 +21,7 @@ import 'package:cw_core/balance.dart';
import 'package:cw_core/crypto_amount_format.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/currency_for_wallet_type.dart';
+import 'package:cw_core/sync_status.dart';
import 'package:cw_core/transaction_history.dart';
import 'package:cw_core/transaction_info.dart';
import 'package:cw_core/utils/print_verbose.dart';
@@ -50,8 +51,9 @@ abstract class OutputBase with Store {
parsedAddress = ParsedAddress(parsedAddressByCurrencyMap: {}) {
autorun((_) {
final status = _wallet.syncStatus;
- printV("Sync status changed to $status. Recalculating fees");
-
+ if (status is! SyncedSyncStatus) {
+ return;
+ }
calculateEstimatedFee();
});
}
diff --git a/lib/view_model/transaction_details_view_model.dart b/lib/view_model/transaction_details_view_model.dart
index a3b7d8a..16f12ac 100644
--- a/lib/view_model/transaction_details_view_model.dart
+++ b/lib/view_model/transaction_details_view_model.dart
@@ -391,8 +391,8 @@ abstract class TransactionDetailsViewModelBase with Store {
}
String get formattedTitle {
- if (transactionInfo.additionalInfo['autoShield'] == true) {
- return "Autoshield";
+ if (transactionInfo.additionalInfo['isAutoShield'] == true) {
+ return S.current.shielding;
}
if (transactionInfo.direction == TransactionDirection.incoming) {
return S.current.received;
diff --git a/lib/zcash/cw_zcash.dart b/lib/zcash/cw_zcash.dart
index ee6505f..1284aa3 100644
--- a/lib/zcash/cw_zcash.dart
+++ b/lib/zcash/cw_zcash.dart
@@ -209,16 +209,10 @@ class CWZcash extends Zcash {
}
@override
- bool showMissingFundsCard(WalletBase wallet) {
- final zcashWallet = wallet as ZcashWallet;
- return zcashWallet.couldBeZashiWallet();
- }
+ bool showMissingFundsCard(WalletBase wallet) => false;
@override
- Future<void> rescanInternalChange(WalletBase wallet) {
- final zcashWallet = wallet as ZcashWallet;
- return zcashWallet.rescanInternalChange();
- }
+ Future<void> rescanInternalChange(WalletBase wallet) async {}
}
const wordList = [
diff --git a/pubspec_base.yaml b/pubspec_base.yaml
index ee0c0f8..fb00813 100644
--- a/pubspec_base.yaml
+++ b/pubspec_base.yaml
@@ -268,8 +268,6 @@ flutter:
- assets/text/
- assets/faq/
- assets/animation/
- - scripts/zcash_lib/assets/sapling-spend.params
- - scripts/zcash_lib/assets/sapling-output.params
- assets/new-ui/
- assets/new-ui/icons/
- assets/new-ui/hero/
diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb
index efde482..e52f356 100644
--- a/res/values/strings_en.arb
+++ b/res/values/strings_en.arb
@@ -1071,6 +1071,8 @@
"share_this_node": "Share this node",
"shared_seed_wallet_groups": "Shared Seed Wallet Groups",
"shared_seed_wallets": "Shared Seed Wallets",
+ "share_this_node": "Share this node",
+ "shielding": "Autoshield",
"show": "Show",
"show_address_book_popup": "Show Address Book popup",
"show_balance": "Long Press to Show Balance",
Why this scored 44/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.