diff --git a/cw_monero/pubspec.yaml b/cw_monero/pubspec.yaml index 3f1c10f7b..04d3f3997 100644 --- a/cw_monero/pubspec.yaml +++ b/cw_monero/pubspec.yaml @@ -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 diff --git a/cw_wownero/pubspec.yaml b/cw_wownero/pubspec.yaml index 6f9902452..da8f90997 100644 --- a/cw_wownero/pubspec.yaml +++ b/cw_wownero/pubspec.yaml @@ -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 diff --git a/cw_zano/lib/api/model/transfer.dart b/cw_zano/lib/api/model/transfer.dart index 2d9b51d5c..37a73085a 100644 --- a/cw_zano/lib/api/model/transfer.dart +++ b/cw_zano/lib/api/model/transfer.dart @@ -66,8 +66,13 @@ class Transfer { ? [] : (json['remote_aliases'] as List).cast(), showSender: json['show_sender'] as bool? ?? false, - subtransfers: (json['subtransfers'] as List? ?? []) - .map((e) => Subtransfer.fromJson(e as Map)) + subtransfers: (json['subtransfers_by_pid'] as List? ?? []) + .whereType>() + .expand( + (group) => (group['subtransfers'] as List? ?? []) + .whereType>() + .map(Subtransfer.fromJson), + ) .toList(), timestamp: json['timestamp'] as int? ?? 0, transferInternalIndex: json['transfer_internal_index'] == null diff --git a/cw_zano/lib/bip39_seed.dart b/cw_zano/lib/bip39_seed.dart new file mode 100644 index 000000000..b054a5dcb --- /dev/null +++ b/cw_zano/lib/bip39_seed.dart @@ -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 get bip39EnglishWords => List.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(); +} diff --git a/cw_zano/lib/get_height_by_data.txt b/cw_zano/lib/get_height_by_data.txt new file mode 100644 index 000000000..43df05f1e --- /dev/null +++ b/cw_zano/lib/get_height_by_data.txt @@ -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 diff --git a/cw_zano/lib/get_height_by_date.dart b/cw_zano/lib/get_height_by_date.dart new file mode 100644 index 000000000..83cef7328 --- /dev/null +++ b/cw_zano/lib/get_height_by_date.dart @@ -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; +} diff --git a/cw_zano/lib/zano_utils.dart b/cw_zano/lib/zano_utils.dart index f39f20462..72b07b72f 100644 --- a/cw_zano/lib/zano_utils.dart +++ b/cw_zano/lib/zano_utils.dart @@ -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( diff --git a/cw_zano/lib/zano_wallet.dart b/cw_zano/lib/zano_wallet.dart index 8b45b142a..df5ceff70 100644 --- a/cw_zano/lib/zano_wallet.dart +++ b/cw_zano/lib/zano_wallet.dart @@ -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 - 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 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 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 changePassword(String password) async { setPassword(password); + if (_hasSeed) { + await saveKeysFile(_password, _encryptionFileUtils); + saveKeysFile(_password, _encryptionFileUtils, true); + } } - static Future create({required WalletCredentials credentials}) async { + static Future 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 restore( - {required ZanoRestoreWalletFromSeedCredentials credentials}) async { + static Future 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 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 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 _hasKeysFile() async { + try { + final path = await makeKeysFilePath(); + return File(path).existsSync() || File('$path.backup').existsSync(); + } catch (_) { + return false; + } + } + + Future _loadKeysFileIfPresent() async { + if (!await _hasKeysFile()) { + return; + } + final path = await makeKeysFilePath(); + Future read(String readPath) async { + final jsonSource = await _encryptionFileUtils.read(path: readPath, password: _password); + return WalletKeysData.fromJSON(json.decode(jsonSource) as Map); + } + + 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 _saveKeysFileIfNeeded() async { + if (!_hasSeed || await _hasKeysFile()) { + return; + } + await saveKeysFile(_password, _encryptionFileUtils); + saveKeysFile(_password, _encryptionFileUtils, true); + } + + Future _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 _syncSecretsAndDerivation() async { + if (!_hasSeed || _didSyncSecrets) { + return; + } + try { + await _persistSecretsIfNeeded(); + await _setDerivationForSeed(); + await _saveKeysFileIfNeeded(); + _didSyncSecrets = true; + } catch (e) { + printV('error persisting zano secrets $e'); + } + } + + Future _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 _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 close({bool shouldCleanup = true}) async { closeWallet(null); @@ -310,12 +516,13 @@ abstract class ZanoWalletBase Future> fetchTransactions() async { try { final transfers = []; + 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 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 + // (...//); Directory(filePath).delete leaves seed sidecars. + await Directory(currentWalletDirPath).delete(recursive: true); } @override @@ -362,6 +588,7 @@ abstract class ZanoWalletBase @override Future 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, diff --git a/cw_zano/lib/zano_wallet_api.dart b/cw_zano/lib/zano_wallet_api.dart index cec26ed55..7196c1d94 100644 --- a/cw_zano/lib/zano_wallet_api.dart +++ b/cw_zano/lib/zano_wallet_api.dart @@ -46,12 +46,13 @@ mixin ZanoWalletApi { void setPassword(String password) => zano.PlainWallet_resetWalletPassword(hWallet, password); - void closeWallet(int? walletToClose, {bool force = false}) async { + Future 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 _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> _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; + } 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; } Future _setSecrets(Map 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 getBip39Mnemonic() async { + return await _getWalletSecret("bip39_mnemonic"); + } + + Future 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 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 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?; + 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); + openWalletCache[path] = result; + printV('restore_wallet_from_derivations ${result.name} ${result.wi.address}'); + return result; + } + Future 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?; if (map?['error'] != null) { final code = map!['error']!['code'] ?? ''; diff --git a/cw_zano/lib/zano_wallet_service.dart b/cw_zano/lib/zano_wallet_service.dart index 9a33bc34e..cf50509f5 100644 --- a/cw_zano/lib/zano_wallet_service.dart +++ b/cw_zano/lib/zano_wallet_service.dart @@ -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 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 restoreFromSeed(ZanoRestoreWalletFromSeedCredentials credentials, {bool? isTestnet}) async { - return ZanoWalletBase.restore(credentials: credentials); + return ZanoWalletBase.restore( + credentials: credentials, encryptionFileUtils: encryptionFileUtilsFor(isDirect)); } @override diff --git a/cw_zano/pubspec.yaml b/cw_zano/pubspec.yaml index 7e8ede8dd..63711349c 100644 --- a/cw_zano/pubspec.yaml +++ b/cw_zano/pubspec.yaml @@ -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: diff --git a/cw_zano/test/bip39_seed_test.dart b/cw_zano/test/bip39_seed_test.dart new file mode 100644 index 000000000..26cda4c87 --- /dev/null +++ b/cw_zano/test/bip39_seed_test.dart @@ -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); + }); + }); + }); +} diff --git a/lib/core/wallet_creation_service.dart b/lib/core/wallet_creation_service.dart index d627c1a31..3700a1c84 100644 --- a/lib/core/wallet_creation_service.dart +++ b/lib/core/wallet_creation_service.dart @@ -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; } } diff --git a/lib/di.dart b/lib/di.dart index 147f1900c..ee904e21f 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -1341,7 +1341,7 @@ Future 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); diff --git a/lib/reactions/wallet_utils.dart b/lib/reactions/wallet_utils.dart index 56bf2080c..57d11a4c8 100644 --- a/lib/reactions/wallet_utils.dart +++ b/lib/reactions/wallet_utils.dart @@ -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; } diff --git a/lib/src/screens/restore/wallet_restore_from_seed_form.dart b/lib/src/screens/restore/wallet_restore_from_seed_form.dart index d1b5cf0f8..933a5feaf 100644 --- a/lib/src/screens/restore/wallet_restore_from_seed_form.dart +++ b/lib/src/screens/restore/wallet_restore_from_seed_form.dart @@ -286,7 +286,12 @@ class WalletRestoreFromSeedFormState extends State { '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, diff --git a/lib/src/screens/restore/wallet_restore_page.dart b/lib/src/screens/restore/wallet_restore_page.dart index 1a0db8d1c..af8e9b91e 100644 --- a/lib/src/screens/restore/wallet_restore_page.dart +++ b/lib/src/screens/restore/wallet_restore_page.dart @@ -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 diff --git a/lib/src/widgets/blockchain_height_widget.dart b/lib/src/widgets/blockchain_height_widget.dart index 273db827f..68160c268 100644 --- a/lib/src/widgets/blockchain_height_widget.dart +++ b/lib/src/widgets/blockchain_height_widget.dart @@ -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 { 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"); } diff --git a/lib/view_model/advanced_privacy_settings_view_model.dart b/lib/view_model/advanced_privacy_settings_view_model.dart index 39e9346b6..f8f1ce1d7 100644 --- a/lib/view_model/advanced_privacy_settings_view_model.dart +++ b/lib/view_model/advanced_privacy_settings_view_model.dart @@ -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; } } diff --git a/lib/view_model/wallet_groups_display_view_model.dart b/lib/view_model/wallet_groups_display_view_model.dart index fd8fdc23e..b394da550 100644 --- a/lib/view_model/wallet_groups_display_view_model.dart +++ b/lib/view_model/wallet_groups_display_view_model.dart @@ -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; } diff --git a/lib/view_model/wallet_new_vm.dart b/lib/view_model/wallet_new_vm.dart index e313ca035..6597776fd 100644 --- a/lib/view_model/wallet_new_vm.dart +++ b/lib/view_model/wallet_new_vm.dart @@ -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( diff --git a/lib/view_model/wallet_restore_view_model.dart b/lib/view_model/wallet_restore_view_model.dart index 16767a66e..350b21337 100644 --- a/lib/view_model/wallet_restore_view_model.dart +++ b/lib/view_model/wallet_restore_view_model.dart @@ -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; } diff --git a/lib/zano/cw_zano.dart b/lib/zano/cw_zano.dart index ce2f82ccc..ab14e332e 100644 --- a/lib/zano/cw_zano.dart +++ b/lib/zano/cw_zano.dart @@ -46,7 +46,7 @@ class CWZano extends Zano { @override List 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> debugCallLength() { return api.debugCallLength(); diff --git a/pubspec.lock b/pubspec.lock index cbcdbf567..90493aca9 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -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" diff --git a/pubspec_overrides.yaml b/pubspec_overrides.yaml index b91e7dc84..90cceec37 100644 --- a/pubspec_overrides.yaml +++ b/pubspec_overrides.yaml @@ -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: diff --git a/scripts/prepare_moneroc.sh b/scripts/prepare_moneroc.sh index 8d0d6e5ae..f4434ab4c 100755 --- a/scripts/prepare_moneroc.sh +++ b/scripts/prepare_moneroc.sh @@ -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 diff --git a/tool/configure.dart b/tool/configure.dart index a741c2024..2f170465c 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -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 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 getKeys(Object wallet); Object createZanoTransactionCredentials({required List 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 getZanoAssets(WalletBase wallet); String getZanoAssetAddress(CryptoCurrency asset); @@ -1211,6 +1212,8 @@ abstract class Zano { Future getZanoAsset(WalletBase wallet, String contractAddress); String getAddress(WalletBase wallet); bool validateAddress(String address); + bool isBip39Seed(String mnemonic); + int getHeightByDate({required DateTime date}); Map> debugCallLength(); bool isTokenAlreadyAdded(WalletBase wallet, String contractAddress); }