feat: zano hf + bip39 (#3359)

* feat: bip39 zano hf

* fix: correct monero_c hash

* lockfile [skip ci]

* fix: zano: proper bip32
fix: zano respect 24 word setting
fix: rename / delete

* fix: nonBip39WalletTypes

* fix: zano wallet rename

* fix: passphrase store zano

---------

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>
This commit is contained in:
cyan
2026-08-28 15:49:51 +08:00
committed by GitHub
co-authored by Omar Hatem
parent 14167a85e8
commit 905db64f77
27 changed files with 1074 additions and 106 deletions
+1 -1
View File
@@ -26,7 +26,7 @@ dependencies:
monero:
git:
url: https://github.com/mrcyjanek/monero_c.git
ref: 3bfb3856a838f2bf6b729501837bb0295dedf25d
ref: cd4fe366cc8d3c188c91de279f43b29c0e044b15
path: impls/monero.dart
mutex: ^3.1.0
ledger_flutter_plus: ^1.4.1
+1 -1
View File
@@ -24,7 +24,7 @@ dependencies:
monero:
git:
url: https://github.com/mrcyjanek/monero_c.git
ref: 3bfb3856a838f2bf6b729501837bb0295dedf25d
ref: cd4fe366cc8d3c188c91de279f43b29c0e044b15
path: impls/monero.dart
mutex: ^3.1.0
+7 -2
View File
@@ -66,8 +66,13 @@ class Transfer {
? []
: (json['remote_aliases'] as List<dynamic>).cast<String>(),
showSender: json['show_sender'] as bool? ?? false,
subtransfers: (json['subtransfers'] as List<dynamic>? ?? [])
.map((e) => Subtransfer.fromJson(e as Map<String, dynamic>))
subtransfers: (json['subtransfers_by_pid'] as List<dynamic>? ?? [])
.whereType<Map<String, dynamic>>()
.expand(
(group) => (group['subtransfers'] as List<dynamic>? ?? [])
.whereType<Map<String, dynamic>>()
.map(Subtransfer.fromJson),
)
.toList(),
timestamp: json['timestamp'] as int? ?? 0,
transferInternalIndex: json['transfer_internal_index'] == null
+60
View File
@@ -0,0 +1,60 @@
import 'dart:typed_data';
import 'package:bip32/bip32.dart' as bip32;
import 'package:bip39/bip39.dart' as bip39;
import 'package:bip39/src/wordlists/english.dart' as bip39_english;
bool isBip39Seed(String mnemonic) => bip39.validateMnemonic(mnemonic);
List<String> get bip39EnglishWords => List<String>.from(bip39_english.WORDLIST);
String generateBip39Mnemonic({int strength = 128}) =>
bip39.generateMnemonic(strength: strength);
String getSecretDerivationFromBip39(String mnemonic, {String passphrase = ''}) {
final seed = bip39.mnemonicToSeed(mnemonic, passphrase: passphrase);
final bip32KeyPair = bip32.BIP32.fromSeed(seed).derivePath("m/44'/128'/0'/0/0");
return _reduceECKey(bip32KeyPair.privateKey!).toHexString();
}
const _ed25519CurveOrder = "1000000000000000000000000000000014DEF9DEA2F79CD65812631A5CF5D3ED";
Uint8List _reduceECKey(Uint8List buffer) {
final curveOrder = BigInt.parse(_ed25519CurveOrder, radix: 16);
final bigNumber = _readBytes(buffer);
var result = bigNumber % curveOrder;
final resultBuffer = Uint8List(32);
for (var i = 0; i < 32; i++) {
resultBuffer[i] = (result & BigInt.from(0xff)).toInt();
result = result >> 8;
}
return resultBuffer;
}
/// Read BigInt from a little-endian Uint8List
/// From https://github.com/dart-lang/sdk/issues/32803#issuecomment-387405784
BigInt _readBytes(Uint8List bytes) {
BigInt read(int start, int end) {
if (end - start <= 4) {
var result = 0;
for (int i = end - 1; i >= start; i--) {
result = result * 256 + bytes[i];
}
return BigInt.from(result);
}
final mid = start + ((end - start) >> 1);
return read(start, mid) + read(mid, end) * (BigInt.one << ((mid - start) * 8));
}
return read(0, bytes.length);
}
extension on Uint8List {
String toHexString() =>
map((b) => b.toRadixString(16).padLeft(2, '0')).join();
}
+4
View File
@@ -0,0 +1,4 @@
NODE=http://37.27.100.59:10500/json_rpc
HEIGHT=$(curl -s -X POST $NODE -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","id":"0","method":"getinfo"}' | python3 -c "import sys,json; print(json.load(sys.stdin)['result']['height'])")
echo "_Checkpoint($(curl -s -X POST $NODE -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","id":"0","method":"getblockheaderbyheight","params":{"height":1}}' | python3 -c "import sys,json; print(json.load(sys.stdin)['result']['block_header']['timestamp'])"), 1),"
for i in $(seq 1 $((HEIGHT/10000))); do echo "_Checkpoint($(curl -s -X POST $NODE -H 'Content-Type: application/json' -d "{\"jsonrpc\":\"2.0\",\"id\":\"0\",\"method\":\"getblockheaderbyheight\",\"params\":{\"height\":${i}0000}}" | python3 -c "import sys,json; print(json.load(sys.stdin)['result']['block_header']['timestamp'])"), ${i}0000),"; done
+467
View File
@@ -0,0 +1,467 @@
class ZanoHeight {
static final List<_Checkpoint> _points = [
_Checkpoint(1557342384, 1),
_Checkpoint(1557906733, 10000),
_Checkpoint(1558516097, 20000),
_Checkpoint(1559119125, 30000),
_Checkpoint(1559741805, 40000),
_Checkpoint(1560341175, 50000),
_Checkpoint(1560948480, 60000),
_Checkpoint(1561555965, 70000),
_Checkpoint(1562142135, 80000),
_Checkpoint(1562749400, 90000),
_Checkpoint(1563354315, 100000),
_Checkpoint(1563958673, 110000),
_Checkpoint(1564559835, 120000),
_Checkpoint(1565159325, 130000),
_Checkpoint(1565772195, 140000),
_Checkpoint(1566389235, 150000),
_Checkpoint(1566992793, 160000),
_Checkpoint(1567597678, 170000),
_Checkpoint(1568204955, 180000),
_Checkpoint(1568812530, 190000),
_Checkpoint(1569418329, 200000),
_Checkpoint(1570035660, 210000),
_Checkpoint(1570641964, 220000),
_Checkpoint(1571242140, 230000),
_Checkpoint(1571841282, 240000),
_Checkpoint(1572443911, 250000),
_Checkpoint(1573049280, 260000),
_Checkpoint(1573653255, 270000),
_Checkpoint(1574251680, 280000),
_Checkpoint(1574854984, 290000),
_Checkpoint(1575459060, 300000),
_Checkpoint(1576064640, 310000),
_Checkpoint(1576665225, 320000),
_Checkpoint(1577271045, 330000),
_Checkpoint(1577867835, 340000),
_Checkpoint(1578470370, 350000),
_Checkpoint(1579078170, 360000),
_Checkpoint(1579674765, 370000),
_Checkpoint(1580277860, 380000),
_Checkpoint(1580878155, 390000),
_Checkpoint(1581482220, 400000),
_Checkpoint(1582087577, 410000),
_Checkpoint(1582689270, 420000),
_Checkpoint(1583284005, 430000),
_Checkpoint(1583880062, 440000),
_Checkpoint(1584480692, 450000),
_Checkpoint(1585085520, 460000),
_Checkpoint(1585694261, 470000),
_Checkpoint(1586292210, 480000),
_Checkpoint(1586893822, 490000),
_Checkpoint(1587498656, 500000),
_Checkpoint(1588099364, 510000),
_Checkpoint(1588696710, 520000),
_Checkpoint(1589300843, 530000),
_Checkpoint(1589904930, 540000),
_Checkpoint(1590499185, 550000),
_Checkpoint(1591111133, 560000),
_Checkpoint(1591715587, 570000),
_Checkpoint(1592316375, 580000),
_Checkpoint(1592918012, 590000),
_Checkpoint(1593521050, 600000),
_Checkpoint(1594123886, 610000),
_Checkpoint(1594725660, 620000),
_Checkpoint(1595326710, 630000),
_Checkpoint(1595929088, 640000),
_Checkpoint(1596535020, 650000),
_Checkpoint(1597132395, 660000),
_Checkpoint(1597740870, 670000),
_Checkpoint(1598339827, 680000),
_Checkpoint(1598945010, 690000),
_Checkpoint(1599547530, 700000),
_Checkpoint(1600153050, 710000),
_Checkpoint(1600754010, 720000),
_Checkpoint(1601355810, 730000),
_Checkpoint(1601956635, 740000),
_Checkpoint(1602558615, 750000),
_Checkpoint(1603155648, 760000),
_Checkpoint(1603759399, 770000),
_Checkpoint(1604365586, 780000),
_Checkpoint(1604966602, 790000),
_Checkpoint(1605564777, 800000),
_Checkpoint(1606168023, 810000),
_Checkpoint(1606772409, 820000),
_Checkpoint(1607377110, 830000),
_Checkpoint(1607988084, 840000),
_Checkpoint(1608593385, 850000),
_Checkpoint(1609196310, 860000),
_Checkpoint(1609793426, 870000),
_Checkpoint(1610398975, 880000),
_Checkpoint(1610999055, 890000),
_Checkpoint(1611601290, 900000),
_Checkpoint(1612208050, 910000),
_Checkpoint(1612805250, 920000),
_Checkpoint(1613409645, 930000),
_Checkpoint(1614003255, 940000),
_Checkpoint(1614600803, 950000),
_Checkpoint(1615202091, 960000),
_Checkpoint(1615802123, 970000),
_Checkpoint(1616414192, 980000),
_Checkpoint(1617015705, 990000),
_Checkpoint(1617613785, 1000000),
_Checkpoint(1618218467, 1010000),
_Checkpoint(1618821464, 1020000),
_Checkpoint(1619424052, 1030000),
_Checkpoint(1620027510, 1040000),
_Checkpoint(1620630585, 1050000),
_Checkpoint(1621232355, 1060000),
_Checkpoint(1621832220, 1070000),
_Checkpoint(1622426032, 1080000),
_Checkpoint(1623041250, 1090000),
_Checkpoint(1623644732, 1100000),
_Checkpoint(1624244801, 1110000),
_Checkpoint(1624848450, 1120000),
_Checkpoint(1625449290, 1130000),
_Checkpoint(1626049974, 1140000),
_Checkpoint(1626656033, 1150000),
_Checkpoint(1627255830, 1160000),
_Checkpoint(1627855505, 1170000),
_Checkpoint(1628464065, 1180000),
_Checkpoint(1629064455, 1190000),
_Checkpoint(1629665715, 1200000),
_Checkpoint(1630268475, 1210000),
_Checkpoint(1630870911, 1220000),
_Checkpoint(1631475471, 1230000),
_Checkpoint(1632071441, 1240000),
_Checkpoint(1632677576, 1250000),
_Checkpoint(1633286445, 1260000),
_Checkpoint(1633884645, 1270000),
_Checkpoint(1634480355, 1280000),
_Checkpoint(1635077039, 1290000),
_Checkpoint(1635679740, 1300000),
_Checkpoint(1636284345, 1310000),
_Checkpoint(1636882865, 1320000),
_Checkpoint(1637482276, 1330000),
_Checkpoint(1638082296, 1340000),
_Checkpoint(1638682601, 1350000),
_Checkpoint(1639287225, 1360000),
_Checkpoint(1639884465, 1370000),
_Checkpoint(1640484903, 1380000),
_Checkpoint(1641084033, 1390000),
_Checkpoint(1641688500, 1400000),
_Checkpoint(1642286685, 1410000),
_Checkpoint(1642886842, 1420000),
_Checkpoint(1643493960, 1430000),
_Checkpoint(1644093045, 1440000),
_Checkpoint(1644691833, 1450000),
_Checkpoint(1645291245, 1460000),
_Checkpoint(1645892190, 1470000),
_Checkpoint(1646489895, 1480000),
_Checkpoint(1647089648, 1490000),
_Checkpoint(1647694665, 1500000),
_Checkpoint(1648293585, 1510000),
_Checkpoint(1648893525, 1520000),
_Checkpoint(1649497306, 1530000),
_Checkpoint(1650098656, 1540000),
_Checkpoint(1650694980, 1550000),
_Checkpoint(1651303650, 1560000),
_Checkpoint(1651899285, 1570000),
_Checkpoint(1652501596, 1580000),
_Checkpoint(1653101220, 1590000),
_Checkpoint(1653700166, 1600000),
_Checkpoint(1654302094, 1610000),
_Checkpoint(1654906575, 1620000),
_Checkpoint(1655510644, 1630000),
_Checkpoint(1656108615, 1640000),
_Checkpoint(1656712545, 1650000),
_Checkpoint(1657313460, 1660000),
_Checkpoint(1657911515, 1670000),
_Checkpoint(1658515717, 1680000),
_Checkpoint(1659121230, 1690000),
_Checkpoint(1659722213, 1700000),
_Checkpoint(1660318875, 1710000),
_Checkpoint(1660921522, 1720000),
_Checkpoint(1661518436, 1730000),
_Checkpoint(1662118050, 1740000),
_Checkpoint(1662717982, 1750000),
_Checkpoint(1663306425, 1760000),
_Checkpoint(1663904520, 1770000),
_Checkpoint(1664511195, 1780000),
_Checkpoint(1665113460, 1790000),
_Checkpoint(1665705148, 1800000),
_Checkpoint(1666306807, 1810000),
_Checkpoint(1666909050, 1820000),
_Checkpoint(1667510577, 1830000),
_Checkpoint(1668111697, 1840000),
_Checkpoint(1668711150, 1850000),
_Checkpoint(1669309785, 1860000),
_Checkpoint(1669910715, 1870000),
_Checkpoint(1670511885, 1880000),
_Checkpoint(1671111596, 1890000),
_Checkpoint(1671713348, 1900000),
_Checkpoint(1672312933, 1910000),
_Checkpoint(1672913610, 1920000),
_Checkpoint(1673517614, 1930000),
_Checkpoint(1674116655, 1940000),
_Checkpoint(1674717105, 1950000),
_Checkpoint(1675316550, 1960000),
_Checkpoint(1675914240, 1970000),
_Checkpoint(1676519608, 1980000),
_Checkpoint(1677113500, 1990000),
_Checkpoint(1677715095, 2000000),
_Checkpoint(1678316835, 2010000),
_Checkpoint(1678912224, 2020000),
_Checkpoint(1679513175, 2030000),
_Checkpoint(1680112070, 2040000),
_Checkpoint(1680711218, 2050000),
_Checkpoint(1681313478, 2060000),
_Checkpoint(1681909155, 2070000),
_Checkpoint(1682507671, 2080000),
_Checkpoint(1683107382, 2090000),
_Checkpoint(1683712020, 2100000),
_Checkpoint(1684311975, 2110000),
_Checkpoint(1684915377, 2120000),
_Checkpoint(1685514352, 2130000),
_Checkpoint(1686112770, 2140000),
_Checkpoint(1686710430, 2150000),
_Checkpoint(1687309584, 2160000),
_Checkpoint(1687916053, 2170000),
_Checkpoint(1688521450, 2180000),
_Checkpoint(1689118965, 2190000),
_Checkpoint(1689719584, 2200000),
_Checkpoint(1690321515, 2210000),
_Checkpoint(1690920915, 2220000),
_Checkpoint(1691524920, 2230000),
_Checkpoint(1692120135, 2240000),
_Checkpoint(1692715440, 2250000),
_Checkpoint(1693313578, 2260000),
_Checkpoint(1693915410, 2270000),
_Checkpoint(1694516824, 2280000),
_Checkpoint(1695114075, 2290000),
_Checkpoint(1695712485, 2300000),
_Checkpoint(1696314266, 2310000),
_Checkpoint(1696913190, 2320000),
_Checkpoint(1697513447, 2330000),
_Checkpoint(1698115590, 2340000),
_Checkpoint(1698715860, 2350000),
_Checkpoint(1699318665, 2360000),
_Checkpoint(1699918515, 2370000),
_Checkpoint(1700510340, 2380000),
_Checkpoint(1701107490, 2390000),
_Checkpoint(1701715953, 2400000),
_Checkpoint(1702319655, 2410000),
_Checkpoint(1702921425, 2420000),
_Checkpoint(1703518740, 2430000),
_Checkpoint(1704117450, 2440000),
_Checkpoint(1704715350, 2450000),
_Checkpoint(1705314150, 2460000),
_Checkpoint(1705911585, 2470000),
_Checkpoint(1706513190, 2480000),
_Checkpoint(1707111871, 2490000),
_Checkpoint(1707712515, 2500000),
_Checkpoint(1708316785, 2510000),
_Checkpoint(1708917275, 2520000),
_Checkpoint(1709515140, 2530000),
_Checkpoint(1710115320, 2540000),
_Checkpoint(1710718811, 2550000),
_Checkpoint(1711340233, 2560000),
_Checkpoint(1711937827, 2570000),
_Checkpoint(1712536797, 2580000),
_Checkpoint(1713132840, 2590000),
_Checkpoint(1713732780, 2600000),
_Checkpoint(1714330320, 2610000),
_Checkpoint(1714933845, 2620000),
_Checkpoint(1715536185, 2630000),
_Checkpoint(1716146082, 2640000),
_Checkpoint(1716742155, 2650000),
_Checkpoint(1717343205, 2660000),
_Checkpoint(1717937699, 2670000),
_Checkpoint(1718535495, 2680000),
_Checkpoint(1719141615, 2690000),
_Checkpoint(1719744075, 2700000),
_Checkpoint(1720336724, 2710000),
_Checkpoint(1720941803, 2720000),
_Checkpoint(1721539733, 2730000),
_Checkpoint(1722139174, 2740000),
_Checkpoint(1722737769, 2750000),
_Checkpoint(1723336755, 2760000),
_Checkpoint(1723937074, 2770000),
_Checkpoint(1724533860, 2780000),
_Checkpoint(1725126437, 2790000),
_Checkpoint(1725728340, 2800000),
_Checkpoint(1726330290, 2810000),
_Checkpoint(1726931550, 2820000),
_Checkpoint(1727531335, 2830000),
_Checkpoint(1728132893, 2840000),
_Checkpoint(1728731641, 2850000),
_Checkpoint(1729328707, 2860000),
_Checkpoint(1729927125, 2870000),
_Checkpoint(1730525159, 2880000),
_Checkpoint(1731124123, 2890000),
_Checkpoint(1731728397, 2900000),
_Checkpoint(1732328405, 2910000),
_Checkpoint(1732929444, 2920000),
_Checkpoint(1733527918, 2930000),
_Checkpoint(1734125775, 2940000),
_Checkpoint(1734721755, 2950000),
_Checkpoint(1735326599, 2960000),
_Checkpoint(1735919505, 2970000),
_Checkpoint(1736520114, 2980000),
_Checkpoint(1737119579, 2990000),
_Checkpoint(1737724515, 3000000),
_Checkpoint(1738328220, 3010000),
_Checkpoint(1738929119, 3020000),
_Checkpoint(1739527564, 3030000),
_Checkpoint(1740125219, 3040000),
_Checkpoint(1740726039, 3050000),
_Checkpoint(1741327110, 3060000),
_Checkpoint(1741927857, 3070000),
_Checkpoint(1742530920, 3080000),
_Checkpoint(1743149987, 3090000),
_Checkpoint(1743747268, 3100000),
_Checkpoint(1744349415, 3110000),
_Checkpoint(1744944112, 3120000),
_Checkpoint(1745543580, 3130000),
_Checkpoint(1746142732, 3140000),
_Checkpoint(1746742705, 3150000),
_Checkpoint(1747344191, 3160000),
_Checkpoint(1747946610, 3170000),
_Checkpoint(1748545387, 3180000),
_Checkpoint(1749144165, 3190000),
_Checkpoint(1749742950, 3200000),
_Checkpoint(1750343363, 3210000),
_Checkpoint(1750946445, 3220000),
_Checkpoint(1751545747, 3230000),
_Checkpoint(1752144620, 3240000),
_Checkpoint(1752746280, 3250000),
_Checkpoint(1753343664, 3260000),
_Checkpoint(1753948349, 3270000),
_Checkpoint(1754547975, 3280000),
_Checkpoint(1755145610, 3290000),
_Checkpoint(1755744210, 3300000),
_Checkpoint(1756341045, 3310000),
_Checkpoint(1756940460, 3320000),
_Checkpoint(1757537565, 3330000),
_Checkpoint(1758141330, 3340000),
_Checkpoint(1758742563, 3350000),
_Checkpoint(1759340190, 3360000),
_Checkpoint(1759941840, 3370000),
_Checkpoint(1760542420, 3380000),
_Checkpoint(1761144358, 3390000),
_Checkpoint(1761745435, 3400000),
_Checkpoint(1762345095, 3410000),
_Checkpoint(1762942865, 3420000),
_Checkpoint(1763540460, 3430000),
_Checkpoint(1764144321, 3440000),
_Checkpoint(1764745065, 3450000),
_Checkpoint(1765347169, 3460000),
_Checkpoint(1765944370, 3470000),
_Checkpoint(1766548020, 3480000),
_Checkpoint(1767144555, 3490000),
_Checkpoint(1767748830, 3500000),
_Checkpoint(1768350722, 3510000),
_Checkpoint(1768945425, 3520000),
_Checkpoint(1769545406, 3530000),
_Checkpoint(1770144700, 3540000),
_Checkpoint(1770745380, 3550000),
_Checkpoint(1771345176, 3560000),
_Checkpoint(1771943526, 3570000),
_Checkpoint(1772545646, 3580000),
_Checkpoint(1773146830, 3590000),
_Checkpoint(1773749868, 3600000),
_Checkpoint(1774347077, 3610000),
_Checkpoint(1774950120, 3620000),
_Checkpoint(1775548635, 3630000),
_Checkpoint(1776148320, 3640000),
_Checkpoint(1776750266, 3650000),
_Checkpoint(1777351110, 3660000),
_Checkpoint(1777953826, 3670000),
_Checkpoint(1778551170, 3680000),
_Checkpoint(1779153960, 3690000),
_Checkpoint(1779758055, 3700000),
_Checkpoint(1780362465, 3710000),
_Checkpoint(1780966500, 3720000),
_Checkpoint(1781563275, 3730000),
_Checkpoint(1782174166, 3740000),
_Checkpoint(1782775815, 3750000),
_Checkpoint(1783378725, 3760000),
_Checkpoint(1783971435, 3770000),
_Checkpoint(1784575305, 3780000),
_Checkpoint(1785177870, 3790000),
_Checkpoint(1785774750, 3800000),
_Checkpoint(1786377455, 3810000),
_Checkpoint(1786978902, 3820000),
];
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;
}
+10 -1
View File
@@ -1,9 +1,18 @@
import 'dart:convert';
import 'package:monero/zano.dart' as zano;
import 'package:cw_zano/api/model/get_address_info_result.dart';
import 'package:cw_zano/get_height_by_date.dart';
import 'package:monero/zano.dart' as zano;
class ZanoUtils {
static int heightByDate(DateTime date) =>
ZanoHeight.getBlockHeightByTime(date.toUtc().subtract(const Duration(days: 1)));
static int creationTimestampFromHeight(int height) {
if (height <= 0) return 0;
return ZanoHeight.getTimeByBlockHeight(height).millisecondsSinceEpoch ~/ 1000;
}
static bool validateAddress(String address) {
try {
final result = GetAddressInfoResult.fromJson(
+274 -44
View File
@@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:convert';
import 'dart:core';
import 'dart:io';
import 'dart:math';
@@ -6,6 +7,7 @@ import 'dart:math';
import 'package:cw_core/amount/money.dart';
import 'package:cw_core/cake_hive.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/encryption_file_utils.dart';
import 'package:cw_core/node.dart';
import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/pending_transaction.dart';
@@ -15,6 +17,7 @@ import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_credentials.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_keys_file.dart';
import 'package:cw_core/zano_asset.dart';
import 'package:cw_zano/api/model/create_wallet_result.dart';
import 'package:cw_zano/api/model/destination.dart';
@@ -32,7 +35,10 @@ import 'package:cw_zano/zano_wallet_addresses.dart';
import 'package:cw_zano/zano_wallet_api.dart';
import 'package:cw_zano/zano_wallet_exceptions.dart';
import 'package:cw_zano/zano_wallet_service.dart';
import 'package:cw_zano/zano_utils.dart';
import 'package:cw_zano/api/model/balance.dart';
import 'package:cw_zano/bip39_seed.dart';
import 'package:path/path.dart' as p;
import 'package:mobx/mobx.dart';
@@ -42,10 +48,12 @@ class ZanoWallet = ZanoWalletBase with _$ZanoWallet;
abstract class ZanoWalletBase
extends WalletBase<ZanoBalance, ZanoTransactionHistory, ZanoTransactionInfo>
with Store, ZanoWalletApi {
with Store, ZanoWalletApi, WalletKeysFile {
static const int _autoSaveIntervalSeconds = 30;
static const int _pollIntervalMilliseconds = 5000;
static const int _maxLoadAssetsRetries = 5;
static const int _walletStateReady = 2;
static const String _cakeKeysFileSuffix = '.cw.keys';
@override
void setPassword(String password) {
@@ -54,10 +62,23 @@ abstract class ZanoWalletBase
}
String _password;
final EncryptionFileUtils _encryptionFileUtils;
bool _didSyncSecrets = false;
@override
String get password => _password;
@override
Future<String> makeKeysFilePath() async => "${await makePath()}$_cakeKeysFileSuffix";
@override
WalletKeysData get walletKeysData => WalletKeysData(
mnemonic: _hasSeed ? seed : null,
passphrase: (passphrase ?? '').isEmpty ? null : passphrase,
);
bool get _hasSeed => seed.trim().isNotEmpty;
@override
Future<String> signMessage(String message, {String? address = null}) =>
super.signMessage(message, address: address);
@@ -105,7 +126,8 @@ abstract class ZanoWalletBase
/// number of transactions in each request
static final int _txChunkSize = (pow(2, 32) - 1).toInt();
ZanoWalletBase(WalletInfo walletInfo, DerivationInfo derivationInfo, String password)
ZanoWalletBase(WalletInfo walletInfo, DerivationInfo derivationInfo, String password,
this._encryptionFileUtils)
: balance = ObservableMap.of({CryptoCurrency.zano: ZanoBalance.empty(CryptoCurrency.zano)}),
_isTransactionUpdating = false,
_hasSyncAfterStartup = false,
@@ -126,74 +148,138 @@ abstract class ZanoWalletBase
@override
Future<void> changePassword(String password) async {
setPassword(password);
if (_hasSeed) {
await saveKeysFile(_password, _encryptionFileUtils);
saveKeysFile(_password, _encryptionFileUtils, true);
}
}
static Future<ZanoWallet> create({required WalletCredentials credentials}) async {
static Future<ZanoWallet> create({
required WalletCredentials credentials,
required EncryptionFileUtils encryptionFileUtils,
}) async {
final wallet = ZanoWallet(credentials.walletInfo!,
await credentials.walletInfo!.getDerivationInfo(), credentials.password!);
await credentials.walletInfo!.getDerivationInfo(), credentials.password!, encryptionFileUtils);
await wallet.initWallet();
final path = await pathForWallet(name: credentials.name, type: credentials.walletInfo!.type);
final createWalletResult = await wallet.createWallet(path, credentials.password!);
final strength = credentials.seedPhraseLength == 24 ? 256 : 128;
final providedMnemonic =
credentials is ZanoNewWalletCredentials ? credentials.mnemonic : null;
final mnemonic = providedMnemonic ?? generateBip39Mnemonic(strength: strength);
final passphrase = credentials.passphrase ?? '';
final secretDerivation = getSecretDerivationFromBip39(mnemonic, passphrase: passphrase);
final creationTimestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000;
final createWalletResult = await wallet.restoreWalletFromDerivations(
path,
credentials.password!,
secretDerivation,
creationTimestamp: creationTimestamp,
);
await wallet.initWallet();
wallet.seed = mnemonic;
wallet.passphrase = passphrase;
await wallet.parseCreateWalletResult(createWalletResult);
if (credentials.passphrase != null) {
await wallet.setPassphrase(credentials.passphrase!);
wallet.seed = await createWalletResult.seed(wallet);
wallet.passphrase = await wallet.getPassphrase();
}
await wallet._syncSecretsAndDerivation();
await wallet.init(createWalletResult.wi.address);
await wallet.save();
return wallet;
}
static Future<ZanoWallet> restore(
{required ZanoRestoreWalletFromSeedCredentials credentials}) async {
static Future<ZanoWallet> restore({
required ZanoRestoreWalletFromSeedCredentials credentials,
required EncryptionFileUtils encryptionFileUtils,
}) async {
final wallet = ZanoWallet(credentials.walletInfo!,
await credentials.walletInfo!.getDerivationInfo(), credentials.password!);
await credentials.walletInfo!.getDerivationInfo(), credentials.password!, encryptionFileUtils);
await wallet.initWallet();
final path = await pathForWallet(name: credentials.name, type: credentials.walletInfo!.type);
final createWalletResult = await wallet.restoreWalletFromSeed(
path, credentials.password!, credentials.mnemonic, credentials.passphrase);
await wallet.initWallet();
await wallet.parseCreateWalletResult(createWalletResult);
if (credentials.passphrase != null) {
await wallet.setPassphrase(credentials.passphrase!);
wallet.seed = await createWalletResult.seed(wallet);
wallet.passphrase = await wallet.getPassphrase();
final passphrase = credentials.passphrase ?? '';
final CreateWalletResult createWalletResult;
final isBip39 = isBip39Seed(credentials.mnemonic);
final creationTimestamp = ZanoUtils.creationTimestampFromHeight(credentials.height ?? 0);
if (isBip39) {
final secretDerivation =
getSecretDerivationFromBip39(credentials.mnemonic, passphrase: passphrase);
createWalletResult = await wallet.restoreWalletFromDerivations(
path,
credentials.password!,
secretDerivation,
creationTimestamp: creationTimestamp,
);
} else {
createWalletResult = await wallet.restoreWalletFromSeed(
path,
credentials.password!,
credentials.mnemonic,
credentials.passphrase,
creationTimestamp: creationTimestamp,
);
}
await wallet.initWallet();
wallet.seed = credentials.mnemonic;
wallet.passphrase = passphrase;
await wallet.parseCreateWalletResult(createWalletResult);
if (!isBip39) {
try {
if (passphrase.isNotEmpty) {
await wallet.setPassphrase(passphrase);
}
final nativeSeed = await createWalletResult.seed(wallet);
if (nativeSeed.isNotEmpty) {
wallet.seed = nativeSeed;
}
} catch (e) {
printV('native seed not available yet: $e');
}
}
await wallet._syncSecretsAndDerivation();
await wallet.init(createWalletResult.wi.address);
await wallet.save();
return wallet;
}
static Future<ZanoWallet> open(
{required String name, required String password, required WalletInfo walletInfo}) async {
{required String name,
required String password,
required WalletInfo walletInfo,
required EncryptionFileUtils encryptionFileUtils}) async {
final path = await pathForWallet(name: name, type: walletInfo.type);
final wallet =
ZanoWallet(walletInfo, await walletInfo.getDerivationInfo(), password, encryptionFileUtils);
await wallet._loadKeysFileIfPresent();
late final CreateWalletResult createWalletResult;
if (ZanoWalletApi.openWalletCache[path] != null) {
final wallet = ZanoWallet(walletInfo, await walletInfo.getDerivationInfo(), password);
await wallet.parseCreateWalletResult(ZanoWalletApi.openWalletCache[path]!).then((_) {
unawaited(wallet.init(ZanoWalletApi.openWalletCache[path]!.wi.address));
});
return wallet;
createWalletResult = ZanoWalletApi.openWalletCache[path]!;
} else {
final wallet = ZanoWallet(walletInfo, await walletInfo.getDerivationInfo(), password);
await wallet.initWallet();
final createWalletResult = await wallet.loadWallet(path, password);
await wallet.parseCreateWalletResult(createWalletResult).then((_) {
unawaited(wallet.init(createWalletResult.wi.address));
});
return wallet;
createWalletResult = await wallet.loadWallet(path, password);
}
await wallet.parseCreateWalletResult(createWalletResult);
if (!wallet._hasSeed) {
await wallet._loadSeedIfAvailable();
}
await wallet._syncSecretsAndDerivation();
unawaited(wallet.init(createWalletResult.wi.address));
return wallet;
}
Future<void> parseCreateWalletResult(CreateWalletResult result) async {
hWallet = result.walletId;
seed = await result.seed(this);
keys = ZanoWalletKeys(
privateSpendKey: result.privateSpendKey,
privateViewKey: result.privateViewKey,
publicSpendKey: result.publicSpendKey,
publicViewKey: result.publicViewKey,
);
passphrase = await getPassphrase();
if ((passphrase ?? '').isEmpty) {
try {
passphrase = await getPassphrase();
} catch (e) {
printV('passphrase not available yet: $e');
}
}
printV('setting hWallet = ${result.walletId}');
walletAddresses.address = result.wi.address;
@@ -214,6 +300,126 @@ abstract class ZanoWalletBase
}
}
bool _isWalletReady(GetWalletStatusResult walletStatus) =>
!walletStatus.isInLongRefresh && walletStatus.walletState == _walletStateReady;
Future<bool> _hasKeysFile() async {
try {
final path = await makeKeysFilePath();
return File(path).existsSync() || File('$path.backup').existsSync();
} catch (_) {
return false;
}
}
Future<void> _loadKeysFileIfPresent() async {
if (!await _hasKeysFile()) {
return;
}
final path = await makeKeysFilePath();
Future<WalletKeysData> read(String readPath) async {
final jsonSource = await _encryptionFileUtils.read(path: readPath, password: _password);
return WalletKeysData.fromJSON(json.decode(jsonSource) as Map<String, dynamic>);
}
try {
WalletKeysData keysData;
if (File(path).existsSync()) {
keysData = await read(path);
} else {
keysData = await read('$path.backup');
await saveKeysFile(_password, _encryptionFileUtils);
}
if (keysData.mnemonic != null && keysData.mnemonic!.trim().isNotEmpty) {
seed = keysData.mnemonic!;
}
if (keysData.passphrase != null && keysData.passphrase!.isNotEmpty) {
passphrase = keysData.passphrase;
}
} catch (e) {
printV('error reading zano keys file $e');
}
}
Future<void> _saveKeysFileIfNeeded() async {
if (!_hasSeed || await _hasKeysFile()) {
return;
}
await saveKeysFile(_password, _encryptionFileUtils);
saveKeysFile(_password, _encryptionFileUtils, true);
}
Future<void> _loadSeedIfAvailable() async {
if (!isBip39Seed(seed)) {
try {
final loaded = await getSeed();
if (loaded.isNotEmpty) {
seed = loaded;
}
} catch (e) {
printV('seed not available yet: $e');
}
}
if ((passphrase ?? '').isEmpty) {
try {
passphrase = await getPassphrase() ?? passphrase;
} catch (e) {
printV('passphrase not available yet: $e');
}
}
}
Future<void> _syncSecretsAndDerivation() async {
if (!_hasSeed || _didSyncSecrets) {
return;
}
try {
await _persistSecretsIfNeeded();
await _setDerivationForSeed();
await _saveKeysFileIfNeeded();
_didSyncSecrets = true;
} catch (e) {
printV('error persisting zano secrets $e');
}
}
Future<void> _persistSecretsIfNeeded() async {
if (isBip39Seed(seed) && await getBip39Mnemonic() == null) {
await setBip39Secrets(
mnemonic: seed,
creationTimestamp: walletInfo.restoreHeight > 0
? ZanoUtils.creationTimestampFromHeight(walletInfo.restoreHeight)
: DateTime.now().millisecondsSinceEpoch ~/ 1000,
);
}
if ((passphrase ?? '').isNotEmpty && await getPassphrase() == null) {
await setPassphrase(passphrase!);
}
}
Future<void> _setDerivationForSeed() async {
if (!_hasSeed) {
return;
}
final di = await walletInfo.getDerivationInfo();
if (isBip39Seed(seed)) {
if (di.derivationType == DerivationType.bip39 &&
(di.derivationPath ?? '').isNotEmpty) {
return;
}
di.derivationType = DerivationType.bip39;
di.derivationPath = "m/44'/128'/0'/0/0";
await di.save();
return;
}
if (di.derivationType != DerivationType.bip39 && di.derivationPath == 'legacy') {
return;
}
di.derivationType = DerivationType.unknown;
di.derivationPath = 'legacy';
await di.save();
}
@override
Future<void> close({bool shouldCleanup = true}) async {
closeWallet(null);
@@ -310,12 +516,13 @@ abstract class ZanoWalletBase
Future<Map<String, ZanoTransactionInfo>> fetchTransactions() async {
try {
final transfers = <Transfer>[];
var offset = 0;
late GetRecentTxsAndInfoResult result;
do {
result = await getRecentTxsAndInfo(offset: 0, count: _txChunkSize);
// _lastTxIndex += result.transfers.length;
result = await getRecentTxsAndInfo(offset: offset, count: _txChunkSize);
transfers.addAll(result.transfers);
} while (result.lastItemIndex + 1 < result.totalTransfers);
offset = result.lastItemIndex + 1;
} while (offset < result.totalTransfers);
return Transfer.makeMap(transfers, zanoAssets, currentDaemonHeight);
} catch (e) {
printV((e.toString()));
@@ -334,12 +541,18 @@ abstract class ZanoWalletBase
@override
Future<void> renameWalletFiles(String newWalletName) async {
final currentWalletPath = await pathForWallet(name: name, type: type);
final currentWalletDirPath = await pathForWalletDir(name: name, type: type);
final currentWalletPath = '$currentWalletDirPath/$name';
final currentCacheFile = File(currentWalletPath);
final currentKeysFile = File('$currentWalletPath.keys');
final currentAddressListFile = File('$currentWalletPath.address.txt');
final currentPendingKiFile = File('$currentWalletPath.outkey2ki');
final currentSecretsFile = File(p.join(currentWalletDirPath, 'zano-secrets.json.bin'));
final currentCakeKeysFile = File('$currentWalletPath$_cakeKeysFileSuffix');
final currentCakeKeysBackupFile = File('$currentWalletPath$_cakeKeysFileSuffix.backup');
final newWalletPath = await pathForWallet(name: newWalletName, type: type);
final newWalletDirPath = await pathForWalletDir(name: newWalletName, type: type);
final newWalletPath = '$newWalletDirPath/$newWalletName';
// Copies current wallet files into new wallet name's dir and files
if (currentCacheFile.existsSync()) {
@@ -351,9 +564,22 @@ abstract class ZanoWalletBase
if (currentAddressListFile.existsSync()) {
await currentAddressListFile.copy('$newWalletPath.address.txt');
}
if (currentPendingKiFile.existsSync()) {
await currentPendingKiFile.copy('$newWalletPath.outkey2ki');
}
if (currentSecretsFile.existsSync()) {
await currentSecretsFile.copy(p.join(newWalletDirPath, 'zano-secrets.json.bin'));
}
if (currentCakeKeysFile.existsSync()) {
await currentCakeKeysFile.copy('$newWalletPath$_cakeKeysFileSuffix');
}
if (currentCakeKeysBackupFile.existsSync()) {
await currentCakeKeysBackupFile.copy('$newWalletPath$_cakeKeysFileSuffix.backup');
}
// Delete old name's dir and files
await Directory(currentWalletPath).delete(recursive: true);
// Delete the old wallet *directory*. pathForWallet() is the wallet file
// (.../<name>/<name>); Directory(filePath).delete leaves seed sidecars.
await Directory(currentWalletDirPath).delete(recursive: true);
}
@override
@@ -362,6 +588,7 @@ abstract class ZanoWalletBase
@override
Future<void> save() async {
try {
await _saveKeysFileIfNeeded();
await store();
await walletAddresses.updateAddressesInBox();
} catch (e) {
@@ -544,9 +771,12 @@ abstract class ZanoWalletBase
_updateSyncProgress(walletStatus);
// we can call getWalletInfo ONLY if getWalletStatus returns NOT is in long refresh and wallet state is 2 (ready)
if (!walletStatus.isInLongRefresh && walletStatus.walletState == 2) {
if (_isWalletReady(walletStatus)) {
if (!_hasSeed) {
await _loadSeedIfAvailable();
}
await _syncSecretsAndDerivation();
final walletInfo = await getWalletInfo();
seed = await walletInfo.wiExtended.seed(this);
keys = ZanoWalletKeys(
privateSpendKey: walletInfo.wiExtended.spendPrivateKey,
privateViewKey: walletInfo.wiExtended.viewPrivateKey,
+90 -19
View File
@@ -46,12 +46,13 @@ mixin ZanoWalletApi {
void setPassword(String password) => zano.PlainWallet_resetWalletPassword(hWallet, password);
void closeWallet(int? walletToClose, {bool force = false}) async {
Future<void> closeWallet(int? walletToClose, {bool force = false}) async {
printV('close_wallet ${walletToClose ?? hWallet}: $force');
if (Platform.isWindows || force) {
final result = await _closeWallet(walletToClose ?? hWallet);
final id = walletToClose ?? hWallet;
final result = await _closeWallet(id);
printV('close_wallet result $result');
openWalletCache.removeWhere((_, cwr) => cwr.walletId == (walletToClose ?? hWallet));
openWalletCache.removeWhere((_, cwr) => cwr.walletId == id);
}
}
@@ -76,31 +77,34 @@ mixin ZanoWalletApi {
Future<File> _getWalletSecretsFile() async {
final dir = await getWalletDir();
final file = File(p.join(dir.path, "zano-secrets.json.bin"));
return file;
return File(p.join(dir.path, 'zano-secrets.json.bin'));
}
Future<Map<String, dynamic>> _getSecrets() async {
final file = await _getWalletSecretsFile();
if (!file.existsSync()) {
try {
final file = await _getWalletSecretsFile();
if (!file.existsSync()) {
return {};
}
final data = file.readAsBytesSync();
final b64 = convert.base64.encode(data);
final respStr = await invokeMethod('decrypt_data', {'buff': b64});
final resp = convert.json.decode(respStr);
final dataBytes = convert.base64.decode(resp['result']['res_buff'] as String);
final dataStr = convert.utf8.decode(dataBytes);
return convert.json.decode(dataStr) as Map<String, dynamic>;
} catch (e) {
printV('error reading zano secrets $e');
return {};
}
final data = file.readAsBytesSync();
final b64 = convert.base64.encode(data);
final respStr = await invokeMethod("decrypt_data", {"buff": "$b64"});
final resp = convert.json.decode(respStr);
final dataBytes = convert.base64.decode(resp["result"]["res_buff"] as String);
final dataStr = convert.utf8.decode(dataBytes);
final dataObject = convert.json.decode(dataStr);
return dataObject as Map<String, dynamic>;
}
Future<void> _setSecrets(Map<String, dynamic> data) async {
final dataStr = convert.json.encode(data);
final b64 = convert.base64.encode(convert.utf8.encode(dataStr));
final respStr = await invokeMethod("encrypt_data", {"buff": "$b64"});
final respStr = await invokeMethod('encrypt_data', {'buff': b64});
final resp = convert.json.decode(respStr);
final dataBytes = convert.base64.decode(resp["result"]["res_buff"] as String);
final dataBytes = convert.base64.decode(resp['result']['res_buff'] as String);
final file = await _getWalletSecretsFile();
file.writeAsBytesSync(dataBytes);
}
@@ -124,7 +128,25 @@ mixin ZanoWalletApi {
return _setWalletSecret("passphrase", passphrase);
}
Future<String?> getBip39Mnemonic() async {
return await _getWalletSecret("bip39_mnemonic");
}
Future<void> setBip39Secrets({
required String mnemonic,
required int creationTimestamp,
}) async {
final secrets = await _getSecrets();
secrets["bip39_mnemonic"] = mnemonic;
secrets["creation_timestamp"] = creationTimestamp.toString();
await _setSecrets(secrets);
}
Future<String> getSeed() async {
final bip39Mnemonic = await getBip39Mnemonic();
if (bip39Mnemonic != null && bip39Mnemonic.isNotEmpty) {
return bip39Mnemonic;
}
final passphrase = await getPassphrase();
final respStr = await invokeMethod("get_restore_info", {"seed_password": passphrase ?? ""});
final resp = convert.json.decode(respStr);
@@ -328,10 +350,59 @@ mixin ZanoWalletApi {
return result;
}
Future<CreateWalletResult> restoreWalletFromDerivations(
String path,
String password,
String secretDerivation, {
bool isAuditable = false,
required int creationTimestamp,
}) async {
printV('restore_wallet_from_derivations path $path');
final params = jsonEncode({
"creation_timestamp": creationTimestamp,
"is_auditable": isAuditable,
"pass": password,
"path": path,
"secret_derivation": secretDerivation,
});
final json = await callSyncMethod('restore_from_derivations', 0, params);
final map = jsonDecode(json) as Map<String, dynamic>?;
if (map?['error'] != null) {
final code = map!['error']!['code'] ?? '';
final message = map['error']!['message'] ?? '';
if (code == Consts.errorWrongSeed) {
throw RestoreFromSeedsException(
'Error restoring wallet\nPlease check the seed words are correct. Additionally, if you created this wallet with a passphrase please add it under the Advanced Settings page.');
} else if (code == Consts.errorAlreadyExists) {
throw RestoreFromSeedsException('Error restoring wallet, already exists');
}
throw RestoreFromSeedsException('Error restoring wallet, $message ($code)');
}
if (map?['result'] == null) {
throw RestoreFromSeedsException('Error restoring wallet, empty response');
}
final result = CreateWalletResult.fromJson(map!['result'] as Map<String, dynamic>);
openWalletCache[path] = result;
printV('restore_wallet_from_derivations ${result.name} ${result.wi.address}');
return result;
}
Future<CreateWalletResult> restoreWalletFromSeed(
String path, String password, String seed, String? passphrase) async {
String path,
String password,
String seed,
String? passphrase, {
required int creationTimestamp,
}) async {
printV('restore_wallet path $path');
final json = zano.PlainWallet_restore(seed, path, password, passphrase ?? '');
final params = jsonEncode({
"creation_timestamp": creationTimestamp,
"pass": password,
"path": path,
"seed_phrase": seed,
"seed_pass": passphrase ?? '',
});
final json = await callSyncMethod('restore', 0, params);
final map = jsonDecode(json) as Map<String, dynamic>?;
if (map?['error'] != null) {
final code = map!['error']!['code'] ?? '';
+42 -13
View File
@@ -1,6 +1,7 @@
import 'dart:io';
import 'package:collection/collection.dart';
import 'package:cw_core/encryption_file_utils.dart';
import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/wallet_base.dart';
@@ -14,8 +15,14 @@ import 'package:hive/hive.dart';
import 'package:monero/zano.dart' as zano;
class ZanoNewWalletCredentials extends WalletCredentials {
ZanoNewWalletCredentials({required String name, String? password, required String? passphrase})
: super(name: name, password: password, passphrase: passphrase);
ZanoNewWalletCredentials({
required String name,
String? password,
required String? passphrase,
this.mnemonic,
}) : super(name: name, password: password, passphrase: passphrase);
final String? mnemonic;
}
class ZanoRestoreWalletFromSeedCredentials extends WalletCredentials {
@@ -52,7 +59,9 @@ class ZanoWalletService extends WalletService<
ZanoRestoreWalletFromSeedCredentials,
ZanoRestoreWalletFromKeysCredentials,
ZanoNewWalletCredentials> {
ZanoWalletService();
ZanoWalletService(this.isDirect);
final bool isDirect;
static bool walletFilesExist(String path) =>
!File(path).existsSync() && !File('$path.keys').existsSync();
@@ -65,7 +74,8 @@ class ZanoWalletService extends WalletService<
@override
Future<ZanoWallet> create(WalletCredentials credentials, {bool? isTestnet}) async {
printV('zanowallet service create isTestnet $isTestnet');
return await ZanoWalletBase.create(credentials: credentials);
return await ZanoWalletBase.create(
credentials: credentials, encryptionFileUtils: encryptionFileUtilsFor(isDirect));
}
@override
@@ -82,12 +92,21 @@ class ZanoWalletService extends WalletService<
}
try {
final wallet =
await ZanoWalletBase.open(name: name, password: password, walletInfo: walletInfo);
await ZanoWalletBase.open(
name: name,
password: password,
walletInfo: walletInfo,
encryptionFileUtils: encryptionFileUtilsFor(isDirect));
saveBackup(name);
return wallet;
} catch (e) {
printV('openWallet $name failed: $e');
await restoreWalletFilesFromBackup(name);
return await ZanoWalletBase.open(name: name, password: password, walletInfo: walletInfo);
return await ZanoWalletBase.open(
name: name,
password: password,
walletInfo: walletInfo,
encryptionFileUtils: encryptionFileUtilsFor(isDirect));
}
}
@@ -115,15 +134,24 @@ class ZanoWalletService extends WalletService<
throw Exception('Wallet not found');
}
final currentWallet =
ZanoWallet(currentWalletInfo, await currentWalletInfo.getDerivationInfo(), password);
ZanoWallet(currentWalletInfo, await currentWalletInfo.getDerivationInfo(), password,
encryptionFileUtilsFor(isDirect));
final oldPath = await pathForWallet(name: currentName, type: getType());
final cached = ZanoWalletApi.openWalletCache.remove(oldPath);
if (cached != null) {
currentWallet.hWallet = cached.walletId;
await currentWallet.closeWallet(cached.walletId, force: true);
}
await currentWallet.renameWalletFiles(newName);
final newWalletInfo = currentWalletInfo;
newWalletInfo.id = WalletBase.idFor(newName, getType());
newWalletInfo.name = newName;
await newWalletInfo.save();
final newDirPath = await pathForWalletDir(name: newName, type: getType());
currentWalletInfo.id = WalletBase.idFor(newName, getType());
currentWalletInfo.name = newName;
currentWalletInfo.dirPath = newDirPath;
currentWalletInfo.path = '$newDirPath/$newName';
await currentWalletInfo.save();
}
@override
@@ -135,7 +163,8 @@ class ZanoWalletService extends WalletService<
@override
Future<ZanoWallet> restoreFromSeed(ZanoRestoreWalletFromSeedCredentials credentials,
{bool? isTestnet}) async {
return ZanoWalletBase.restore(credentials: credentials);
return ZanoWalletBase.restore(
credentials: credentials, encryptionFileUtils: encryptionFileUtilsFor(isDirect));
}
@override
+3 -1
View File
@@ -11,6 +11,8 @@ environment:
dependencies:
flutter:
sdk: flutter
bip39: ^1.0.6
bip32: ^2.0.0
ffi: ^2.0.1
http: ^1.1.0
path_provider: ^2.0.11
@@ -25,7 +27,7 @@ dependencies:
monero:
git:
url: https://github.com/mrcyjanek/monero_c.git
ref: 3bfb3856a838f2bf6b729501837bb0295dedf25d
ref: cd4fe366cc8d3c188c91de279f43b29c0e044b15
path: impls/monero.dart
dev_dependencies:
flutter_test:
+38
View File
@@ -0,0 +1,38 @@
import 'package:cw_zano/bip39_seed.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group("Zano bip39 secret derivation", () {
group("Test Wallet 1", () {
final bip39Seed =
'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about';
final expectedSecretDerivation =
"bfafd1eb0e43da200c5c11537d355e458e7c326b3bc1b19f4546573d6bac9d0f";
final expectedSecretDerivationWithPassphrase =
"674ba1caeaf8a79e66a026858c78d4e565032953e98d39bdea12559bcae86a0c";
test("Get secret derivation from bip39", () {
final secretDerivation = getSecretDerivationFromBip39(bip39Seed);
expect(secretDerivation, expectedSecretDerivation);
});
test("Get secret derivation from bip39 with passphrase", () {
final secretDerivation =
getSecretDerivationFromBip39(bip39Seed, passphrase: 'TREZOR');
expect(secretDerivation, expectedSecretDerivationWithPassphrase);
});
});
group("Test Wallet 2", () {
final bip39Seed =
'melt veteran patrol echo miss fat wrap apple crowd eyebrow weird boring';
final expectedSecretDerivation =
"8b2693e1d60b0431e3208762bc8a3f232890f50a3fea1e9605ec10a86afe1800";
test("Get secret derivation from bip39", () {
final secretDerivation = getSecretDerivationFromBip39(bip39Seed);
expect(secretDerivation, expectedSecretDerivation);
});
});
});
}
+1 -1
View File
@@ -86,6 +86,7 @@ class WalletCreationService {
case WalletType.dogecoin:
case WalletType.nano:
case WalletType.zcash:
case WalletType.zano:
case WalletType.decred:
return true;
case WalletType.monero:
@@ -93,7 +94,6 @@ class WalletCreationService {
case WalletType.none:
case WalletType.haven:
case WalletType.banano:
case WalletType.zano:
return false;
}
}
+1 -1
View File
@@ -1341,7 +1341,7 @@ Future<void> setup({
case WalletType.wownero:
return wownero!.createWowneroWalletService(_unspentCoinsInfoSource);
case WalletType.zano:
return zano!.createZanoWalletService();
return zano!.createZanoWalletService(SettingsStoreBase.walletPasswordDirectInput);
case WalletType.decred:
return decred!.createDecredWalletService(
_unspentCoinsInfoSource, SettingsStoreBase.walletPasswordDirectInput);
+1 -1
View File
@@ -17,11 +17,11 @@ bool isBIP39Wallet(WalletType walletType) {
case WalletType.monero:
case WalletType.dogecoin:
case WalletType.zcash:
case WalletType.zano:
case WalletType.decred:
return true;
case WalletType.wownero:
case WalletType.haven:
case WalletType.zano:
case WalletType.none:
return false;
}
@@ -286,7 +286,12 @@ class WalletRestoreFromSeedFormState extends State<WalletRestoreFromSeedForm> {
'wallet_restore_from_seed_blockheight_textfield_key',
),
onHeightOrDateEntered: widget.onHeightOrDateEntered,
hasDatePicker: [WalletType.monero, WalletType.wownero, WalletType.zcash].contains(
hasDatePicker: [
WalletType.monero,
WalletType.wownero,
WalletType.zcash,
WalletType.zano,
].contains(
widget.type,
),
walletType: widget.type,
@@ -13,6 +13,7 @@ import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/restore/restore_mode.dart';
import 'package:cake_wallet/view_model/seed_settings_view_model.dart';
import 'package:cake_wallet/view_model/wallet_restore_view_model.dart';
import 'package:cake_wallet/zano/zano.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart';
@@ -606,7 +607,13 @@ class _WalletRestorePageBodyState extends State<_WalletRestorePageBody>
final seedWords = seedPhrase.split(' ');
if (seedWords.length == 14 && walletRestoreViewModel.type == WalletType.wownero) return true;
if (seedWords.length == 26 && walletRestoreViewModel.type == WalletType.zano) return true;
if (walletRestoreViewModel.type == WalletType.zano) {
if (seedWords.length == 26) return true;
if ([12, 24].contains(seedWords.length) && zano!.isBip39Seed(seedPhrase.trim())) {
return true;
}
return false;
}
if (seedWords.length == 12 && walletRestoreViewModel.type == WalletType.monero) {
return walletRestoreFromSeedFormKey.currentState?.blockchainHeightKey.currentState
@@ -2,6 +2,7 @@ import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/src/widgets/standard_switch.dart';
import 'package:cake_wallet/utils/date_picker.dart';
import 'package:cake_wallet/wownero/wownero.dart';
import 'package:cake_wallet/zano/zano.dart';
import 'package:cake_wallet/zcash/zcash.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart';
@@ -200,6 +201,8 @@ class BlockchainHeightState extends State<BlockchainHeightWidget> {
height = wownero!.getHeightByDate(date: date);
} else if (widget.walletType == WalletType.zcash) {
height = await zcash!.getHeightByDate(date);
} else if (widget.walletType == WalletType.zano) {
height = zano!.getHeightByDate(date: date);
} else {
throw Exception("unknown currency in BlockchainHeightWidget");
}
@@ -59,6 +59,7 @@ abstract class AdvancedPrivacySettingsViewModelBase with Store {
case WalletType.solana:
case WalletType.tron:
case WalletType.zcash:
case WalletType.zano:
case WalletType.decred:
return true;
@@ -74,7 +75,6 @@ abstract class AdvancedPrivacySettingsViewModelBase with Store {
case WalletType.wownero:
case WalletType.none:
case WalletType.haven:
case WalletType.zano:
return false;
}
}
@@ -128,8 +128,9 @@ abstract class WalletGroupsDisplayViewModelBase with Store {
bool isNonSeedWallet = wallet.isNonSeedWallet;
bool isNotMoneroBip39Wallet =
wallet.type == WalletType.monero && di.derivationType != DerivationType.bip39;
bool isNotBip39CoinWallet =
(wallet.type == WalletType.monero || wallet.type == WalletType.zano) &&
di.derivationType != DerivationType.bip39;
bool isNotDecredBip39Wallet = wallet.type == WalletType.decred &&
di.derivationType != DerivationType.bip39 &&
@@ -142,7 +143,7 @@ abstract class WalletGroupsDisplayViewModelBase with Store {
isElectrumDerivationType ||
isSameTypeAsSelectedWallet ||
isNonSeedWallet ||
isNotMoneroBip39Wallet ||
isNotBip39CoinWallet ||
isNotDecredBip39Wallet;
}
+1
View File
@@ -139,6 +139,7 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store {
name: name,
password: walletPassword,
passphrase: passphrase,
mnemonic: newWalletArguments!.mnemonic,
);
case WalletType.zcash:
return zcash!.createZcashNewWalletCredentials(
+21 -3
View File
@@ -83,8 +83,13 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store {
late final bool hasSeedLanguageSelector =
[WalletType.monero, WalletType.haven, WalletType.wownero].contains(type);
late final bool hasBlockchainHeightSelector =
[WalletType.monero, WalletType.haven, WalletType.wownero, WalletType.zcash].contains(type);
late final bool hasBlockchainHeightSelector = [
WalletType.monero,
WalletType.haven,
WalletType.wownero,
WalletType.zcash,
WalletType.zano,
].contains(type);
late final bool hasRestoreFromPrivateKey = [
WalletType.ethereum,
@@ -204,13 +209,15 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store {
height: height,
);
case WalletType.zano:
return zano!.createZanoRestoreWalletFromSeedCredentials(
final credentials = zano!.createZanoRestoreWalletFromSeedCredentials(
name: name,
password: password,
height: height,
passphrase: passphrase ?? '',
mnemonic: seed,
);
credentials.derivationInfo = derivationInfo;
return credentials;
case WalletType.decred:
return decred!.createDecredRestoreWalletFromSeedCredentials(
name: name,
@@ -363,6 +370,17 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store {
seedKey: seedKey,
node: node,
);
case WalletType.zano:
final mnemonic = (credentials['seed'] as String?)?.trim() ?? '';
if (mnemonic.isEmpty) break;
if (zano!.isBip39Seed(mnemonic)) {
return [
DerivationInfo(derivationType: DerivationType.bip39),
];
}
return [
DerivationInfo(derivationType: DerivationType.unknown),
];
default:
break;
}
+21 -6
View File
@@ -46,7 +46,7 @@ class CWZano extends Zano {
@override
List<String> getWordList(String language) {
assert(language.toLowerCase() == LanguageList.english.toLowerCase());
return EnglishMnemonics.words;
return [...zano_bip39.bip39EnglishWords, ...EnglishMnemonics.words];
}
@override
@@ -61,9 +61,18 @@ class CWZano extends Zano {
}
@override
WalletCredentials createZanoNewWalletCredentials(
{required String name, required String? password, required String? passphrase}) {
return ZanoNewWalletCredentials(name: name, password: password, passphrase: passphrase);
WalletCredentials createZanoNewWalletCredentials({
required String name,
required String? password,
required String? passphrase,
String? mnemonic,
}) {
return ZanoNewWalletCredentials(
name: name,
password: password,
passphrase: passphrase,
mnemonic: mnemonic,
);
}
@override
@@ -122,8 +131,8 @@ class CWZano extends Zano {
// }
@override
WalletService createZanoWalletService() {
return ZanoWalletService();
WalletService createZanoWalletService(bool isDirect) {
return ZanoWalletService(isDirect);
}
@override
@@ -146,6 +155,12 @@ class CWZano extends Zano {
@override
bool validateAddress(String address) => ZanoUtils.validateAddress(address);
@override
bool isBip39Seed(String mnemonic) => zano_bip39.isBip39Seed(mnemonic);
@override
int getHeightByDate({required DateTime date}) => ZanoUtils.heightByDate(date);
@override
Map<String, List<int>> debugCallLength() {
return api.debugCallLength();
+2 -2
View File
@@ -2050,8 +2050,8 @@ packages:
dependency: "direct overridden"
description:
path: "impls/monero.dart"
ref: "3bfb3856a838f2bf6b729501837bb0295dedf25d"
resolved-ref: "3bfb3856a838f2bf6b729501837bb0295dedf25d"
ref: cd4fe366cc8d3c188c91de279f43b29c0e044b15
resolved-ref: cd4fe366cc8d3c188c91de279f43b29c0e044b15
url: "https://github.com/mrcyjanek/monero_c.git"
source: git
version: "0.0.0"
+1 -1
View File
@@ -960,7 +960,7 @@ dependency_overrides:
monero:
git:
url: https://github.com/mrcyjanek/monero_c.git
ref: 3bfb3856a838f2bf6b729501837bb0295dedf25d
ref: cd4fe366cc8d3c188c91de279f43b29c0e044b15
path: ./impls/monero.dart
msgpack_dart:
git:
+1 -1
View File
@@ -16,7 +16,7 @@ fi
# NOTE: Make sure to update monero_c prebuilds link in workflow files
# https://github.com/MrCyjaneK/monero_c/releases/download/v0.18.4.6-RC2/release-bundle.zip
git fetch -a
git checkout 5952bef2ec01b0b7e57c11613cbdc081dcd727c5
git checkout cd4fe366cc8d3c188c91de279f43b29c0e044b15
git reset --hard
git submodule update --init --force --recursive
+5 -2
View File
@@ -1178,6 +1178,7 @@ import 'package:cw_core/zano_asset.dart';
import 'package:hive/hive.dart';
""";
const zanoCWHeaders = """
import 'package:cw_zano/bip39_seed.dart' as zano_bip39;
import 'package:cw_zano/mnemonics/english.dart';
import 'package:cw_zano/model/zano_transaction_credentials.dart';
import 'package:cw_zano/model/zano_transaction_info.dart';
@@ -1196,12 +1197,12 @@ abstract class Zano {
List<String> getWordList(String language);
WalletCredentials createZanoRestoreWalletFromSeedCredentials({required String name, required String password, required String passphrase, required int height, required String mnemonic});
WalletCredentials createZanoNewWalletCredentials({required String name, required String? password, required String? passphrase});
WalletCredentials createZanoNewWalletCredentials({required String name, required String? password, required String? passphrase, String? mnemonic});
Map<String, String> getKeys(Object wallet);
Object createZanoTransactionCredentials({required List<Output> outputs, required TransactionPriority priority, required CryptoCurrency currency});
double formatterIntAmountToDouble({required int amount, required CryptoCurrency currency, required bool forFee});
int formatterParseAmount({required String amount, required CryptoCurrency currency});
WalletService createZanoWalletService();
WalletService createZanoWalletService(bool isDirect);
CryptoCurrency? assetOfTransaction(WalletBase wallet, TransactionInfo tx);
List<ZanoAsset> getZanoAssets(WalletBase wallet);
String getZanoAssetAddress(CryptoCurrency asset);
@@ -1211,6 +1212,8 @@ abstract class Zano {
Future<CryptoCurrency?> getZanoAsset(WalletBase wallet, String contractAddress);
String getAddress(WalletBase wallet);
bool validateAddress(String address);
bool isBip39Seed(String mnemonic);
int getHeightByDate({required DateTime date});
Map<String, List<int>> debugCallLength();
bool isTokenAlreadyAdded(WalletBase wallet, String contractAddress);
}