feat: zkool2 (#3165)

* feat: zkool2

* feat: rescan on zkool
fix: sending to extracted addresses
fix: labeling shielding txs
fix: autoshield endless loop
fix: autoshield t addr not firing
fix: autoshield destinations
chore: remove old routes for rescan with zkool
other minor fixes/cleanups

* fix: zcash hardfork

* fix: update sync progress more reliably

* fix: pending transaction loading
fix: autoshield txs
fix: taddress
fix: fee estimation
fix: tx send errors

* chore: bump zkool2 to latest version
fix: reduce amount of taddrs generated
fix: derivation path for internal wallets
fix: display address in correct hidden/usable spots
fix: get wallet db height before it syncs for the first time
fix: taddr rotation with passphrase
fix: sync progress ui refresh
fix: taddress generation not getting capped
fix: hidden addresses not being hidden
fix: pending transactions not being shown
fix: t address cache misses
fix: t address rotation not getting refreshed
fix: t address high index not being used

* fix: refresh t addresses

* fix: patch rust side to make t addr rotation work

* fix: ui issue in recipient address
fix: zcash multiaddress send to show in the ui
fix: potential race condition

* minor fixes

* format

* fix: flashing on receive screen

* remove unused 'autoShield'

* review
This commit is contained in:
cyan
2026-07-17 16:01:31 +03:00
committed by GitHub
parent 3070612f15
commit 28c2cc152e
33 changed files with 2843 additions and 1651 deletions
@@ -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
+2 -1
View File
@@ -226,4 +226,5 @@ scripts/android/yttrium
scripts/bitbox_flutter
scripts/reown_flutter
*.salive
*.salive
android/.kotlin
+2 -2
View File
@@ -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"
+416
View File
@@ -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;
}
-1
View File
@@ -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';
-196
View File
@@ -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,
};
}
+96
View File
@@ -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)");
}
+31 -49
View File
@@ -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);
Money get amount {
final isAll = credentials.outputs.fold<bool>(false, (final a, final b) => a || (b.sendAll));
if (isAll) {
return availableBalance - fee;
}
return credentials.outputs
.map((final output) => output.cryptoAmount)
.reduce((final a, final b) => a + b);
}
@override
String get amountFormatted => amount.toString();
int get _totalAmount {
final isAll = credentials.outputs.fold<bool>(false, (final a, final b) => a || (b.sendAll));
if (isAll) {
return availableBalance.amount.toInt();
}
return credentials.outputs.fold<int>(
0,
(final a, final b) => a + b.cryptoAmount.amount.toInt(),
);
}
@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();
+5
View File
@@ -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();
}
+118
View File
@@ -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),
};
}
@@ -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.';
}
+480 -351
View File
@@ -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 isSeedForWallet(final String? mainWallet, final String? subWallet) {
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 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<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;
}
if (!firstSession) {
final head = slots.where((final e) => e.txCount == 0).firstOrNull?.dindex;
final key = '$rotationAccountId:$head';
if (_poolRescanAttempted.contains(key)) {
return;
}
_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 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) {
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());
}
deserialize(f.readAsBytesSync());
} catch (e) {
printV("Failed to deserialize T address DB for zcash, it doesn't really matter: $e");
}
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 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()),
),
};
if (kDebugMode) {
return utf8.encode(JsonEncoder.withIndent(" ").convert(data));
static Future<int?> getRotationAccountForMainAccount(final int mainAccountId) async {
final cached = _rotationAccountByMain[mainAccountId];
if (cached != null) {
return cached;
}
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);
}
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(),
),
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;
}
}
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;
}
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,
);
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;
_rotationAccountByMain[mainAccountId] = id;
return id;
} finally {
ZcashWalletBase.runWithCoinMutex.release();
}
}
static Uint8List atob(final String value) =>
Uint8List.fromList(List<int>.from(base64.decode(value)));
// 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));
}
// 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),
);
}
if (toGenerate > 0) {
printV("topped up disposable taddr pool by $toGenerate (had $unused)");
}
}
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;
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})",
);
if (_sweepJobRunning) {
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!;
}
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] = [];
_sweepJobRunning = true;
try {
final mainAccountIds = ZcashWalletBase.walletsByAccountId.keys.toList();
if (mainAccountIds.isEmpty) {
await createAndSweepTAddressesForAccount(c.account);
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;
}
}
printV("didAddNewAccount: ${didAddNewAccount}");
if (didAddNewAccount) {
return createAndSweepTAddresses();
}
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;
for (final mainAccountId in mainAccountIds) {
try {
bal += await WarpApi.getTBalance(coin, accs[j].id);
} catch (e) {
printV("getTBalance: $e");
return;
await createAndSweepTAddressesForAccount(mainAccountId);
} catch (e, s) {
printV("createAndSweepTAddresses for $mainAccountId error: $e");
s.toString().split("\n").forEach(printV);
}
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,
);
}
} finally {
_sweepJobRunning = false;
}
}
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,
static Future<void> createAndSweepTAddressesForAccount(final int cId) async {
if (!_isWalletSynced(cId)) {
printV("rotation sweep waiting for wallet sync (account $cId)");
return;
}
printV("createAndSweepTAddresses for account $cId");
final rotationAccount = await getRotationAccountForMainAccount(cId);
if (rotationAccount == null) {
printV("rotation account is null, bailing out");
return;
}
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;
}
if (transparentBal < BigInt.from(_sweepThreshold)) {
await updateCache(mainAccountId: cId);
await _refreshWalletAfterRotationChange(cId);
return;
}
if (!_canBroadcastSweep(cId)) {
printV("rotation sweep rate-limited for account $cId");
await updateCache(mainAccountId: cId);
await _refreshWalletAfterRotationChange(cId);
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');
}
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();
}
}
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;
}
}
}
shieldedAccountsTx = newShieldedAccountsTx.map((final k, final v) => MapEntry(k, v.toList()));
await serializeToFile();
return;
await ZcashWalletService.addShieldedTx(result);
_lastSweepBroadcastAt[cId] = DateTime.now();
await updateCache(mainAccountId: cId);
await _refreshWalletAfterRotationChange(cId);
}
static ShieldedTx reverseAmount(final ShieldedTx t) {
final tt = t.unpack();
tt.value = -tt.value;
return ShieldedTx(base64.decode(flatBuffersPack(tt.pack)));
static Future<void> _refreshWalletAfterRotationChange(final int mainAccountId) async {
final wallet = ZcashWalletBase.walletsByAccountId[mainAccountId];
if (wallet == null) {
return;
}
unawaited(wallet.updateTransactions());
unawaited(wallet.updateBalance());
}
static Account? accountForSeed(final String seed) {
final accounts = WarpApi.getAccountList(coin);
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);
}
}
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 List<ZkoolTx> rotationTxsForMainAccount(final int mainAccountId) {
return shieldedAccountsTx[mainAccountId] ?? const [];
}
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;
}
}
+14 -50
View File
@@ -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");
}
}
}
File diff suppressed because it is too large Load Diff
+123 -90
View File
@@ -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 "";
}
}
@override
@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 "";
}
}
String get addressForExchange =>
_transparentObservableAddress ?? transparentAddress ?? "unknown addressForExchange";
@override
@computed
String get addressForExchange => transparentAddressRotated ?? transparentAddress;
@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;
addressInfos = {
0:
ZcashTaddressRotation.allAddressesForAccount(accountId)?.map((final v) {
return WalletInfoAddressInfo(
walletInfoId: walletInfo.internalId,
mapKey: ++accountIndex,
accountIndex: 0,
address: v,
label: "",
);
}).toList() ??
[],
};
hiddenAddresses.addAll(
ZcashTaddressRotation.allUsedAddressesForAccount(accountId)?.toSet() ?? {},
final addr = await ZcashWalletBase.runWithCoin(
accountId: accountId,
func: (final coin) => zkool_account.getAddresses(c: coin, uaPools: 7),
);
// addressInfos[0]?.removeWhere((final test) => hiddenAddresses.contains(test.address));
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: [
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();
if (isPlaceholderAddress(walletInfo.address)) {
final resolved = _resolvedWalletInfoAddress();
if (!isPlaceholderAddress(resolved)) {
walletInfo.address = resolved;
await walletInfo.save();
}
}
await saveAddressesInBox();
}
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
+33 -20
View File
@@ -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);
+3
View File
@@ -0,0 +1,3 @@
import 'dart:typed_data';
final zecBase = Uint8List(32);
+312
View File
@@ -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?,
),
),
),
);
}
}
+4 -4
View File
@@ -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
+71 -15
View File
@@ -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
-4
View File
@@ -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 */,
+5 -1
View File
@@ -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()}");
+1 -1
View File
@@ -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,
@@ -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),
],
);
}
}
@@ -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();
}
}
@@ -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,
@@ -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);
@@ -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;
+4 -2
View File
@@ -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();
});
}
@@ -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;
+2 -8
View File
@@ -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 = [
-2
View File
@@ -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/
+2
View File
@@ -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",