Merge branch 'cake-tech:dev' into dev

This commit is contained in:
BSN ∞/21M
2026-07-19 13:07:22 +02:00
committed by GitHub
66 changed files with 4357 additions and 2828 deletions
@@ -159,11 +159,6 @@ jobs:
- name: Generate localization
run: |
dart run tool/generate_localization.dart
- name: Workaround zcash
run: |
mkdir -p scripts/zcash_lib/assets/
touch scripts/zcash_lib/assets/sapling-spend.params
touch scripts/zcash_lib/assets/sapling-output.params
- name: Build linux
run: |
flutter build linux --dart-define=hasDevOptions=true --release
+2 -1
View File
@@ -226,4 +226,5 @@ scripts/android/yttrium
scripts/bitbox_flutter
scripts/reown_flutter
*.salive
*.salive
android/.kotlin
-1
View File
@@ -32,7 +32,6 @@ Cake Wallet includes support for several cryptocurrencies, including:
* Nano (XNO)
* Zano (ZANO)
* Decred (DCR)
* Wownero (WOW)
## Features
+2 -2
View File
@@ -5,8 +5,8 @@
useSSL: true
isEnabledForAutoSwitching: true
label: "Cake Wallet"
-
uri: zec.rocks:443
- uri: zec.rocks:443
is_default: true
useSSL: true
isEnabledForAutoSwitching: true
label: "Zec.rocks"
+5 -5
View File
@@ -634,10 +634,10 @@ packages:
dependency: transitive
description:
name: meta
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev"
source: hosted
version: "1.18.0"
version: "1.17.0"
mime:
dependency: transitive
description:
@@ -1100,10 +1100,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
url: "https://pub.dev"
source: hosted
version: "0.7.11"
version: "0.7.10"
torch_dart:
dependency: transitive
description:
@@ -1322,5 +1322,5 @@ packages:
source: hosted
version: "2.2.2"
sdks:
dart: ">=3.10.0-0 <4.0.0"
dart: ">=3.9.0 <4.0.0"
flutter: ">=3.29.0"
+416
View File
@@ -0,0 +1,416 @@
class ZcashHeight {
// for i in $(seq 1 331); do echo "_Checkpoint($(date -j -f "%Y-%m-%d %H:%M:%S" "$(curl -s "https://mainnet.zcashexplorer.app/blocks/${i}0000" | grep -oE '[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}')" "+%s"), ${i}0000),"; done > points.txt
static final List<_Checkpoint> _points = [
_Checkpoint(1479114667, 10000),
_Checkpoint(1480618460, 20000),
_Checkpoint(1482129143, 30000),
_Checkpoint(1483635610, 40000),
_Checkpoint(1485145514, 50000),
_Checkpoint(1486655742, 60000),
_Checkpoint(1488163642, 70000),
_Checkpoint(1489668244, 80000),
_Checkpoint(1491175107, 90000),
_Checkpoint(1492681094, 100000),
_Checkpoint(1494185626, 110000),
_Checkpoint(1495691260, 120000),
_Checkpoint(1497196465, 130000),
_Checkpoint(1498702428, 140000),
_Checkpoint(1500209116, 150000),
_Checkpoint(1501716823, 160000),
_Checkpoint(1503222893, 170000),
_Checkpoint(1504730866, 180000),
_Checkpoint(1506236963, 190000),
_Checkpoint(1507744233, 200000),
_Checkpoint(1509254578, 210000),
_Checkpoint(1510768449, 220000),
_Checkpoint(1512275502, 230000),
_Checkpoint(1513783951, 240000),
_Checkpoint(1515292290, 250000),
_Checkpoint(1516799219, 260000),
_Checkpoint(1518306180, 270000),
_Checkpoint(1519813817, 280000),
_Checkpoint(1521319150, 290000),
_Checkpoint(1522828107, 300000),
_Checkpoint(1524336223, 310000),
_Checkpoint(1525844954, 320000),
_Checkpoint(1527354342, 330000),
_Checkpoint(1528860301, 340000),
_Checkpoint(1530365206, 350000),
_Checkpoint(1531873693, 360000),
_Checkpoint(1533380392, 370000),
_Checkpoint(1534888873, 380000),
_Checkpoint(1536393381, 390000),
_Checkpoint(1537899117, 400000),
_Checkpoint(1539406330, 410000),
_Checkpoint(1540912214, 420000),
_Checkpoint(1542422891, 430000),
_Checkpoint(1543931111, 440000),
_Checkpoint(1545437758, 450000),
_Checkpoint(1546945529, 460000),
_Checkpoint(1548450107, 470000),
_Checkpoint(1549957767, 480000),
_Checkpoint(1551466960, 490000),
_Checkpoint(1552969806, 500000),
_Checkpoint(1554477648, 510000),
_Checkpoint(1555981451, 520000),
_Checkpoint(1557489241, 530000),
_Checkpoint(1558997282, 540000),
_Checkpoint(1560503840, 550000),
_Checkpoint(1562011717, 560000),
_Checkpoint(1563518091, 570000),
_Checkpoint(1565027136, 580000),
_Checkpoint(1566532760, 590000),
_Checkpoint(1568039395, 600000),
_Checkpoint(1569547911, 610000),
_Checkpoint(1571051853, 620000),
_Checkpoint(1572559831, 630000),
_Checkpoint(1574069743, 640000),
_Checkpoint(1575576895, 650000),
_Checkpoint(1576603014, 660000),
_Checkpoint(1577356346, 670000),
_Checkpoint(1578109375, 680000),
_Checkpoint(1578863180, 690000),
_Checkpoint(1579616443, 700000),
_Checkpoint(1580369806, 710000),
_Checkpoint(1581122786, 720000),
_Checkpoint(1581876634, 730000),
_Checkpoint(1582630471, 740000),
_Checkpoint(1583383678, 750000),
_Checkpoint(1584133944, 760000),
_Checkpoint(1584887940, 770000),
_Checkpoint(1585641057, 780000),
_Checkpoint(1586395543, 790000),
_Checkpoint(1587148500, 800000),
_Checkpoint(1587901493, 810000),
_Checkpoint(1588655183, 820000),
_Checkpoint(1589408476, 830000),
_Checkpoint(1590163464, 840000),
_Checkpoint(1590916379, 850000),
_Checkpoint(1591669786, 860000),
_Checkpoint(1592423887, 870000),
_Checkpoint(1593176771, 880000),
_Checkpoint(1593930930, 890000),
_Checkpoint(1594684380, 900000),
_Checkpoint(1595439209, 910000),
_Checkpoint(1596192463, 920000),
_Checkpoint(1596946009, 930000),
_Checkpoint(1597700256, 940000),
_Checkpoint(1598453538, 950000),
_Checkpoint(1599207483, 960000),
_Checkpoint(1599960598, 970000),
_Checkpoint(1600714563, 980000),
_Checkpoint(1601467884, 990000),
_Checkpoint(1602220941, 1000000),
_Checkpoint(1602974722, 1010000),
_Checkpoint(1603728848, 1020000),
_Checkpoint(1604485637, 1030000),
_Checkpoint(1605238381, 1040000),
_Checkpoint(1605992807, 1050000),
_Checkpoint(1606747750, 1060000),
_Checkpoint(1607500606, 1070000),
_Checkpoint(1608255278, 1080000),
_Checkpoint(1609008684, 1090000),
_Checkpoint(1609763782, 1100000),
_Checkpoint(1610517871, 1110000),
_Checkpoint(1611271575, 1120000),
_Checkpoint(1612025546, 1130000),
_Checkpoint(1612779940, 1140000),
_Checkpoint(1613533993, 1150000),
_Checkpoint(1614287669, 1160000),
_Checkpoint(1615042297, 1170000),
_Checkpoint(1615792993, 1180000),
_Checkpoint(1616546145, 1190000),
_Checkpoint(1617299867, 1200000),
_Checkpoint(1618054038, 1210000),
_Checkpoint(1618808513, 1220000),
_Checkpoint(1619563243, 1230000),
_Checkpoint(1620318159, 1240000),
_Checkpoint(1621071298, 1250000),
_Checkpoint(1621826156, 1260000),
_Checkpoint(1622580237, 1270000),
_Checkpoint(1623335553, 1280000),
_Checkpoint(1624090141, 1290000),
_Checkpoint(1624844712, 1300000),
_Checkpoint(1625597720, 1310000),
_Checkpoint(1626351652, 1320000),
_Checkpoint(1627106054, 1330000),
_Checkpoint(1627860648, 1340000),
_Checkpoint(1628614015, 1350000),
_Checkpoint(1629368398, 1360000),
_Checkpoint(1630122679, 1370000),
_Checkpoint(1630877190, 1380000),
_Checkpoint(1631631884, 1390000),
_Checkpoint(1632385851, 1400000),
_Checkpoint(1633141006, 1410000),
_Checkpoint(1633895592, 1420000),
_Checkpoint(1634649550, 1430000),
_Checkpoint(1635403121, 1440000),
_Checkpoint(1636157669, 1450000),
_Checkpoint(1636915133, 1460000),
_Checkpoint(1637670036, 1470000),
_Checkpoint(1638424121, 1480000),
_Checkpoint(1639177413, 1490000),
_Checkpoint(1639931234, 1500000),
_Checkpoint(1640684476, 1510000),
_Checkpoint(1641438827, 1520000),
_Checkpoint(1642192402, 1530000),
_Checkpoint(1642946383, 1540000),
_Checkpoint(1643700901, 1550000),
_Checkpoint(1644455522, 1560000),
_Checkpoint(1645208504, 1570000),
_Checkpoint(1645962081, 1580000),
_Checkpoint(1646715150, 1590000),
_Checkpoint(1647465727, 1600000),
_Checkpoint(1648219045, 1610000),
_Checkpoint(1648973131, 1620000),
_Checkpoint(1649727422, 1630000),
_Checkpoint(1650481408, 1640000),
_Checkpoint(1651234287, 1650000),
_Checkpoint(1651989865, 1660000),
_Checkpoint(1652743327, 1670000),
_Checkpoint(1653497455, 1680000),
_Checkpoint(1654252094, 1690000),
_Checkpoint(1655005944, 1700000),
_Checkpoint(1655759708, 1710000),
_Checkpoint(1656513765, 1720000),
_Checkpoint(1657268358, 1730000),
_Checkpoint(1658022453, 1740000),
_Checkpoint(1658777019, 1750000),
_Checkpoint(1659531133, 1760000),
_Checkpoint(1660283907, 1770000),
_Checkpoint(1661037645, 1780000),
_Checkpoint(1661791977, 1790000),
_Checkpoint(1662544394, 1800000),
_Checkpoint(1663298018, 1810000),
_Checkpoint(1664051450, 1820000),
_Checkpoint(1664805547, 1830000),
_Checkpoint(1665558994, 1840000),
_Checkpoint(1666312456, 1850000),
_Checkpoint(1667066473, 1860000),
_Checkpoint(1667823393, 1870000),
_Checkpoint(1668576623, 1880000),
_Checkpoint(1669330657, 1890000),
_Checkpoint(1670084156, 1900000),
_Checkpoint(1670837831, 1910000),
_Checkpoint(1671591094, 1920000),
_Checkpoint(1672343975, 1930000),
_Checkpoint(1673097703, 1940000),
_Checkpoint(1673852016, 1950000),
_Checkpoint(1674605810, 1960000),
_Checkpoint(1675360050, 1970000),
_Checkpoint(1676113710, 1980000),
_Checkpoint(1676867174, 1990000),
_Checkpoint(1677620242, 2000000),
_Checkpoint(1678374852, 2010000),
_Checkpoint(1679125063, 2020000),
_Checkpoint(1679878121, 2030000),
_Checkpoint(1680632254, 2040000),
_Checkpoint(1681386518, 2050000),
_Checkpoint(1682140381, 2060000),
_Checkpoint(1682894564, 2070000),
_Checkpoint(1683648178, 2080000),
_Checkpoint(1684401932, 2090000),
_Checkpoint(1685156464, 2100000),
_Checkpoint(1685910040, 2110000),
_Checkpoint(1686663406, 2120000),
_Checkpoint(1687417875, 2130000),
_Checkpoint(1688172069, 2140000),
_Checkpoint(1688925610, 2150000),
_Checkpoint(1689679500, 2160000),
_Checkpoint(1690432691, 2170000),
_Checkpoint(1691186349, 2180000),
_Checkpoint(1691940901, 2190000),
_Checkpoint(1692694780, 2200000),
_Checkpoint(1693449743, 2210000),
_Checkpoint(1694203665, 2220000),
_Checkpoint(1694957643, 2230000),
_Checkpoint(1695710712, 2240000),
_Checkpoint(1696465787, 2250000),
_Checkpoint(1697218701, 2260000),
_Checkpoint(1697973031, 2270000),
_Checkpoint(1698726864, 2280000),
_Checkpoint(1699483348, 2290000),
_Checkpoint(1700237650, 2300000),
_Checkpoint(1700991210, 2310000),
_Checkpoint(1701745742, 2320000),
_Checkpoint(1702500723, 2330000),
_Checkpoint(1703253121, 2340000),
_Checkpoint(1704007238, 2350000),
_Checkpoint(1704760935, 2360000),
_Checkpoint(1705514669, 2370000),
_Checkpoint(1706268087, 2380000),
_Checkpoint(1707021423, 2390000),
_Checkpoint(1707776257, 2400000),
_Checkpoint(1708529542, 2410000),
_Checkpoint(1709283065, 2420000),
_Checkpoint(1710037165, 2430000),
_Checkpoint(1710787780, 2440000),
_Checkpoint(1711541488, 2450000),
_Checkpoint(1712294996, 2460000),
_Checkpoint(1713048843, 2470000),
_Checkpoint(1713803486, 2480000),
_Checkpoint(1714557127, 2490000),
_Checkpoint(1715311181, 2500000),
_Checkpoint(1716064271, 2510000),
_Checkpoint(1716818792, 2520000),
_Checkpoint(1717572765, 2530000),
_Checkpoint(1718326333, 2540000),
_Checkpoint(1719080275, 2550000),
_Checkpoint(1719834297, 2560000),
_Checkpoint(1720588042, 2570000),
_Checkpoint(1721341832, 2580000),
_Checkpoint(1722095122, 2590000),
_Checkpoint(1722848604, 2600000),
_Checkpoint(1723602357, 2610000),
_Checkpoint(1724355843, 2620000),
_Checkpoint(1725110085, 2630000),
_Checkpoint(1725863681, 2640000),
_Checkpoint(1726616892, 2650000),
_Checkpoint(1727371443, 2660000),
_Checkpoint(1728124893, 2670000),
_Checkpoint(1728878141, 2680000),
_Checkpoint(1729631572, 2690000),
_Checkpoint(1730385888, 2700000),
_Checkpoint(1731142437, 2710000),
_Checkpoint(1731896266, 2720000),
_Checkpoint(1732650193, 2730000),
_Checkpoint(1733403626, 2740000),
_Checkpoint(1734157390, 2750000),
_Checkpoint(1734911442, 2760000),
_Checkpoint(1735665720, 2770000),
_Checkpoint(1736419845, 2780000),
_Checkpoint(1737173511, 2790000),
_Checkpoint(1737926795, 2800000),
_Checkpoint(1738681511, 2810000),
_Checkpoint(1739434236, 2820000),
_Checkpoint(1740188214, 2830000),
_Checkpoint(1740941352, 2840000),
_Checkpoint(1741692338, 2850000),
_Checkpoint(1742445247, 2860000),
_Checkpoint(1743198927, 2870000),
_Checkpoint(1743952947, 2880000),
_Checkpoint(1744706370, 2890000),
_Checkpoint(1745461377, 2900000),
_Checkpoint(1746214735, 2910000),
_Checkpoint(1746967908, 2920000),
_Checkpoint(1747722271, 2930000),
_Checkpoint(1748474582, 2940000),
_Checkpoint(1749228582, 2950000),
_Checkpoint(1749983372, 2960000),
_Checkpoint(1750737278, 2970000),
_Checkpoint(1751490762, 2980000),
_Checkpoint(1752243931, 2990000),
_Checkpoint(1752997873, 3000000),
_Checkpoint(1753751756, 3010000),
_Checkpoint(1754504935, 3020000),
_Checkpoint(1755259449, 3030000),
_Checkpoint(1756012904, 3040000),
_Checkpoint(1756765335, 3050000),
_Checkpoint(1757519206, 3060000),
_Checkpoint(1758272484, 3070000),
_Checkpoint(1759025597, 3080000),
_Checkpoint(1759778957, 3090000),
_Checkpoint(1760532780, 3100000),
_Checkpoint(1761286210, 3110000),
_Checkpoint(1762039783, 3120000),
_Checkpoint(1762796627, 3130000),
_Checkpoint(1763550532, 3140000),
_Checkpoint(1764303860, 3150000),
_Checkpoint(1765057381, 3160000),
_Checkpoint(1765811222, 3170000),
_Checkpoint(1766564356, 3180000),
_Checkpoint(1767318326, 3190000),
_Checkpoint(1768071956, 3200000),
_Checkpoint(1768824957, 3210000),
_Checkpoint(1769578781, 3220000),
_Checkpoint(1770332206, 3230000),
_Checkpoint(1771086265, 3240000),
_Checkpoint(1771840180, 3250000),
_Checkpoint(1772592783, 3260000),
_Checkpoint(1773343947, 3270000),
_Checkpoint(1774097085, 3280000),
_Checkpoint(1774850161, 3290000),
_Checkpoint(1775603525, 3300000),
_Checkpoint(1776358117, 3310000),
];
static int getBlockHeightByTime(DateTime time) {
final t = time.toUtc().millisecondsSinceEpoch;
if (_points.isEmpty) return 0;
if (t <= _points.first.timeMs) {
return 1;
}
for (int i = 0; i < _points.length - 1; i++) {
final p0 = _points[i];
final p1 = _points[i + 1];
if (t >= p0.timeMs && t < p1.timeMs) {
return _interpolate(
p0.height,
p1.height,
p0.timeMs,
p1.timeMs,
t,
);
}
}
final last = _points[_points.length - 1];
final prev = _points[_points.length - 2];
final avgBlockTimeMs = (last.timeMs - prev.timeMs) / (last.height - prev.height);
final blocksSinceLast = ((t - last.timeMs) / avgBlockTimeMs).floor();
return last.height + blocksSinceLast;
}
static DateTime getTimeByBlockHeight(int height) {
if (_points.isEmpty) return DateTime.fromMillisecondsSinceEpoch(0, isUtc: true);
if (height <= 1) {
return DateTime.fromMillisecondsSinceEpoch(_points.first.timeMs, isUtc: true);
}
for (int i = 0; i < _points.length - 1; i++) {
final p0 = _points[i];
final p1 = _points[i + 1];
if (height >= p0.height && height < p1.height) {
final t = _interpolate(
p0.timeMs,
p1.timeMs,
p0.height,
p1.height,
height,
);
return DateTime.fromMillisecondsSinceEpoch(t, isUtc: true);
}
}
final last = _points[_points.length - 1];
final prev = _points[_points.length - 2];
final avgBlockTimeMs = (last.timeMs - prev.timeMs) / (last.height - prev.height);
final msSinceLast = ((height - last.height) * avgBlockTimeMs).floor();
return DateTime.fromMillisecondsSinceEpoch(last.timeMs + msSinceLast, isUtc: true);
}
static int _interpolate(
int h0,
int h1,
int t0,
int t1,
int t,
) {
final ratio = (t - t0) / (t1 - t0);
return h0 + ((h1 - h0) * ratio).floor();
}
}
class _Checkpoint {
final int timeMs;
final int height;
_Checkpoint(int timeSec, this.height) : timeMs = timeSec * 1000;
}
+140 -37
View File
@@ -1,19 +1,20 @@
import 'dart:convert';
import "dart:convert";
import 'package:bech32/bech32.dart';
import 'package:cw_core/amount/money.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
import "package:bech32/bech32.dart";
import "package:cw_core/amount/money.dart";
import "package:cw_core/crypto_currency.dart";
import "package:cw_core/utils/proxy_wrapper.dart";
const _BOLT_PREFIXES = ["lnbcrt", "lntbs", "lnbc", "lntb"];
const _LUD17_PREFIXES = ['lnurlw', 'lnurlc', 'lnurlp', 'keyauth'];
const _LUD17_PREFIXES = ["lnurlw", "lnurlc", "lnurlp", "keyauth"];
bool isBolt11ZeroInvoice(String invoice) {
try {
final request = Bech32Codec().decode(invoice.replaceFirst("lightning:", ""), invoice.length);
final request =
const Bech32Codec().decode(invoice.replaceFirst("lightning:", ""), invoice.length);
final prefix =
_BOLT_PREFIXES.firstWhere((prefix) => request.hrp.startsWith(prefix), orElse: () => "");
_BOLT_PREFIXES.firstWhere(request.hrp.startsWith, orElse: () => "");
return request.hrp.length == prefix.length;
} catch (e) {
@@ -23,48 +24,56 @@ bool isBolt11ZeroInvoice(String invoice) {
/// Get the amount of a Bolt 11 Invoice in
int? _getAmountBolt11Msat(String invoice) {
final request = Bech32Codec().decode(invoice.replaceFirst("lightning:", ""), invoice.length);
final request =
const Bech32Codec().decode(invoice.replaceFirst("lightning:", ""), invoice.length);
final prefix = _BOLT_PREFIXES.firstWhere(
(prefix) => request.hrp.startsWith(prefix),
orElse: () => throw FormatException('Invalid BOLT11 invoice: unknown HRP prefix.'),
request.hrp.startsWith,
orElse: () => throw const FormatException("Invalid BOLT11 invoice: unknown HRP prefix."),
);
final amountPart = request.hrp.substring(prefix.length);
if (amountPart.isEmpty) return null; // zero-amount invoice
if (amountPart.isEmpty) {
return null; // zero-amount invoice
}
final hasMultiplier = RegExp(r'[munp]$').hasMatch(amountPart);
final multiplier = hasMultiplier ? amountPart[amountPart.length - 1] : '';
final hasMultiplier = RegExp(r"[munp]$").hasMatch(amountPart);
final multiplier = hasMultiplier ? amountPart[amountPart.length - 1] : "";
final numberStr = hasMultiplier ? amountPart.substring(0, amountPart.length - 1) : amountPart;
if (numberStr.isEmpty || !RegExp(r'^\d+$').hasMatch(numberStr)) {
throw FormatException('Invalid BOLT11 invoice: invalid amount number in HRP.');
if (numberStr.isEmpty || !RegExp(r"^\d+$").hasMatch(numberStr)) {
throw const FormatException("Invalid BOLT11 invoice: invalid amount number in HRP.");
}
final amount = int.parse(numberStr);
switch (multiplier) {
case '': // bitcoin
case "": // bitcoin
return amount * 100000000000;
case 'm': // milli-bitcoin
case "m": // milli-bitcoin
return amount * 100000000;
case 'u': // micro-bitcoin
case "u": // micro-bitcoin
return amount * 100000;
case 'n': // nano-bitcoin
case "n": // nano-bitcoin
return amount * 100;
case 'p':
case "p":
if (amount % 10 != 0) {
throw FormatException('Invalid BOLT11 invoice: amount not representable in whole msat.');
throw const FormatException(
"Invalid BOLT11 invoice: amount not representable in whole msat.",
);
}
return amount ~/ 10;
default:
throw FormatException('Invalid BOLT11 invoice: unknown amount multiplier.');
throw const FormatException("Invalid BOLT11 invoice: unknown amount multiplier.");
}
}
Money? getBolt11Amount(String invoice) {
final msat = _getAmountBolt11Msat(invoice);
if (msat == null || msat % 1000 != 0) return null;
if (msat == null || msat % 1000 != 0) {
return null;
}
return Money.fromInt(msat ~/ 1000, CryptoCurrency.btcln);
}
@@ -73,30 +82,99 @@ class LNURL {
final url = decode(lnurl);
final response = await ProxyWrapper().get(clearnetUri: url);
if (response.statusCode != 200) return null;
if (response.statusCode != 200) {
return null;
}
try {
final body = jsonDecode(response.body) as Map;
final tag = body["tag"] as String?;
if (tag != "payRequest") return null;
if (tag != "payRequest") {
return null;
}
final msat = body["minSendable"] as int?;
final maxSendable = body["maxSendable"] as int?;
// if minSendable and maxSendable are the same we assume a specific payment request
if (msat != maxSendable) return null;
if (msat != maxSendable) {
return null;
}
if (msat == null || msat % 1000 != 0) return null;
if (msat == null || msat % 1000 != 0) {
return null;
}
return Money.fromInt(msat ~/ 1000, CryptoCurrency.btcln);
} catch (_) {
return null;
}
}
static Future<LNURLWithdrawRequest?> getWithdrawRequest(String lnurl) async {
final url = decode(lnurl);
final response = await ProxyWrapper().get(clearnetUri: url);
if (response.statusCode != 200) {
return null;
}
try {
final body = jsonDecode(response.body) as Map;
final tag = body["tag"] as String?;
if (tag != "withdrawRequest") {
return null;
}
final minWithdrawable = body["minWithdrawable"] as int?;
final maxWithdrawable = body["maxWithdrawable"] as int?;
if ((minWithdrawable == null || minWithdrawable % 1000 != 0) ||
(maxWithdrawable == null || maxWithdrawable % 1000 != 0)) {
return null;
}
return LNURLWithdrawRequest(
callback: body["callback"] as String,
k1: body["k1"] as String,
description: body["defaultDescription"] as String,
minAmount: Money.fromInt(minWithdrawable ~/ 1000, CryptoCurrency.btcln),
maxAmount: Money.fromInt(maxWithdrawable ~/ 1000, CryptoCurrency.btcln),
);
} catch (_) {
return null;
}
}
Future<bool> commitWithdrawRequest(LNURLWithdrawRequest request, String invoice) async {
try {
final url = Uri.parse(request.callback);
url.queryParameters["k1"] = request.k1;
url.queryParameters["pr"] = invoice;
final response = await ProxyWrapper().get(clearnetUri: url);
if (response.statusCode != 200) {
return false;
}
final body = jsonDecode(response.body) as Map;
final status = body["status"] as String?;
if (status == "OK") {
return true;
}
return false;
} catch (_) {
return false;
}
}
static String encode(String url) {
final raw = _convert(utf8.encode(url), 8, 5, true);
return const Bech32Codec().encode(Bech32('lnurl', raw), 999);
return const Bech32Codec().encode(Bech32("lnurl", raw), 999);
}
static Uri decode(String encodedUrl) {
@@ -114,9 +192,10 @@ class LNURL {
if (_LUD17_PREFIXES.contains(decodedUri.scheme)) {
/// If the non-bech32 LNURL is a Tor address, the port has to be http instead of https for the clearnet LNURL so check if the host ends with '.onion' or '.onion.'
decodedUri = decodedUri.replace(
scheme: decodedUri.host.endsWith('onion') || decodedUri.host.endsWith('onion.')
? 'http'
: 'https');
scheme: decodedUri.host.endsWith("onion") || decodedUri.host.endsWith("onion.")
? "http"
: "https",
);
} else {
/// Try to parse the input as a lnUrl. Will throw an error if it fails.
final lnUrl = _findLnUrl(encodedUrl);
@@ -129,12 +208,30 @@ class LNURL {
}
}
class LNURLWithdrawRequest {
const LNURLWithdrawRequest({
required this.callback,
required this.k1,
required this.description,
required this.minAmount,
required this.maxAmount,
});
final String callback;
final String k1;
final String description;
final Money minAmount;
final Money maxAmount;
}
/// Parse and return a given lnurl string if it's valid. Will remove
/// `lightning:` from the beginning of it if present.
String _findLnUrl(String input) {
final res = RegExp(r',*?((lnurl)([0-9]+[a-z0-9]+))').allMatches(input.toLowerCase());
final res = RegExp(r",*?((lnurl)([0-9]+[a-z0-9]+))").allMatches(input.toLowerCase());
if (res.length != 1) throw ArgumentError('Not a valid lnurl string');
if (res.length != 1) {
throw ArgumentError("Not a valid lnurl string");
}
return res.first.group(0)!;
}
@@ -156,10 +253,16 @@ List<int> _convert(List<int> data, int inBits, int outBits, bool pad) {
}
if (pad) {
if (bits > 0) result.add((value << (outBits - bits)) & maxV);
if (bits > 0) {
result.add((value << (outBits - bits)) & maxV);
}
} else {
if (bits >= inBits) throw Exception('[BECH32] Excess padding');
if ((value << (outBits - bits)) & maxV > 0) throw Exception('[BECH32] Non-zero padding');
if (bits >= inBits) {
throw Exception("[BECH32] Excess padding");
}
if ((value << (outBits - bits)) & maxV > 0) {
throw Exception("[BECH32] Non-zero padding");
}
}
return result;
+5 -5
View File
@@ -394,10 +394,10 @@ packages:
dependency: transitive
description:
name: meta
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev"
source: hosted
version: "1.18.0"
version: "1.17.0"
mime:
dependency: transitive
description:
@@ -738,10 +738,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
url: "https://pub.dev"
source: hosted
version: "0.7.11"
version: "0.7.10"
torch_dart:
dependency: "direct main"
description:
@@ -838,5 +838,5 @@ packages:
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.10.0-0 <4.0.0"
dart: ">=3.9.0 <4.0.0"
flutter: ">=3.27.0"
+4 -4
View File
@@ -417,10 +417,10 @@ packages:
dependency: transitive
description:
name: meta
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev"
source: hosted
version: "1.18.0"
version: "1.17.0"
mime:
dependency: transitive
description:
@@ -769,10 +769,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
url: "https://pub.dev"
source: hosted
version: "0.7.11"
version: "0.7.10"
torch_dart:
dependency: transitive
description:
+4
View File
@@ -257,6 +257,10 @@ void setRecoveringFromSeed({required bool isRecovery}) {
final storeMutex = Mutex();
void refresh() {
currentWallet!.refresh();
}
int lastStorePointer = 0;
int lastStoreHeight = 0;
void storeSync({bool force = false}) async {
+5
View File
@@ -150,6 +150,11 @@ void restoreWalletFromSeedSync(
_lastOpenedWallet = path;
}
void storePassphrase({required String path, required String passphrase}) {
currentWallet!.setCacheAttribute(key: "cakewallet.passphrase", value: passphrase);
currentWallet!.store(path: path);
}
void restoreWalletFromKeys(
{required String path,
required String password,
+13 -5
View File
@@ -382,22 +382,30 @@ abstract class MoneroWalletBase
}
MoneroTrezorService? trezorService;
Future<Trezor> _getTrezor() async {
if (trezorService == null) throw Exception("Trezor not connected");
final trezor = Trezor(trezorService!);
await trezor.newPassphraseSession(passphrase);
return trezor;
}
Future<void> syncTrezor() async {
if (trezorService == null) throw Exception("Trezor not connected");
final ptr = Pointer<Void>.fromAddress(currentWallet!.ffiAddress());
final tdis = monero.Wallet_exportTrezorTdis(ptr);
final response = await Trezor(trezorService!).keyImageSync(tdis);
final trezor = await _getTrezor();
final response = await trezor.keyImageSync(tdis);
final success = monero.Wallet_importTrezorEncryptedKeyImagesJson(ptr, response);
if (!success) throw Exception(monero.Wallet_errorString(ptr));
}
Future<String> signTrezorTransaction(String json) async {
if (trezorService == null) throw Exception("Trezor not connected");
return Trezor(trezorService!).signTransaction(json);
final trezor = await _getTrezor();
return trezor.signTransaction(json);
}
@override
+20 -5
View File
@@ -44,10 +44,15 @@ class MoneroNewWalletCredentials extends WalletCredentials {
}
class MoneroRestoreWalletFromHardwareCredentials extends WalletCredentials {
MoneroRestoreWalletFromHardwareCredentials(
{required String name, required this.hardwareWalletService, int height = 0, String? password})
: super(name: name, password: password, height: height);
HardwareWalletService hardwareWalletService;
MoneroRestoreWalletFromHardwareCredentials({
required String name,
required this.hardwareWalletService,
int height = 0,
String? password,
String? passphrase,
}) : super(name: name, password: password, height: height, passphrase: passphrase);
final HardwareWalletService hardwareWalletService;
}
class MoneroRestoreWalletFromSeedCredentials extends WalletCredentials {
@@ -306,7 +311,13 @@ class MoneroWalletService extends WalletService<
deviceName: 'Ledger',
);
} else if (credentials.hardwareWalletService case MoneroTrezorService service) {
final watchCredentials = await Trezor(service).getWatchCredentials();
final trezor = Trezor(service);
if (credentials.passphrase != null) {
await trezor.newPassphraseSession(credentials.passphrase);
}
final watchCredentials = await trezor.getWatchCredentials();
monero_wallet_manager.restoreWalletFromKeys(
path: path,
@@ -317,6 +328,10 @@ class MoneroWalletService extends WalletService<
viewKey: watchCredentials.watchKey,
spendKey: "",
);
if (credentials.passphrase != null) {
monero_wallet_manager.storePassphrase(path: path, passphrase: credentials.passphrase!);
}
}
final wallet = MoneroWallet(
+9
View File
@@ -21,6 +21,15 @@ class Trezor {
Trezor(this.service);
String? _sessionPassphrase;
Future<void> newPassphraseSession(String? passphrase) async {
if (passphrase == null) return;
if (_sessionPassphrase == passphrase) return;
_sessionPassphrase = passphrase;
return service.client.createChannel(passphrase: _sessionPassphrase);
}
Future<MoneroTrezorWatchCredentials> getWatchCredentials() async {
final credentials = await TrezorMonero(service.client).getWatchCredentials();
+9 -9
View File
@@ -505,10 +505,10 @@ packages:
dependency: transitive
description:
name: meta
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev"
source: hosted
version: "1.18.0"
version: "1.17.0"
mime:
dependency: transitive
description:
@@ -906,10 +906,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
url: "https://pub.dev"
source: hosted
version: "0.7.11"
version: "0.7.10"
torch_dart:
dependency: transitive
description:
@@ -921,8 +921,8 @@ packages:
dependency: "direct main"
description:
path: trezor-flutter
ref: d1289dfa4247a93ee57342dfd249957b339915ef
resolved-ref: d1289dfa4247a93ee57342dfd249957b339915ef
ref: "944080d950a62dd48a199b9736237cbe580351de"
resolved-ref: "944080d950a62dd48a199b9736237cbe580351de"
url: "https://github.com/cake-tech/trezor-flutter.git"
source: git
version: "0.1.0"
@@ -930,8 +930,8 @@ packages:
dependency: transitive
description:
path: trezor_usb_transport
ref: d1289dfa4247a93ee57342dfd249957b339915ef
resolved-ref: d1289dfa4247a93ee57342dfd249957b339915ef
ref: "944080d950a62dd48a199b9736237cbe580351de"
resolved-ref: "944080d950a62dd48a199b9736237cbe580351de"
url: "https://github.com/cake-tech/trezor-flutter.git"
source: git
version: "0.0.1"
@@ -1048,5 +1048,5 @@ packages:
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.10.0-0 <4.0.0"
dart: ">=3.9.0 <4.0.0"
flutter: ">=3.24.0"
+1 -1
View File
@@ -33,7 +33,7 @@ dependencies:
trezor_flutter:
git:
url: https://github.com/cake-tech/trezor-flutter.git
ref: d1289dfa4247a93ee57342dfd249957b339915ef
ref: 944080d950a62dd48a199b9736237cbe580351de
path: trezor-flutter
dev_dependencies:
+4 -4
View File
@@ -462,10 +462,10 @@ packages:
dependency: transitive
description:
name: meta
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev"
source: hosted
version: "1.18.0"
version: "1.17.0"
mime:
dependency: transitive
description:
@@ -887,10 +887,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
url: "https://pub.dev"
source: hosted
version: "0.7.11"
version: "0.7.10"
torch_dart:
dependency: transitive
description:
+4 -4
View File
@@ -417,10 +417,10 @@ packages:
dependency: transitive
description:
name: meta
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev"
source: hosted
version: "1.18.0"
version: "1.17.0"
mime:
dependency: transitive
description:
@@ -786,10 +786,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
url: "https://pub.dev"
source: hosted
version: "0.7.11"
version: "0.7.10"
torch_dart:
dependency: transitive
description:
+4 -4
View File
@@ -422,10 +422,10 @@ packages:
dependency: transitive
description:
name: meta
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev"
source: hosted
version: "1.18.0"
version: "1.17.0"
mime:
dependency: transitive
description:
@@ -783,10 +783,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
url: "https://pub.dev"
source: hosted
version: "0.7.11"
version: "0.7.10"
torch_dart:
dependency: transitive
description:
-1
View File
@@ -4,7 +4,6 @@
library;
export 'src/zcash_balance.dart';
export 'src/zcash_mnemonic_is_incorrect_exception.dart';
export 'src/zcash_transaction_info.dart';
export 'src/zcash_transactions.dart';
export 'src/zcash_wallet.dart';
-196
View File
@@ -1,196 +0,0 @@
import 'dart:io';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:path/path.dart' as p;
import 'package:zkool/src/rust/api/account.dart' as zkool_account;
import 'package:zkool/src/rust/api/coin.dart' as zkool_coin;
import 'package:zkool/src/rust/api/sync.dart' as zkool_sync;
import 'package:zkool/src/rust/api/pay.dart' as zkool_pay;
import 'package:zkool/src/rust/api/key.dart' as zkool_key;
import 'package:zkool/src/rust/api/network.dart' as zkool_network;
import 'package:zkool/src/rust/pay.dart' as zkool_paydart;
import 'package:zkool/src/rust/frb_generated.dart' as zkool_frb;
class ZkoolSweep {
ZkoolSweep({
required this.currentBalance,
required this.cacheDir,
required this.seed,
required this.passphrase,
required this.address,
required this.url,
required this.height,
});
final int currentBalance;
final String seed;
final String cacheDir;
final String passphrase;
final String address;
final String url;
final int height;
static SweepUpdate msg = SweepUpdate(message: 'Idling', currentHeight: 0, networkHeight: 0);
Future<void> start() async {
try {
await _init();
if (!zkool_key.isValidAddress(address: address)) {
msg.message = "$address is not valid";
return;
}
msg = SweepUpdate(message: 'Starting import', currentHeight: 0, networkHeight: 0);
await _newAccount();
msg.message = "Starting sync";
await warpSync();
msg.message = "Waiting";
while (msg.blocksLeft != 0 && msg.networkHeight != 0) {
await Future.delayed(Duration(seconds: 1));
}
await Future.delayed(Duration(seconds: 5));
final balance = await getBalance();
if (balance.toInt() == balance) {
msg.message = "No need to sweep, balance is the same";
return;
}
msg.message = "Sweeping";
await sweep(balance);
} catch (e) {
msg.message = e.toString();
printV("zkool_sweep: $e");
rethrow;
}
}
Future<BigInt> getBalance() async {
final b = await zkool_sync.balance(c: c);
return b.field0.reduce((final a, final b) => a + b);
}
Future<void> sweep(final BigInt amount) async {
msg.message = "prepare";
final tx = await zkool_pay.prepare(
recipients: [zkool_paydart.Recipient(address: address, amount: amount)],
options: zkool_pay.PaymentOptions(
srcPools: 7,
recipientPaysFee: true,
smartTransparent: false,
),
c: c,
);
msg.message = "getLatestHeight";
final height = await getLatestHeight();
msg.message = "signTransaction";
final signTx = await zkool_pay.signTransaction(pczt: tx, c: c);
msg.message = "extractTransaction";
final txBytes = await zkool_pay.extractTransaction(package: signTx);
msg.message = "broadcastTransaction";
final result = await zkool_pay.broadcastTransaction(height: height, txBytes: txBytes, c: c);
msg.message = "result";
if (result.isEmpty) {
msg.message = "Unknown error";
throw Exception(result);
}
}
static var c = zkool_coin.Coin();
static bool _isInit = false;
File get dbFile => File(p.join(cacheDir, "zkool-tmp-mirror.db"));
Future<int> warpSync() async {
final currentHeight = await getLatestHeight();
msg.networkHeight = currentHeight;
final sync = zkool_sync.synchronize(
accounts: [currentAccount],
currentHeight: currentHeight,
actionsPerSync: 10000,
transparentLimit: 100,
checkpointAge: 200,
c: c,
);
bool success = false;
await sync.listen(
(final syncProgress) {
msg.currentHeight = syncProgress.height;
msg.message = "Sync ${syncProgress.height} / ${syncProgress.time}";
},
onError: (final e) {
msg.message = e.toString();
},
onDone: () {
msg.message = "Sync done!";
success = true;
},
);
if (msg.currentHeight != currentHeight && msg.currentHeight != 0) {
msg.message = "Canceling sync";
await zkool_sync.cancelSync();
await Future.delayed(Duration(seconds: 1));
return warpSync();
}
return 0;
}
Future<int> getLatestHeight() async {
try {
return await zkool_network.getCurrentHeight(c: c);
} catch (e) {
printV("getLatestHeight: $e");
}
return 0;
}
Future<void> _newAccount() async {
final id = await zkool_account.newAccount(
na: zkool_account.NewAccount(
name: 'restoretmp' + DateTime.now().microsecondsSinceEpoch.toString(),
restore: true,
passphrase: "",
key: seed,
aindex: 0,
birth: height,
folder: '',
useInternal: true,
internal: false,
ledger: false,
),
c: c,
);
currentAccount = id;
c = await c.setAccount(account: id);
}
int currentAccount = 0;
Future<void> _init() async {
if (_isInit) return;
_isInit = true;
printV(".init()");
await zkool_frb.RustLib.init();
await zkool_network.initDatadir(directory: dbFile.parent.path);
c = await c.openDatabase(dbFilepath: dbFile.path, password: 'cw_zcash_migration');
c = c.setLwd(url: url, serverType: 0);
printV(".init(): Done");
}
}
class SweepUpdate {
SweepUpdate({required this.message, required this.currentHeight, required this.networkHeight});
String message;
int currentHeight;
int networkHeight;
double get progress {
if (networkHeight == 0) return 0.01;
return currentHeight / networkHeight;
}
int get blocksLeft => networkHeight - currentHeight;
Map<String, dynamic> toJson() => {
"message": message,
"currentHeight": currentHeight,
"networkHeight": networkHeight,
"progress": progress,
};
}
+96
View File
@@ -0,0 +1,96 @@
import 'dart:math';
import 'package:cw_core/get_height_by_date_zec.dart';
import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cw_zcash/cw_zcash.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
import 'package:path/path.dart' as p;
class ZkoolSeed {
ZkoolSeed({required this.seed, required this.passphrase});
factory ZkoolSeed.fromYwalletSeed({required final String ywalletSeed}) {
final splSeed = ywalletSeed.split(" ");
String seed = "";
;
String passphrase = "";
if ([13, 25].contains(splSeed.length)) {
seed = splSeed.take(splSeed.length - 1).join(" ");
passphrase = ywalletSeed.replaceFirst("${seed} ", "");
} else {
seed = ywalletSeed;
}
return ZkoolSeed(seed: seed, passphrase: passphrase);
}
String seed;
String passphrase;
}
class YwalletAccountInfo {
YwalletAccountInfo({
required this.idAccount,
required this.name,
required this.seed,
required this.aindex,
required this.sk,
required this.ivk,
required this.address,
});
final int idAccount;
final String name;
final String seed;
late final zkoolSeed = ZkoolSeed.fromYwalletSeed(ywalletSeed: seed);
final int aindex;
final String sk;
final String ivk;
final String address;
static YwalletAccountInfo fromJson(final Map<String, dynamic> j) => YwalletAccountInfo(
idAccount: j['id_account'],
name: j['name'],
seed: j['seed'],
aindex: j['aindex'],
sk: j['sk'],
ivk: j['ivk'],
address: j['address'],
);
}
Future<void> migrateOldSqliteToZkool2({required final String walletName}) async {
final dbFile = p.join(await pathForWalletTypeDir(type: WalletType.zcash), "zec.db");
final migrateDb = await openDatabase(dbFile);
final outputRaw = await migrateDb.rawQuery(
"SELECT `id_account`, `name`, `seed`, `aindex`, `sk`, `ivk`, `address` FROM `accounts`",
);
final output = outputRaw.map(YwalletAccountInfo.fromJson);
final walletId = await ZcashWalletBase.getLegacyZcashAccountIdForName(walletName);
for (final ya in output) {
if (ya.idAccount != walletId) {
continue;
}
printV("migrating account: ${ya.name}");
int birthHeight = await ZcashHeight.getBlockHeightByTime(DateTime(2026, 1, 1));
final accTxs = await migrateDb.rawQuery(
"SELECT height FROM transactions WHERE account = ${ya.idAccount}",
);
for (final tx in accTxs) {
final txHeight = int.tryParse(tx['height'].toString()) ?? birthHeight;
birthHeight = min(birthHeight, txHeight);
}
final accountId = await ZcashWalletBase.restoreZcashWalletFromSeed(
name: ya.name,
seed: ya.zkoolSeed.seed,
passphrase: ya.zkoolSeed.passphrase,
birthHeight: birthHeight,
);
await ZcashWalletBase.saveAccountId(ya.name, accountId);
return;
}
throw Exception("migration not finished (wallet name: $walletName not found)");
}
+31 -49
View File
@@ -1,14 +1,10 @@
import 'dart:convert';
import 'dart:math';
import 'package:cw_core/amount/money.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/output_info.dart';
import 'package:cw_core/exceptions.dart';
import 'package:cw_core/pending_transaction.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_zcash/cw_zcash.dart';
import 'package:cw_zcash/src/zcash_taddress_rotation.dart';
import 'package:warp_api/data_fb_generated.dart';
import 'package:warp_api/warp_api.dart';
import 'package:zkool/src/rust/api/pay.dart' as zkool_pay;
import 'package:zkool/src/rust/api/network.dart' as zkool_network;
class PendingZcashTransaction with PendingTransaction {
PendingZcashTransaction({
@@ -21,7 +17,7 @@ class PendingZcashTransaction with PendingTransaction {
final ZcashWallet zcashWallet;
final ZcashTransactionCredentials credentials;
final String txPlan;
final zkool_pay.PcztPackage txPlan;
String? _txId;
final Money availableBalance;
@@ -32,56 +28,42 @@ class PendingZcashTransaction with PendingTransaction {
String get hex => '';
@override
Money get amount => Money.fromInt(_totalAmount, CryptoCurrency.zec);
Money get amount {
final isAll = credentials.outputs.fold<bool>(false, (final a, final b) => a || (b.sendAll));
if (isAll) {
return availableBalance - fee;
}
return credentials.outputs
.map((final output) => output.cryptoAmount)
.reduce((final a, final b) => a + b);
}
@override
String get amountFormatted => amount.toString();
int get _totalAmount {
final isAll = credentials.outputs.fold<bool>(false, (final a, final b) => a || (b.sendAll));
if (isAll) {
return availableBalance.amount.toInt();
}
return credentials.outputs.fold<int>(
0,
(final a, final b) => a + b.cryptoAmount.amount.toInt(),
);
}
@override
final Money fee;
@override
Future<void> commit() async {
_txId = await ZcashWalletService.runInDbMutex(
() => WarpApi.signAndBroadcast(ZcashWalletBase.coin, zcashWallet.accountId, txPlan),
);
ZcashWalletBase.temporarySentTx[zcashWallet.accountId] ??= [];
ZcashWalletBase.temporarySentTx[zcashWallet.accountId]?.add(
ShieldedTx(
base64.decode(
ZcashTaddressRotation.flatBuffersPack(
ShieldedTxT(
id: Random().nextInt(pow(2, 32).toInt()),
txId: _txId,
height: 0,
timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000,
value: -_totalAmount,
).pack,
),
),
),
);
await ZcashTransactionInfo.addCachedDestinationAddress(
_txId ?? '',
credentials.outputs.reduce((final o1, final o2) {
return OutputInfo(
cryptoAmount: Money.zero(CryptoCurrency.zec),
address: "${o1.address},${o2.address}",
sendAll: false,
isParsedAddress: false,
await ZcashWalletBase.runWithCoin(
accountId: zcashWallet.accountId,
func: (coin) async {
final signTx = await zkool_pay.signTransaction(pczt: txPlan, c: coin);
final txBytes = await zkool_pay.extractTransaction(package: signTx);
final currentHeight = await zkool_network.getCurrentHeight(c: coin);
final result = await zkool_pay.broadcastTransaction(
height: currentHeight,
txBytes: txBytes,
c: coin,
);
}).address,
printV("result: $result");
final txId = ZcashWalletService.normalizeTxId(result);
if (txId.length != 64) {
throw TransactionCommitFailed(errorMessage: result);
}
_txId = txId;
},
);
await zcashWallet.updateTransactions();
await zcashWallet.updateBalance();
+5
View File
@@ -0,0 +1,5 @@
import 'package:flutter/services.dart';
String uint8ListToHex(final Uint8List bytes) {
return bytes.map((final b) => b.toRadixString(16).padLeft(2, '0')).join();
}
+118
View File
@@ -0,0 +1,118 @@
import 'dart:async';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_zcash/src/zcash_wallet_service.dart';
import 'package:zkool/src/rust/api/coin.dart' as zkool_coin;
import 'package:zkool/src/rust/api/mempool.dart' as zkool_mempool;
typedef ZcashMempoolRefreshCallback = void Function(Set<int> accountIds);
class ZcashMempoolService {
ZcashMempoolService._();
static final ZcashMempoolService instance = ZcashMempoolService._();
zkool_mempool.Mempool? _mempool;
final Map<String, zkool_mempool.MempoolTx> _txsById = {};
zkool_mempool.Mempool get _mempoolInstance => _mempool ??= zkool_mempool.Mempool();
ZcashMempoolRefreshCallback? onAccountsUpdated;
bool _loopRunning = false;
String? _nodeUrl;
StreamSubscription<zkool_mempool.MempoolMsg>? _subscription;
List<zkool_mempool.MempoolTx> txsForAccount(final int accountId) => _txsById.values
.where(
(final tx) =>
tx.notes.any((final n) => n.account == accountId) ||
tx.amounts.any((final a) => a.account == accountId),
)
.toList();
Future<void> ensureRunning(final zkool_coin.Coin c) async {
if (c.url.isEmpty) {
return;
}
if (_loopRunning && _nodeUrl == c.url) {
return;
}
await stop();
_nodeUrl = c.url;
_loopRunning = true;
unawaited(_runLoop(c));
}
Future<void> stop() async {
_loopRunning = false;
await _subscription?.cancel();
_subscription = null;
_txsById.clear();
try {
await _mempool?.cancel();
} catch (e) {
printV("mempool cancel: $e");
}
_mempool = null;
}
Future<void> _runLoop(final zkool_coin.Coin c) async {
while (_loopRunning && _nodeUrl == c.url) {
try {
final completer = Completer<void>();
_subscription = _mempoolInstance
.run(c: c)
.listen(
(final msg) {
if (!_loopRunning) {
return;
}
switch (msg) {
case zkool_mempool.MempoolMsg_BlockHeight():
_onBlockMined();
case zkool_mempool.MempoolMsg_TxId(:final field0):
_onMempoolTx(field0);
}
},
onDone: () {
if (!completer.isCompleted) {
completer.complete();
}
},
onError: (final e) {
printV("mempool stream error: $e");
if (!completer.isCompleted) {
completer.complete();
}
},
);
await completer.future;
} catch (e) {
printV("mempool loop error: $e");
}
if (_loopRunning) {
await Future.delayed(const Duration(seconds: 5));
}
}
}
void _onBlockMined() {
final affected = _allAffectedAccounts();
_txsById.clear();
onAccountsUpdated?.call(affected);
}
void _onMempoolTx(final zkool_mempool.MempoolTx tx) {
final normalized = ZcashWalletService.normalizeTxId(tx.txid);
_txsById[normalized] = tx;
onAccountsUpdated?.call(_affectedAccountsForTx(tx));
}
Set<int> _allAffectedAccounts() => _txsById.values.expand(_affectedAccountsForTx).toSet();
Set<int> _affectedAccountsForTx(final zkool_mempool.MempoolTx tx) => {
...tx.notes.map((final n) => n.account),
...tx.amounts.map((final a) => a.account),
};
}
@@ -1,5 +0,0 @@
class ZcashMnemonicIsIncorrectException implements Exception {
@override
String toString() =>
'Zcash mnemonic has incorrect format. Mnemonic should contain 12 or 24 words separated by space.';
}
+480 -351
View File
@@ -12,368 +12,484 @@
// turned off and replaced with proper solution.
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:math' show max;
import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/sync_status.dart';
import 'package:cw_core/transaction_direction.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cw_zcash/src/util/crc32.dart';
import 'package:cw_zcash/src/zcash_wallet.dart';
import 'package:cw_zcash/src/zcash_wallet_service.dart';
import 'package:flutter/foundation.dart';
import 'package:warp_api/data_fb_generated.dart';
import 'package:warp_api/warp_api.dart';
import 'package:flat_buffers/flat_buffers.dart' as fb;
import 'package:path/path.dart' as p;
import 'package:cw_zcash/src/zkool_compat.dart';
import 'package:cw_zcash/src/zkooltx.dart';
import 'package:zkool/src/rust/api/account.dart' as zkool_account;
import 'package:zkool/src/rust/api/coin.dart' as zkool_coin;
import 'package:zkool/src/rust/api/sync.dart' as zkool_sync;
import 'package:zkool/src/rust/api/pay.dart' as zkool_pay;
import 'package:zkool/src/rust/api/network.dart' as zkool_network;
import 'package:zkool/src/rust/api/sweep.dart' as zkool_sweep;
import 'package:zkool/src/rust/pay.dart' as zkool_paydart;
class ZcashTaddressRotation {
ZcashTaddressRotation(this.accountIndex);
final int accountIndex;
static bool _isStarted = false;
static zkool_coin.Coin get c => ZcashWalletBase.c;
static const int _sweepThreshold = 30000;
static const int _lookahead = 5;
// Matches transparentLimit in ZcashWalletBase._oneshotSync.
static const int _transparentSyncLimit = 100;
static const coin = ZcashWalletBase.coin;
static String seedForOffset(final String seed) {
final seedWords = seed.split(" ");
static zkool_account.Seed seedForOffset(final zkool_account.Seed seed) {
final seedStr = "${seed.mnemonic} ${seed.phrase.replaceAll(" ", "_")}".trim();
final seedWords = seedStr.split(" ");
if ([12, 24].contains(seedWords.length)) {
seedWords.add("");
}
final lastI = seedWords.length - 1;
final crc = CRC32.compute(seed);
seedWords[lastI] += ":tgen:${crc}";
return seedWords.join(" ");
final crc = CRC32.compute(seedStr);
seedWords[lastI] += ":tgen:$crc";
final phrase = seedWords.removeLast();
return zkool_account.Seed(mnemonic: seedWords.join(" "), phrase: phrase, aindex: 0);
}
static bool isSeedForWallet(final String? mainWallet, final String? subWallet) {
static bool zkoolAccountSeedIsEqual(final zkool_account.Seed a, final zkool_account.Seed b) {
return a.mnemonic == b.mnemonic && a.phrase == b.phrase;
}
static bool isSeedForWallet(
final zkool_account.Seed? mainWallet,
final zkool_account.Seed? subWallet,
) {
if (mainWallet == null || subWallet == null) return false;
return seedForOffset(mainWallet.trim()) == subWallet;
return zkoolAccountSeedIsEqual(seedForOffset(mainWallet), subWallet);
}
static Map<int, List<ZkoolTx>> shieldedAccountsTx = {};
static final Map<int, int> _rotationAccountByMain = {};
static final Set<int> _resyncedRotationAccounts = {};
static final Set<String> _poolRescanAttempted = {};
static bool _sweepJobRunning = false;
static final Map<int, DateTime> _lastSweepBroadcastAt = {};
static bool _poolNeedsRescan(final List<zkool_account.TAddressTxCount> slots) {
if (slots.isEmpty) {
return false;
}
final unused = slots.where((final e) => e.txCount == 0);
if (unused.isEmpty) {
return false;
}
final head = unused.first;
final tail = slots.last.dindex;
if (slots.any((final e) => e.dindex > head.dindex && e.txCount > 0)) {
return true;
}
return head.dindex + _transparentSyncLimit <= tail;
}
static Future<void> _scanTransparentChain(final int rotationAccountId) async {
await ZcashWalletBase.runWithCoin(
accountId: rotationAccountId,
func: (final coin) async {
final all = await zkool_account.fetchAddressTxCount(
c: coin,
aggregate: false,
poolFilter: 1,
);
final external = all.where((final e) => e.scope == 0).toList();
if (external.isEmpty) {
return;
}
final maxD = external.map((final e) => e.dindex).reduce(max);
final scanner = await zkool_sweep.TransparentScanner.newInstance();
final height = await zkool_network.getCurrentHeight(c: coin);
final completer = Completer<void>();
final sub = scanner
.run(endHeight: height, gapLimit: maxD + _lookahead + 1, c: coin)
.listen(
(_) {},
onError: (final e) {
printV("transparent chain scan: $e");
if (!completer.isCompleted) {
completer.complete();
}
},
onDone: () {
if (!completer.isCompleted) {
completer.complete();
}
},
);
await Future.any([
completer.future,
Future.delayed(Duration(seconds: (maxD + 2).clamp(8, 45))),
]);
await sub.cancel();
},
);
}
static Future<void> _resyncRotationPool(
final int rotationAccountId,
final int transparentLimit,
) async {
await ZcashWalletBase.runWithCoin(
accountId: rotationAccountId,
func: (final coin) async {
await zkool_sync.cancelSync();
await zkool_account.resetSync(id: rotationAccountId, c: coin);
final height = await zkool_network.getCurrentHeight(c: coin);
final sync = zkool_sync.synchronize(
accounts: [rotationAccountId],
currentHeight: height,
actionsPerSync: 10000,
transparentLimit: transparentLimit,
checkpointAge: 200,
c: coin,
fast: false,
);
final completer = Completer<void>();
sync.listen(
(_) {},
onError: (final e) {
printV("rotation pool resync error: $e");
if (!completer.isCompleted) {
completer.complete();
}
},
onDone: () {
if (!completer.isCompleted) {
completer.complete();
}
},
);
await completer.future;
},
);
}
static Future<void> _ensurePoolScanned(final int rotationAccountId) async {
var slots = await _transparentSlots(rotationAccountId);
final firstSession = _resyncedRotationAccounts.add(rotationAccountId);
if (!firstSession && !_poolNeedsRescan(slots)) {
return;
}
if (!firstSession) {
final head = slots.where((final e) => e.txCount == 0).firstOrNull?.dindex;
final key = '$rotationAccountId:$head';
if (_poolRescanAttempted.contains(key)) {
return;
}
_poolRescanAttempted.add(key);
}
await _ensureLookahead(rotationAccountId);
await _scanTransparentChain(rotationAccountId);
slots = await _transparentSlots(rotationAccountId);
final limit = slots.isEmpty ? _transparentSyncLimit : slots.last.dindex + 1;
printV("rotation pool resync account $rotationAccountId limit $limit");
await _resyncRotationPool(rotationAccountId, limit);
}
static Map<int, List<Account>> rotationAccounts = {};
static Map<int, List<Account>> rotationAccountsUsable = {};
static Map<int, List<ShieldedTx>> shieldedAccountsTx = {};
static Future<void> init() async {
printV("Deserializing previous state");
if (_isStarted) {
return;
}
_isStarted = true;
final pfw = await pathForWalletTypeDir(type: WalletType.zcash);
try {
final f = File(p.join(pfw, "zec-taddr.json"));
if (!f.existsSync()) {
f.writeAsBytesSync(serialize());
}
deserialize(f.readAsBytesSync());
} catch (e) {
printV("Failed to deserialize T address DB for zcash, it doesn't really matter: $e");
}
unawaited(
(() async {
await Future.delayed(Duration(seconds: 5));
await Future.delayed(const Duration(seconds: 2));
return _jobRunner();
})(),
);
}
static Future<void> serializeToFile() async {
final pfw = await pathForWalletTypeDir(type: WalletType.zcash);
final f = File(p.join(pfw, "zec-taddr.json"));
f.writeAsBytesSync(serialize());
}
static Uint8List serialize() {
final data = {
"rotationAccounts": rotationAccounts.map(
(final k, final v) => MapEntry(
k.toRadixString(16),
v.map((final a) => flatBuffersPack(a.unpack().pack)).toList(),
),
),
if (kDebugMode)
"_rotationAccounts": rotationAccounts.map(
(final k, final v) =>
MapEntry(k.toRadixString(16), v.map((final a) => a.toString()).toList()),
),
"rotationAccountsUsable": rotationAccountsUsable.map(
(final k, final v) => MapEntry(
k.toRadixString(16),
v.map((final a) => flatBuffersPack(a.unpack().pack)).toList(),
),
),
if (kDebugMode)
"_rotationAccountsUsable": rotationAccountsUsable.map(
(final k, final v) =>
MapEntry(k.toRadixString(16), v.map((final a) => a.toString()).toList()),
),
"shieldedAccountsTx": shieldedAccountsTx.map(
(final k, final v) => MapEntry(
k.toRadixString(16),
v.map((final a) => flatBuffersPack(a.unpack().pack)).toList(),
),
),
if (kDebugMode)
"_shieldedAccountsTx": shieldedAccountsTx.map(
(final k, final v) =>
MapEntry(k.toRadixString(16), v.map((final a) => a.toString()).toList()),
),
};
if (kDebugMode) {
return utf8.encode(JsonEncoder.withIndent(" ").convert(data));
static Future<int?> getRotationAccountForMainAccount(final int mainAccountId) async {
final cached = _rotationAccountByMain[mainAccountId];
if (cached != null) {
return cached;
}
return utf8.encode(jsonEncode(data));
}
static String flatBuffersPack(final int Function(fb.Builder fbBuilder) pack) {
final fbBuilder = fb.Builder();
final offset = pack(fbBuilder);
fbBuilder.finish(offset);
return base64.encode(fbBuilder.buffer);
}
static void deserialize(final Uint8List bytes) {
// todo: replace keys with sums and reverse in serialize
// final List<String> seeds = [];
// for (int i = 0; i < accounts.length; i++) {
// final acc = accounts[i];
// final backup = WarpApi.getBackup(coin, acc.id);
// await WarpApi.transparentSync(coin, acc.id, syncHeight);
// seeds.add(backup.seed!);
// }
try {
final Map<dynamic, dynamic> data = jsonDecode(utf8.decode(bytes));
rotationAccounts = (data["rotationAccounts"] as Map<String, dynamic>).map(
(final k, final v) => MapEntry(
int.parse(k, radix: 16),
(v as List).map((final a) => Account(atob(a))).toList(),
),
await ZcashWalletBase.runWithCoinMutex.acquire();
final wSeed = await zkool_account.getAccountSeed(c: c, account: mainAccountId);
if (wSeed == null) {
printV("Not running Taddr rotation - seed not found");
return null;
}
final seed = seedForOffset(wSeed);
final accs = await zkool_account.listAccounts(c: c);
for (int i = 0; i < accs.length; i++) {
final accSeed = await zkool_account.getAccountSeed(c: c, account: accs[i].id);
if (accSeed?.mnemonic == seed.mnemonic &&
accSeed?.phrase == seed.phrase &&
accSeed?.aindex == seed.aindex) {
_rotationAccountByMain[mainAccountId] = accs[i].id;
return accs[i].id;
}
}
zkool_account.Account? acc;
for (final a in accs) {
if (a.id == mainAccountId) {
acc = a;
break;
}
}
if (acc == null) {
printV("Main account $mainAccountId not found for Taddr rotation");
return null;
}
final name = CRC32.compute(wSeed.mnemonic).toString();
final id = await newAccount(
name: name,
seed: seed.mnemonic,
passphrase: seed.phrase,
height: acc.birth,
aindex: seed.aindex,
);
rotationAccountsUsable = (data["rotationAccountsUsable"] as Map<String, dynamic>).map(
(final k, final v) => MapEntry(
int.parse(k, radix: 16),
(v as List).map((final a) => Account(atob(a))).toList(),
),
);
shieldedAccountsTx = (data["shieldedAccountsTx"] as Map<String, dynamic>).map(
(final k, final v) => MapEntry(
int.parse(k, radix: 16),
(v as List).map((final a) => ShieldedTx(atob(a))).toList(),
),
);
} catch (e) {
rethrow;
_rotationAccountByMain[mainAccountId] = id;
return id;
} finally {
ZcashWalletBase.runWithCoinMutex.release();
}
}
static Uint8List atob(final String value) =>
Uint8List.fromList(List<int>.from(base64.decode(value)));
// External (scope 0) transparent slots for the rotation account, ordered by
// dindex. zkool derives every slot in 0..=dindex, so unused ones (txCount==0)
// form the disposable look-ahead pool.
static Future<List<zkool_account.TAddressTxCount>> _transparentSlots(
final int rotationAccountId,
) async {
final all = await ZcashWalletBase.runWithCoin(
accountId: rotationAccountId,
func: (final coin) =>
zkool_account.fetchAddressTxCount(c: coin, aggregate: false, poolFilter: 1),
);
return all.where((final e) => e.scope == 0).toList()
..sort((final a, final b) => a.dindex.compareTo(b.dindex));
}
// Keep _lookahead unused disposable addresses ready, generating more whenever
// the pool runs low. Newly generated addresses are scanned by ongoing sync
// (the latest addresses are always in the scan window), so no resync needed.
static Future<void> _ensureLookahead(final int rotationAccountId) async {
final slots = await _transparentSlots(rotationAccountId);
final unused = slots.where((final e) => e.txCount == 0).length;
final toGenerate = _lookahead - unused;
for (var i = 0; i < toGenerate; i++) {
await ZcashWalletBase.runWithCoin(
accountId: rotationAccountId,
func: (final coin) => zkool_account.generateNextDindex(c: coin),
);
}
if (toGenerate > 0) {
printV("topped up disposable taddr pool by $toGenerate (had $unused)");
}
}
static bool _isWalletSynced(final int mainAccountId) {
final wallet = ZcashWalletBase.walletsByAccountId[mainAccountId];
return wallet != null && wallet.syncStatus is SyncedSyncStatus;
}
static Future<BigInt> _rotationTransparentBalance(final int rotationAccount) async {
return ZcashWalletBase.runWithCoin(
accountId: rotationAccount,
func: (final coin) async {
final b = await zkool_sync.balance(c: coin);
return b.field0.elementAt(0);
},
);
}
static Future<List<ZkoolTx>> _loadTxHistory(final int accountId) async {
return ZcashWalletBase.runWithCoin(
accountId: accountId,
func: (final coin) async {
final txsI = await zkool_account.listTxHistory(c: coin);
final txs = <ZkoolTx>[];
for (int i = 0; i < txsI.length; i++) {
final tx = txsI[i];
try {
final details = await zkool_account.getTxDetails(idTx: tx.id, c: coin);
txs.add(ZkoolTx(tx, details));
} catch (e) {
printV("getTxDetails skipped for tx ${tx.id}: $e");
}
}
return txs;
},
);
}
static bool _canBroadcastSweep(final int mainAccountId) {
final lastSweep = _lastSweepBroadcastAt[mainAccountId];
if (lastSweep == null) {
return true;
}
return !lastSweep.isAfter(DateTime.now().subtract(const Duration(seconds: 120)));
}
static Future<void> createAndSweepTAddresses() async {
int chainHeight = 0;
try {
chainHeight = await WarpApi.getLatestHeight(coin);
} catch (e) {
printV("Error getting latest height: $e");
}
final dbHeight = WarpApi.getDbHeight(coin);
final height = dbHeight.unpack();
final syncHeight = height.height;
if (chainHeight != syncHeight) {
printV(
"Waiting for sync to finish: chainHeight(${chainHeight}) != syncHeight(${syncHeight})",
);
if (_sweepJobRunning) {
return;
}
final Map<int, List<ShieldedTx>> newShieldedAccountsTx = {};
final accounts = WarpApi.getAccountList(coin);
if (accounts.isEmpty) return;
final Map<int, String> seeds = {};
for (int i = 0; i < accounts.length; i++) {
final acc = accounts[i];
final backup = WarpApi.getBackup(coin, acc.id);
await WarpApi.transparentSync(coin, acc.id, syncHeight);
if (backup.seed == null) continue;
seeds[acc.id] = backup.seed!;
}
for (int i = 0; i < accounts.length; i++) {
final seed = seeds[accounts[i].id]!;
if ([12, 13, 24, 25].contains(seed.split(" ").length)) {
if (seed.split(" ").last.contains(":tgen:")) continue;
rotationAccounts[accounts[i].id] = [];
_sweepJobRunning = true;
try {
final mainAccountIds = ZcashWalletBase.walletsByAccountId.keys.toList();
if (mainAccountIds.isEmpty) {
await createAndSweepTAddressesForAccount(c.account);
return;
}
}
final raKeys = rotationAccounts.keys.toList();
seeds.removeWhere((_, final val) => raKeys.contains(val));
printV("raKeys: ${raKeys.length}");
for (int i = 0; i < raKeys.length; i++) {
final accs = accounts.where((final a) {
final seed = WarpApi.getBackup(coin, a.id).seed;
return isSeedForWallet(seeds[raKeys[i]], seed);
}).toList();
rotationAccounts[raKeys[i]]!.addAll(accs);
final acc = accountForSeed(seeds[raKeys[i]]!)!;
final txs = WarpApi.getTxsSync(coin, acc.id);
newShieldedAccountsTx[acc.id] ??= [];
newShieldedAccountsTx[acc.id]!.addAll(txs);
}
rotationAccountsUsable = rotationAccounts.map((final k, final v) => MapEntry(k, v.toList()));
printV("rotationAccounts: ${rotationAccounts.length}");
for (int i = 0; i < raKeys.length; i++) {
final acc = accountForSeed(seeds[raKeys[i]]!)!;
newShieldedAccountsTx[acc.id] = [];
for (int j = 0; j < (rotationAccounts[raKeys[i]]?.length ?? 0); j++) {
final txs = WarpApi.getTxsSync(coin, rotationAccounts[raKeys[i]]![j].id);
rotationAccounts[raKeys[i]]![j];
newShieldedAccountsTx[acc.id]!.addAll(txs);
}
rotationAccountsUsable[raKeys[i]]!.removeWhere((final a) {
final txs = WarpApi.getTxsSync(coin, a.id);
return txs.isNotEmpty;
});
// remove hidden addresses
final wis = await WalletInfo.selectList('type = ?', [WalletType.zcash.index]);
final List<String> hiddenAddresses_ = [];
for (int k = 0; k < wis.length; k++) {
final addrs = await wis[k].getHiddenAddresses();
final addr2 = await wis[k].getUsedAddresses();
hiddenAddresses_.addAll(addrs);
hiddenAddresses_.addAll(addr2);
}
final Set<String> hiddenAddresses = hiddenAddresses_.toSet();
rotationAccountsUsable[raKeys[i]]!.removeWhere((final a) {
return hiddenAddresses.contains(WarpApi.getTAddr(coin, a.id));
});
}
printV("rotationAccountsUsable: ${rotationAccountsUsable.length}");
bool didAddNewAccount = false;
for (int i = 0; i < raKeys.length; i++) {
if (rotationAccountsUsable[raKeys[i]]!.length < 5) {
final seed = seedForOffset(seeds[raKeys[i]]!);
final name = CRC32.compute(raKeys[i].toString()).toString();
final id = await ZcashWalletService.runInDbMutex(
() => WarpApi.newAccount(coin, name, seed, rotationAccounts[raKeys[i]]!.length),
);
printV("new id: $id / $seed");
printV("${rotationAccounts[raKeys[i]]}");
printV(raKeys[i]);
rotationAccounts[raKeys[i]]!.forEach((final a) {
final b = WarpApi.getBackup(coin, a.id);
printV("${a.id}");
});
didAddNewAccount = true;
}
}
printV("didAddNewAccount: ${didAddNewAccount}");
if (didAddNewAccount) {
return createAndSweepTAddresses();
}
for (var i = 0; i < raKeys.length; i++) {
final accs = rotationAccounts[raKeys[i]]!;
inner:
for (var j = 0; j < accs.length; j++) {
int bal = 0;
for (final mainAccountId in mainAccountIds) {
try {
bal += await WarpApi.getTBalance(coin, accs[j].id);
} catch (e) {
printV("getTBalance: $e");
return;
await createAndSweepTAddressesForAccount(mainAccountId);
} catch (e, s) {
printV("createAndSweepTAddresses for $mainAccountId error: $e");
s.toString().split("\n").forEach(printV);
}
if (bal < 30000) continue inner;
final to = accountForSeed(seeds[raKeys[i]]!)!;
// final toAddress = WarpApi.getTAddr(coin, to.id);
// final fromSeed = WarpApi.getBackup(coin, to.id);
// final txId = await ZcashWalletService.runInDbMutex(
// () => WarpApi.sweepTransparentSeed(
// to.coin,
// to.id,
// chainHeight,
// fromSeed.seed!,
// 7,
// toAddress,
// 0,
// 1,
// FeeT(fee: 10000, minFee: 0, maxFee: 0, scheme: 0),
// ),
// );
final recipientBuilder = RecipientObjectBuilder(
address: WarpApi.getAddress(coin, to.id, 4),
pools: 4,
feeIncluded: true,
amount: bal,
);
}
} finally {
_sweepJobRunning = false;
}
}
final recipient = Recipient(recipientBuilder.toBytes());
final fee = FeeT(fee: 10000, minFee: 0, maxFee: 0, scheme: 0);
await ZcashWalletBase.loadProver();
final txPlan = await ZcashWalletService.runInDbMutex(
() => WarpApi.prepareTx(
coin,
accs[j].id,
[recipient],
3, // pools: (Transparent + Sapling)
1,
0, // anchorOffset
fee,
static Future<void> createAndSweepTAddressesForAccount(final int cId) async {
if (!_isWalletSynced(cId)) {
printV("rotation sweep waiting for wallet sync (account $cId)");
return;
}
printV("createAndSweepTAddresses for account $cId");
final rotationAccount = await getRotationAccountForMainAccount(cId);
if (rotationAccount == null) {
printV("rotation account is null, bailing out");
return;
}
await _ensureLookahead(rotationAccount);
await _ensurePoolScanned(rotationAccount);
await _refreshWalletAddresses(cId);
BigInt transparentBal = BigInt.zero;
try {
transparentBal = await _rotationTransparentBalance(rotationAccount);
printV("rotation account $rotationAccount transparent balance: $transparentBal");
} catch (e) {
printV("getTBalance: $e");
return;
}
if (transparentBal < BigInt.from(_sweepThreshold)) {
await updateCache(mainAccountId: cId);
await _refreshWalletAfterRotationChange(cId);
return;
}
if (!_canBroadcastSweep(cId)) {
printV("rotation sweep rate-limited for account $cId");
await updateCache(mainAccountId: cId);
await _refreshWalletAfterRotationChange(cId);
return;
}
final toAddress = await ZcashWalletBase.runWithCoin(
accountId: cId,
func: (final coin) async {
final addrs = await zkool_account.getAddresses(c: coin, uaPools: 7);
final orchard = addrs.oaddr;
if (orchard == null || orchard.isEmpty) {
throw Exception('Orchard address unavailable for rotation sweep');
}
return orchard;
},
);
final result = await ZcashWalletBase.runWithCoin(
accountId: rotationAccount,
func: (final coin) async {
final tx = await zkool_pay.prepare(
recipients: [
zkool_paydart.Recipient(assetBase: zecBase, address: toAddress, amount: transparentBal),
],
options: zkool_pay.PaymentOptions(
srcPools: 1,
recipientPaysFee: true,
smartTransparent: false,
),
c: coin,
);
final _txId = await ZcashWalletService.runInDbMutex(
() => WarpApi.signAndBroadcast(ZcashWalletBase.coin, accs[j].id, txPlan),
);
await ZcashWalletService.addShieldedTx(_txId);
printV("shielded: $_txId");
final height = await zkool_network.getCurrentHeight(c: coin);
final signTx = await zkool_pay.signTransaction(pczt: tx, c: coin);
final txBytes = await zkool_pay.extractTransaction(package: signTx);
return zkool_pay.broadcastTransaction(height: height, txBytes: txBytes, c: coin);
},
);
printV("rotation sweep broadcast: $result");
if (result.isEmpty) {
throw Exception("rotation sweep broadcast failed");
}
await Future.delayed(Duration(seconds: 120)); // let it be
return createAndSweepTAddresses();
}
}
final nsatKeys = newShieldedAccountsTx.keys.toList();
for (int i = 0; i < nsatKeys.length; i++) {
inner:
for (int j = 0; j < newShieldedAccountsTx[nsatKeys[i]]!.length; j++) {
final tx = newShieldedAccountsTx[nsatKeys[i]]?[j];
if (tx == null) {
continue inner;
}
final _txId = tx.txId;
if (_txId == null) {
continue inner;
}
if (tx.value < 0) {
await ZcashWalletService.addShieldedTx(_txId);
// continue inner;
}
if (ZcashWalletService.autoshieldTx.contains(_txId)) {
continue inner;
}
}
}
shieldedAccountsTx = newShieldedAccountsTx.map((final k, final v) => MapEntry(k, v.toList()));
await serializeToFile();
return;
await ZcashWalletService.addShieldedTx(result);
_lastSweepBroadcastAt[cId] = DateTime.now();
await updateCache(mainAccountId: cId);
await _refreshWalletAfterRotationChange(cId);
}
static ShieldedTx reverseAmount(final ShieldedTx t) {
final tt = t.unpack();
tt.value = -tt.value;
return ShieldedTx(base64.decode(flatBuffersPack(tt.pack)));
static Future<void> _refreshWalletAfterRotationChange(final int mainAccountId) async {
final wallet = ZcashWalletBase.walletsByAccountId[mainAccountId];
if (wallet == null) {
return;
}
unawaited(wallet.updateTransactions());
unawaited(wallet.updateBalance());
}
static Account? accountForSeed(final String seed) {
final accounts = WarpApi.getAccountList(coin);
static Future<void> updateCache({required final int mainAccountId}) async {
final rotationAccount = await getRotationAccountForMainAccount(mainAccountId);
if (rotationAccount == null) {
printV("rotationAccount is null");
return;
}
await _ensureLookahead(rotationAccount);
await _ensurePoolScanned(rotationAccount);
final txs = await _loadTxHistory(rotationAccount);
shieldedAccountsTx[mainAccountId] = txs;
if (mainAccountId != rotationAccount && shieldedAccountsTx.containsKey(rotationAccount)) {
shieldedAccountsTx.remove(rotationAccount);
}
for (final tx in shieldedAccountsTx[mainAccountId]!) {
if (tx.direction == TransactionDirection.outgoing) {
await ZcashWalletService.addShieldedTx(tx.txHash);
}
}
await _refreshWalletAddresses(mainAccountId);
await _refreshWalletAfterRotationChange(mainAccountId);
}
static Future<void> _refreshWalletAddresses(final int mainAccountId) async {
final wallet = ZcashWalletBase.walletsByAccountId[mainAccountId];
if (wallet == null) {
return;
}
try {
await wallet.walletAddresses.refreshRotationAddresses();
} catch (e) {
printV("rotation address refresh: $e");
}
}
static List<ZkoolTx> rotationTxsForMainAccount(final int mainAccountId) {
return shieldedAccountsTx[mainAccountId] ?? const [];
}
static Future<zkool_account.Account?> accountForSeed(final zkool_account.Seed seed) async {
final accounts = await zkool_account.listAccounts(c: c);
if (accounts.isEmpty) return null;
for (int i = 0; i < accounts.length; i++) {
final acc = accounts[i];
final backup = WarpApi.getBackup(coin, acc.id);
if (backup.seed == seed) {
final backup = await zkool_account.getAccountSeed(c: c, account: acc.id);
if (backup?.mnemonic == seed.mnemonic && backup?.phrase == seed.phrase) {
return acc;
}
}
@@ -383,63 +499,76 @@ class ZcashTaddressRotation {
static Future<void> _jobRunner() async {
for (;;) {
try {
await Future.delayed(Duration(seconds: 5));
await Future.delayed(const Duration(seconds: 5));
await createAndSweepTAddresses();
} catch (e, s) {
printV(e);
printV("createAndSweepTAddresses error: $e");
s.toString().split("\n").forEach(printV);
rethrow;
} finally {
await Future.delayed(Duration(seconds: 30));
await Future.delayed(const Duration(seconds: 30));
}
}
}
static String? addressForAccount(final int accountId, final Set<String>? hiddenAddresses) {
final accs = rotationAccountsUsable[accountId] ?? [];
for (int i = 0; i < accs.length; i++) {
final addr = WarpApi.getTAddr(coin, accs[i].id);
if (hiddenAddresses?.contains(addr) == true) {
continue;
}
return addr;
}
return null;
}
static List<String>? allAddressesForAccount(final int accountId) {
final acc = rotationAccounts[accountId];
if (acc == null) {
printV("Nothing found");
// Current disposable address = first unused slot (lowest dindex).
static Future<String?> addressForAccount(final int accountId) async {
final rotationAccount = await getRotationAccountForMainAccount(accountId);
if (rotationAccount == null) {
return null;
}
for (int i = 0; i < acc.length; i++) {
final b = WarpApi.getBackup(coin, acc[i].id);
printV("$i. ${b.seed?.split(" ").last}, ${b.index}, ${WarpApi.getTAddr(coin, acc[i].id)}");
final slots = await _transparentSlots(rotationAccount);
final unused = slots.where((final e) => e.txCount == 0);
if (unused.isNotEmpty) {
return unused.first.address;
}
return acc.map((final a) => WarpApi.getTAddr(coin, a.id)).toList();
return slots.isNotEmpty ? slots.last.address : null;
}
static List<String>? allUsedAddressesForAccount(final int accountId) {
final seed = WarpApi.getBackup(coin, accountId).seed;
if (seed == null) return [];
final acc = rotationAccounts[seed]?.toList();
if (acc == null) {
printV("Nothing found");
return null;
static Future<List<String>> usedAddressesForAccount(final int accountId) async {
final rotationAccount = await getRotationAccountForMainAccount(accountId);
if (rotationAccount == null) {
return const [];
}
acc.removeWhere((final a1) {
for (int i = 0; i < (rotationAccountsUsable[seed]?.length ?? 0); i++) {
if (rotationAccountsUsable[seed]?[i].id == a1.id) {
return true;
}
}
return false;
});
for (int i = 0; i < acc.length; i++) {
final b = WarpApi.getBackup(coin, acc[i].id);
printV("$i. ${b.seed?.split(" ").last}, ${b.index}, ${WarpApi.getTAddr(coin, acc[i].id)}");
final slots = await _transparentSlots(rotationAccount);
return [
for (final entry in slots)
if (entry.txCount > 0) entry.address,
];
}
// The full disposable pool: used slots plus the unused look-ahead window.
static Future<List<String>> allAddressesForAccount(final int accountId) async {
final rotationAccount = await getRotationAccountForMainAccount(accountId);
if (rotationAccount == null) {
return const [];
}
return acc.map((final a) => WarpApi.getTAddr(coin, a.id)).toList();
final slots = await _transparentSlots(rotationAccount);
return slots.map((final e) => e.address).toList();
}
static Future<int> newAccount({
required final String name,
required final String seed,
required final String passphrase,
required final int height,
required final int aindex,
}) async {
final id = await zkool_account.newAccount(
na: zkool_account.NewAccount(
name: name,
restore: true,
passphrase: passphrase,
key: seed,
aindex: aindex,
birth: height,
folder: '',
pools: 1,
useInternal: true,
internal: false,
ledger: false,
),
c: c,
);
return id;
}
}
+14 -50
View File
@@ -1,15 +1,9 @@
import 'dart:convert';
import 'dart:io';
import 'package:cw_core/amount/money.dart';
import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/transaction_direction.dart';
import 'package:cw_core/transaction_info.dart';
import 'package:cw_core/format_amount.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cw_zcash/cw_zcash.dart';
import 'package:path/path.dart' as p;
import 'package:cw_zcash/src/zkooltx.dart';
class ZcashTransactionInfo extends TransactionInfo {
ZcashTransactionInfo({
@@ -23,6 +17,9 @@ class ZcashTransactionInfo extends TransactionInfo {
required final int confirmations,
required final String to,
final String? memo,
final TxType? txType,
final bool isRotationReceive = false,
final bool isShieldAction = false,
}) {
this.id = id;
this.amount = amount;
@@ -32,23 +29,21 @@ class ZcashTransactionInfo extends TransactionInfo {
this.date = date;
this.isPending = isPending;
this.confirmations = confirmations;
this.to = getCachedDestinationAddress(id);
this.to = to;
if (memo != null && memo.isNotEmpty) {
additionalInfo['memo'] = memo;
}
additionalInfo['autoShield'] = false;
// note: this won't work yet, fee is not in zcash_lib metadata,
// leaving here so it can start working automagically in future
if (amount == fee && to.startsWith("u")) {
additionalInfo['autoShield'] = true;
if (txType != null) {
additionalInfo['txType'] = txType.name;
}
// remove below when above starts working
additionalInfo['autoShield'] = ZcashWalletService.autoshieldTx.contains(txHash);
// --- == === cut here === == ---
if (additionalInfo['autoShield'] == true) {
additionalInfo['isRotationReceive'] = isRotationReceive;
additionalInfo['isAutoShield'] = isShieldAction || ZcashWalletService.isAutoshieldTx(txHash);
if (additionalInfo['isAutoShield'] == true) {
additionalInfo['memo'] ??= '';
additionalInfo['memo'] += '\This is an auto-shielding transaction. Enjoy default privacy!';
additionalInfo['memo'] = additionalInfo['memo'].trim();
additionalInfo['memo'] =
'${additionalInfo['memo']}\nThis is an auto-shielding transaction. Enjoy default privacy!.'
.trim();
}
}
@@ -61,35 +56,4 @@ class ZcashTransactionInfo extends TransactionInfo {
void changeFiatAmount(final String amount) => _fiatAmount = formatAmount(amount);
String? get memo => additionalInfo['memo'] as String?;
static final Map<String, String> _destinationAddressMap = {};
static String? getCachedDestinationAddress(final String txId) {
return _destinationAddressMap[txId] ??
_destinationAddressMap['"$txId"'] ??
_destinationAddressMap[txId.replaceAll('"', '')];
}
static Future<void> addCachedDestinationAddress(final String txId, final String address) async {
_destinationAddressMap[txId] = address;
final pfwt = await pathForWalletTypeDir(type: WalletType.zcash);
final f = File(p.join(pfwt, "sent-tx-map.json"));
f.writeAsStringSync(json.encode(_destinationAddressMap));
}
static Future<void> init() async {
try {
final pfwt = await pathForWalletTypeDir(type: WalletType.zcash);
final f = File(p.join(pfwt, "sent-tx-map.json"));
if (!f.existsSync()) {
f.writeAsStringSync('{}');
}
final tmpMap = json.decode(f.readAsStringSync());
tmpMap.forEach((final k, final v) {
_destinationAddressMap[k.toString()] = v.toString();
});
} catch (e, s) {
printV("failed to deserialize: $e, $s");
}
}
}
File diff suppressed because it is too large Load Diff
+123 -90
View File
@@ -6,7 +6,7 @@ import 'package:cw_core/wallet_info.dart';
import 'package:cw_zcash/cw_zcash.dart';
import 'package:cw_zcash/src/zcash_taddress_rotation.dart';
import 'package:mobx/mobx.dart';
import 'package:warp_api/warp_api.dart';
import 'package:zkool/src/rust/api/account.dart' as zkool_account;
part 'zcash_wallet_addresses.g.dart';
@@ -17,24 +17,34 @@ abstract class ZcashWalletAddressesBase extends WalletAddresses with Store {
addressPageType = ZcashReceivePageOption.typeFromString(walletInfo.addressPageType ?? "");
}
final int accountId;
int accountId;
static const int transparentType = 0;
static const int shieldedSaplingType = 1;
static const int shieldedOrchardType = 2;
static const int unifiedType = 3;
String? _transparentAddress;
String? _saplingAddress;
String? _orchardAddress;
String? _unifiedAddress;
@observable
String? transparentAddress;
@observable
String? _transparentObservableAddress;
@observable
String? saplingAddress;
@observable
String? orchardAddress;
@observable
String? unifiedAddress;
@override
@computed
String get latestAddress {
switch (addressPageType) {
case ZcashAddressType.transparent:
return transparentAddress;
return transparentAddress ?? "unknown transparentAddress";
case ZcashAddressType.transparentRotated:
// just to display something, after the wallet is synced the address
// will start rotating, and since we can't rotate address until the wallet syncs
@@ -46,16 +56,15 @@ abstract class ZcashWalletAddressesBase extends WalletAddresses with Store {
// 3) display first address from rotated pool which is also static until synced
// 2 seems like an obvious winner simply because it offers little to no benefit over 3
// and is noticably less complex in implementation
return transparentAddressRotated ?? transparentAddress;
return _transparentObservableAddress ??
transparentAddress ??
"unknown transparentAddressRotated";
case ZcashAddressType.shieldedSapling:
return saplingAddress;
return saplingAddress ?? "unknown saplingAddress";
case ZcashAddressType.shieldedOrchard:
return orchardAddress;
return orchardAddress ?? "unknown orchardAddress";
case ZcashAddressType.unifiedType:
if (_unifiedAddress == null) {
_unifiedAddress = WarpApi.getAddress(ZcashWalletBase.coin, accountId, 6);
}
return _unifiedAddress ?? "";
return unifiedAddress ?? "unknown unifiedAddress";
}
}
@@ -79,63 +88,21 @@ abstract class ZcashWalletAddressesBase extends WalletAddresses with Store {
await walletInfo.save();
}
String get transparentAddress {
try {
if (_transparentAddress == null) {
_transparentAddress = WarpApi.getTAddr(ZcashWalletBase.coin, accountId);
}
return _transparentAddress ?? "";
} catch (e) {
return "";
}
}
@override
@computed
String? get transparentAddressRotated {
try {
return ZcashTaddressRotation.addressForAccount(accountId, hiddenAddresses);
} catch (e) {
return null;
}
}
String get saplingAddress {
try {
if (_saplingAddress == null) {
_saplingAddress = WarpApi.getAddress(ZcashWalletBase.coin, accountId, 2);
}
return _saplingAddress ?? "";
} catch (e) {
return "";
}
}
String get orchardAddress {
try {
if (_orchardAddress == null) {
_orchardAddress = WarpApi.getAddress(ZcashWalletBase.coin, accountId, 4);
}
return _orchardAddress ?? "";
} catch (e) {
return "";
}
}
String get addressForExchange =>
_transparentObservableAddress ?? transparentAddress ?? "unknown addressForExchange";
@override
@computed
String get addressForExchange => transparentAddressRotated ?? transparentAddress;
@override
@computed
String get addressForBuy => transparentAddressRotated ?? transparentAddress;
String get addressForBuy =>
_transparentObservableAddress ?? transparentAddress ?? "unknown addressForBuy";
@override
bool containsAddress(final String address) {
return this.address == address || addressesMap.values.contains(address);
}
static int get coin => ZcashWalletBase.coin;
@override
@observable
Set<String> hiddenAddresses = {};
@@ -149,37 +116,105 @@ abstract class ZcashWalletAddressesBase extends WalletAddresses with Store {
address = latestAddress;
}
Future<void> _syncRotationHiddenAddresses() async {
final all = (await ZcashTaddressRotation.allAddressesForAccount(accountId)).toSet();
final used = (await ZcashTaddressRotation.usedAddressesForAccount(accountId)).toSet();
hiddenAddresses.removeWhere(all.contains);
hiddenAddresses.addAll(used);
}
@override
Future<void> init() async {
try {
await _init();
} catch (e) {
printV("init failed, retrying in 2 seconds");
printV(e);
await Future.delayed(Duration(seconds: 2));
await init();
}
}
static bool isPlaceholderAddress(final String? address) =>
address == null || address.isEmpty || address.startsWith('unknown ');
String _resolvedWalletInfoAddress() {
for (final candidate in [unifiedAddress, orchardAddress, saplingAddress, transparentAddress]) {
if (!isPlaceholderAddress(candidate)) {
return candidate!;
}
}
return latestAddress;
}
Future<void> _init() async {
await _initAddresses();
await ZcashTaddressRotation.init();
int accountIndex = 0;
addressInfos = {
0:
ZcashTaddressRotation.allAddressesForAccount(accountId)?.map((final v) {
return WalletInfoAddressInfo(
walletInfoId: walletInfo.internalId,
mapKey: ++accountIndex,
accountIndex: 0,
address: v,
label: "",
);
}).toList() ??
[],
};
hiddenAddresses.addAll(
ZcashTaddressRotation.allUsedAddressesForAccount(accountId)?.toSet() ?? {},
final addr = await ZcashWalletBase.runWithCoin(
accountId: accountId,
func: (final coin) => zkool_account.getAddresses(c: coin, uaPools: 7),
);
// addressInfos[0]?.removeWhere((final test) => hiddenAddresses.contains(test.address));
transparentAddress = addr.taddr ?? 'unknown addr.taddr';
saplingAddress = addr.saddr ?? 'unknown addr.saddr';
orchardAddress = addr.oaddr ?? 'unknown addr.oaddr';
unifiedAddress = addr.ua ?? 'unknown addr.ua';
_transparentObservableAddress = await ZcashTaddressRotation.addressForAccount(accountId);
final rotationAddrs = await ZcashTaddressRotation.allAddressesForAccount(accountId);
addressInfos = {
0: [
for (int i = 0; i < rotationAddrs.length; i++)
WalletInfoAddressInfo(
walletInfoId: walletInfo.internalId,
mapKey: i + 1,
accountIndex: 0,
address: rotationAddrs[i],
label: "",
),
],
};
await _syncRotationHiddenAddresses();
await _applyAddressForCurrentType();
if (isPlaceholderAddress(walletInfo.address)) {
final resolved = _resolvedWalletInfoAddress();
if (!isPlaceholderAddress(resolved)) {
walletInfo.address = resolved;
await walletInfo.save();
}
}
await saveAddressesInBox();
}
Future<void> refreshRotationAddresses() async {
_transparentObservableAddress = await ZcashTaddressRotation.addressForAccount(accountId);
final rotationAddrs = await ZcashTaddressRotation.allAddressesForAccount(accountId);
addressInfos = {
0: [
for (int i = 0; i < rotationAddrs.length; i++)
WalletInfoAddressInfo(
walletInfoId: walletInfo.internalId,
mapKey: i + 1,
accountIndex: 0,
address: rotationAddrs[i],
label: "",
),
],
};
await _syncRotationHiddenAddresses();
await _applyAddressForCurrentType();
}
Future<void> _applyAddressForCurrentType() async {
if (_addressPageType == ZcashAddressType.transparentRotated) {
final addr = ZcashTaddressRotation.addressForAccount(accountId, hiddenAddresses);
final addr = await ZcashTaddressRotation.addressForAccount(accountId);
if (addr != null) {
address = addr;
}
return;
}
await saveAddressesInBox();
address = latestAddress;
}
@observable
@@ -198,12 +233,17 @@ abstract class ZcashWalletAddressesBase extends WalletAddresses with Store {
@override
Future<void> saveAddressesInBox() async {
try {
if (!isPlaceholderAddress(address)) {
walletInfo.address = address;
}
await walletInfo.setAddresses(addressesMap);
await walletInfo.setAddressInfos(addressInfos);
await walletInfo.setUsedAddresses(usedAddresses.toList());
await walletInfo.setHiddenAddresses(hiddenAddresses.toList());
await walletInfo.setManualAddresses(manualAddresses.toList());
await walletInfo.save();
await _initAddresses();
await _syncRotationHiddenAddresses();
} catch (e) {
printV("Error saving addresses: $e");
}
@@ -211,10 +251,7 @@ abstract class ZcashWalletAddressesBase extends WalletAddresses with Store {
@override
Future<void> updateAddressesInBox() async {
_transparentAddress = null;
_saplingAddress = null;
_orchardAddress = null;
_unifiedAddress = null;
await _initAddresses();
}
@override
@@ -227,11 +264,7 @@ abstract class ZcashWalletAddressesBase extends WalletAddresses with Store {
if (addressPageType != ZcashAddressType.transparentRotated) {
return [];
}
final List<WalletInfoAddressInfo> allInfos = [];
for (final entry in addressInfos.entries) {
allInfos.addAll(entry.value);
}
return allInfos;
return addressInfos[0] ?? [];
}
@override
+33 -20
View File
@@ -9,6 +9,7 @@ import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_service.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cw_zcash/cw_zcash.dart';
import 'package:cw_zcash/src/migrate.dart';
import 'package:mutex/mutex.dart';
import 'package:path/path.dart' as p;
@@ -25,9 +26,9 @@ class ZcashWalletService
static Mutex dbMutex = Mutex();
static int dbMutexQueue = 0;
static Future<T> runInDbMutex<T>(final Future<T> Function() call) async {
dbMutexQueue++;
await dbMutex.acquire();
try {
printV("dbMutexQueue: ${dbMutexQueue++}");
await dbMutex.acquire();
return await call();
} finally {
dbMutex.release();
@@ -37,10 +38,15 @@ class ZcashWalletService
static Set<String> autoshieldTx = {};
static String normalizeTxId(final String txId) =>
txId.replaceAll(RegExp(r'[^a-fA-F0-9]'), '').toLowerCase();
static bool isAutoshieldTx(final String txHash) => autoshieldTx.contains(normalizeTxId(txHash));
static Future<void> addShieldedTx(final String txId) async {
final pathForWalletType = await pathForWalletTypeDir(type: type);
final shieldedListFile = p.join(pathForWalletType, "autoshield_list.txt");
autoshieldTx.add(txId.replaceAll(RegExp(r'[^a-zA-Z0-9 ]'), ''));
final shieldedListFile = p.join(pathForWalletType, "autoshield_list.v2.txt");
autoshieldTx.add(normalizeTxId(txId));
final shieldedList = File(shieldedListFile);
shieldedList.writeAsStringSync(autoshieldTx.join("\n"));
}
@@ -48,12 +54,16 @@ class ZcashWalletService
static Future<void> loadShieldTxs() async {
try {
final pathForWalletType = await pathForWalletTypeDir(type: type);
final shieldedListFile = p.join(pathForWalletType, "autoshield_list.txt");
final shieldedListFile = p.join(pathForWalletType, "autoshield_list.v2.txt");
final shieldedList = File(shieldedListFile);
if (!shieldedList.existsSync()) {
return;
}
autoshieldTx = shieldedList.readAsLinesSync().toSet();
autoshieldTx = shieldedList
.readAsLinesSync()
.map(normalizeTxId)
.where((final line) => line.isNotEmpty)
.toSet();
} catch (e) {
printV("loadShieldTxs failed: $e");
}
@@ -74,12 +84,21 @@ class ZcashWalletService
@override
Future<bool> isWalletExit(final String name) async {
final path = await pathForWallet(name: name, type: getType());
return File(path).existsSync();
final oldPath = (await pathForWallet(name: name, type: getType()));
final path = (await pathForWallet(name: name, type: getType())) + ".v2";
return File(path).existsSync() || File(oldPath).existsSync();
}
@override
Future<ZcashWallet> openWallet(final String name, final String password) async {
await ZcashWalletBase.$init();
if (await isWalletExit(name)) {
final path = (await pathForWallet(name: name, type: getType())) + ".v2";
if (!File(path).existsSync()) {
await migrateOldSqliteToZkool2(walletName: name);
}
}
await loadShieldTxs();
final walletInfo = await WalletInfo.get(name, getType());
if (walletInfo == null) {
@@ -108,7 +127,7 @@ class ZcashWalletService
@override
Future<void> remove(final String wallet) async {
final path = await pathForWalletDir(name: wallet, type: getType());
final path = (await pathForWalletDir(name: wallet, type: getType()));
final file = Directory(path);
final isExist = file.existsSync();
@@ -129,17 +148,11 @@ class ZcashWalletService
if (currentWalletInfo == null) {
throw Exception('Wallet not found');
}
final accountId = await ZcashWalletBase.getZcashAccountIdForName(currentName);
if (accountId == null) {
throw Exception('Wallet account not found for name: $currentName');
if (!await isWalletExit(currentName)) {
throw Exception('Wallet not found');
}
final currentWallet = ZcashWallet(
currentWalletInfo,
await currentWalletInfo.getDerivationInfo(),
accountId: accountId,
);
await currentWallet.renameWalletFiles(newName);
await ZcashWalletBase.renameWalletFilesForName(fromName: currentName, toName: newName);
final newWalletInfo = currentWalletInfo;
newWalletInfo.id = WalletBase.idFor(newName, getType());
@@ -162,11 +175,11 @@ class ZcashWalletService
final bool? isTestnet,
}) async {
if (credentials.seed == null || credentials.seed!.isEmpty) {
throw ZcashMnemonicIsIncorrectException();
throw Exception("Seed is missing");
}
if (!bip39.validateMnemonic(credentials.seed!)) {
throw ZcashMnemonicIsIncorrectException();
throw Exception("Seed is not valid bip39");
}
return ZcashWalletBase.restore(credentials);
+3
View File
@@ -0,0 +1,3 @@
import 'dart:typed_data';
final zecBase = Uint8List(32);
+312
View File
@@ -0,0 +1,312 @@
import 'dart:convert';
import 'dart:math';
import 'dart:typed_data';
import 'package:cw_core/get_height_by_date_zec.dart';
import 'package:cw_core/transaction_direction.dart';
import 'package:cw_zcash/src/util/hex.dart';
import 'package:zkool/src/rust/api/account.dart' as zkool_account;
enum TxType {
unknown,
selfTransfer, // 0
receive, // 1
sent, // 2
unshield, // 4
shield, // 8
transparentSelfTransfer, // 12
}
enum NotePool { transparent, sapling, orchard, unknown }
class ZkoolTx {
ZkoolTx(final zkool_account.Tx tx, final zkool_account.TxAccount txAccount)
: this._tx = tx,
this._txAccount = txAccount;
final zkool_account.Tx _tx;
final zkool_account.TxAccount _txAccount;
int get height => max(
_tx.height,
_txAccount.height,
); // sometimes returns 0 - higher number would be correct in this case
DateTime get time {
final ts = max(_tx.time, _txAccount.time) * 1000;
if (ts == 0) {
return ZcashHeight.getTimeByBlockHeight(height);
}
return DateTime.fromMillisecondsSinceEpoch(ts);
}
String? get to => _txAccount.outputs.firstOrNull?.address;
Iterable<String> get outputAddresses =>
_txAccount.outputs.map((final o) => o.address).whereType<String>();
Iterable<({String address, int pool, BigInt value})> get outputsWithAddress => _txAccount.outputs
.where((final o) => o.address.isNotEmpty)
.map((final o) => (address: o.address, pool: o.pool, value: o.value));
Iterable<int> get spendPools => _txAccount.spends.map((final s) => s.pool);
Iterable<int> get notePools => _txAccount.notes.map((final n) => n.pool);
String? get memo => _txAccount.memos.firstOrNull?.memo;
BigInt get transparentOrSaplingSpent {
if (_txAccount.spends.isEmpty) {
return BigInt.zero;
}
return _txAccount.spends
.where(
(final s) => s.pool == NotePool.transparent.index || s.pool == NotePool.sapling.index,
)
.fold(BigInt.zero, (final a, final s) => a + s.value);
}
BigInt get orchardReceived {
final fromNotes = _txAccount.notes
.where((final n) => n.pool == NotePool.orchard.index)
.fold(BigInt.zero, (final a, final n) => a + n.value);
if (fromNotes > BigInt.zero) {
return fromNotes;
}
return _txAccount.outputs
.where((final o) => o.pool == NotePool.orchard.index)
.fold(BigInt.zero, (final a, final o) => a + o.value);
}
String get txHash {
final reversed = Uint8List.fromList(_txAccount.txid.reversed.toList());
final txId = uint8ListToHex(reversed);
return txId;
}
BigInt get value => _value.abs();
BigInt get fee => value - _calcValue;
BigInt get _value {
if (_tx.value != 0) {
return BigInt.from(_tx.value);
}
return _calcValue;
}
/////////// calc is short for calculator (calculated in this case)
BigInt get _calcValue {
final noteSum = _txAccount.notes.isEmpty
? BigInt.from(0)
: _txAccount.notes.map((final note) => note.value).reduce((final a, final b) => a + b);
final outputSum = _txAccount.outputs.isEmpty
? BigInt.from(0)
: _txAccount.outputs
.map((final output) => output.value)
.reduce((final a, final b) => a + b);
final spentSum = _txAccount.spends.isEmpty
? BigInt.from(0)
: _txAccount.spends.map((final spent) => spent.value).reduce((final a, final b) => a + b);
return noteSum - outputSum - spentSum;
}
TransactionDirection get direction {
switch (type) {
case TxType.selfTransfer:
case TxType.receive:
case TxType.unshield:
case TxType.shield:
case TxType.transparentSelfTransfer:
return TransactionDirection.incoming;
case TxType.sent:
return TransactionDirection.outgoing;
case TxType.unknown:
return _value > BigInt.from(0)
? TransactionDirection.incoming
: TransactionDirection.outgoing;
}
}
TxType get type {
switch (_tx.tpe) {
case 0:
return TxType.selfTransfer;
case 1:
return TxType.receive;
case 2:
return TxType.sent;
case 4:
return TxType.unshield;
case 8:
return TxType.shield;
case 12:
return TxType.transparentSelfTransfer;
default:
return TxType.unknown;
}
}
Map<String, dynamic> toJson() => {
"tx": {
"id": _tx.id,
"txid": base64.encode(_tx.txid),
"height": _tx.height,
"time": _tx.time,
"value": value.toInt(),
"tpe": _tx.tpe,
"zsaValue": _tx.zsaValue,
"assetDisplay": _tx.assetDisplay,
},
"txaccount": {
"id": _txAccount.id,
"account": _txAccount.account,
"txid": base64.encode(_txAccount.txid),
"height": _txAccount.height,
"time": _txAccount.time,
"notes": _txAccount.notes
.map(
(final note) => {
"id": note.id,
"pool": note.pool,
"height": note.height,
"tx": note.tx,
"scope": note.scope,
"value": note.value.toString(),
"locked": note.locked,
},
)
.toList(),
"spends": _txAccount.spends
.map(
(final spend) => {
"id": spend.id,
"pool": spend.pool,
"height": spend.height,
"value": spend.value.toString(),
"assetDisplay": spend.assetDisplay,
},
)
.toList(),
"outputs": _txAccount.outputs
.map(
(final output) => {
"id": output.id,
"pool": output.pool,
"height": output.height,
"value": output.value.toString(),
"address": output.address,
},
)
.toList(),
"memos": _txAccount.memos
.map(
(final memo) => {
"note": memo.note,
"pool": memo.pool,
"output": memo.output,
"memo": memo.memo,
},
)
.toList(),
},
};
static int _asInt(final dynamic value, {final int fallback = 0}) {
if (value == null) {
return fallback;
}
if (value is int) {
return value;
}
if (value is num) {
return value.toInt();
}
return int.parse(value.toString());
}
static Uint8List _txidFromJson(final dynamic raw) {
if (raw is Uint8List) {
return raw;
}
if (raw is List) {
return Uint8List.fromList(List<int>.from(raw));
}
if (raw is String) {
return base64.decode(raw);
}
throw FormatException("Invalid txid in ZkoolTx json: $raw");
}
static List<T> _listFromJson<T>(final dynamic raw, final T Function(Map<String, dynamic>) parse) {
if (raw is! List) {
return const [];
}
return raw.map((final entry) => parse(Map<String, dynamic>.from(entry as Map))).toList();
}
static ZkoolTx fromJson(final Map<String, dynamic> json) {
final txJson = Map<String, dynamic>.from((json["tx"] as Map?)?.cast<String, dynamic>() ?? json);
final txAccountJson = Map<String, dynamic>.from(
(json["txaccount"] as Map?)?.cast<String, dynamic>() ?? json,
);
return ZkoolTx(
zkool_account.Tx(
id: _asInt(txJson["id"]),
txid: _txidFromJson(txJson["txid"]),
height: _asInt(txJson["height"]),
time: _asInt(txJson["time"]),
value: _asInt(txJson["value"]),
tpe: txJson["tpe"] == null ? null : _asInt(txJson["tpe"]),
zsaValue: _asInt(txJson["zsaValue"]),
assetDisplay: txJson["assetDisplay"] as String? ?? "",
),
zkool_account.TxAccount(
id: _asInt(txAccountJson["id"]),
account: _asInt(txAccountJson["account"]),
txid: _txidFromJson(txAccountJson["txid"]),
height: _asInt(txAccountJson["height"]),
time: _asInt(txAccountJson["time"]),
notes: _listFromJson(
txAccountJson["notes"],
(final a) => zkool_account.TxNote(
id: _asInt(a["id"]),
pool: _asInt(a["pool"]),
height: _asInt(a["height"]),
tx: _asInt(a["tx"]),
value: BigInt.parse(a["value"].toString()),
scope: _asInt(a["scope"]),
locked: a["locked"] as bool? ?? false,
assetDisplay: a["assetDisplay"] as String? ?? "",
),
),
spends: _listFromJson(
txAccountJson["spends"],
(final a) => zkool_account.TxSpend(
id: _asInt(a["id"]),
pool: _asInt(a["pool"]),
height: _asInt(a["height"]),
value: BigInt.parse(a["value"].toString()),
assetDisplay: a["assetDisplay"] as String? ?? "",
),
),
outputs: _listFromJson(
txAccountJson["outputs"],
(final a) => zkool_account.TxOutput(
id: _asInt(a["id"]),
pool: _asInt(a["pool"]),
height: _asInt(a["height"]),
value: BigInt.parse(a["value"].toString()),
address: a["address"] as String? ?? "",
),
),
memos: _listFromJson(
txAccountJson["memos"],
(final a) => zkool_account.TxMemo(
note: a["note"] == null ? null : _asInt(a["note"]),
pool: _asInt(a["pool"]),
output: a["output"] == null ? null : _asInt(a["output"]),
memo: a["memo"] as String?,
),
),
),
);
}
}
+4 -4
View File
@@ -9,8 +9,6 @@ environment:
dependencies:
flutter:
sdk: flutter
warp_api:
path: ../scripts/zcash_lib/packages/warp_api_ffi
cw_core:
path: ../cw_core
mobx: ^2.6.0
@@ -20,11 +18,13 @@ dependencies:
path: any
flat_buffers: any
zkool:
# path: ../../zkool2
git:
url: https://github.com/cake-tech/zkool2
ref: b1642f260da4fa00dd2f8d4dd689073d0b56960a
url: https://github.com/cake-tech/zkool2.git
ref: dbd3dde8d8485428326a58e55660b7404666dd7c
path_provider: any
sqflite_common_ffi: any
dependency_overrides:
decimal: ^2.3.3
+71 -15
View File
@@ -1,4 +1,14 @@
PODS:
- AppAuth (1.7.6):
- AppAuth/Core (= 1.7.6)
- AppAuth/ExternalUserAgent (= 1.7.6)
- AppAuth/Core (1.7.6)
- AppAuth/ExternalUserAgent (1.7.6):
- AppAuth/Core
- AppCheckCore (11.2.0):
- GoogleUtilities/Environment (~> 8.0)
- GoogleUtilities/UserDefaults (~> 8.0)
- PromisesObjC (~> 2.4)
- bitbox_flutter (0.0.1):
- Flutter
- breez_sdk_spark_flutter (0.14.0):
@@ -73,6 +83,34 @@ PODS:
- Flutter
- fluttertoast (0.0.2):
- Flutter
- google_sign_in_ios (0.0.1):
- AppAuth (>= 1.7.4)
- Flutter
- FlutterMacOS
- GoogleSignIn (~> 8.0)
- GTMSessionFetcher (>= 3.4.0)
- GoogleSignIn (8.0.0):
- AppAuth (< 2.0, >= 1.7.3)
- AppCheckCore (~> 11.0)
- GTMAppAuth (< 5.0, >= 4.1.1)
- GTMSessionFetcher/Core (~> 3.3)
- GoogleUtilities/Environment (8.1.0):
- GoogleUtilities/Privacy
- GoogleUtilities/Logger (8.1.0):
- GoogleUtilities/Environment
- GoogleUtilities/Privacy
- GoogleUtilities/Privacy (8.1.0)
- GoogleUtilities/UserDefaults (8.1.0):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- GTMAppAuth (4.1.1):
- AppAuth/Core (~> 1.7)
- GTMSessionFetcher/Core (< 4.0, >= 3.3)
- GTMSessionFetcher (3.5.0):
- GTMSessionFetcher/Full (= 3.5.0)
- GTMSessionFetcher/Core (3.5.0)
- GTMSessionFetcher/Full (3.5.0):
- GTMSessionFetcher/Core
- image_picker_ios (0.0.1):
- Flutter
- in_app_review (2.0.0):
@@ -88,6 +126,7 @@ PODS:
- payjoin_flutter (0.23.0)
- permission_handler_apple (9.4.8):
- Flutter
- PromisesObjC (2.4.0)
- quick_actions_ios (0.0.1):
- Flutter
- reown_yttrium (0.0.1):
@@ -174,6 +213,7 @@ DEPENDENCIES:
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
- google_sign_in_ios (from `.symlinks/plugins/google_sign_in_ios/darwin`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- in_app_review (from `.symlinks/plugins/in_app_review/ios`)
- integration_test (from `.symlinks/plugins/integration_test/ios`)
@@ -200,10 +240,17 @@ DEPENDENCIES:
SPEC REPOS:
https://github.com/CocoaPods/Specs.git:
- AppAuth
- AppCheckCore
- CryptoSwift
- DKImagePickerController
- DKPhotoGallery
- GoogleSignIn
- GoogleUtilities
- GTMAppAuth
- GTMSessionFetcher
- OrderedSet
- PromisesObjC
- SDWebImage
- sqlite3
- SwiftyGif
@@ -248,6 +295,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/flutter_secure_storage/ios"
fluttertoast:
:path: ".symlinks/plugins/fluttertoast/ios"
google_sign_in_ios:
:path: ".symlinks/plugins/google_sign_in_ios/darwin"
image_picker_ios:
:path: ".symlinks/plugins/image_picker_ios/ios"
in_app_review:
@@ -296,6 +345,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/warp_api/ios"
SPEC CHECKSUMS:
AppAuth: d4f13a8fe0baf391b2108511793e4b479691fb73
AppCheckCore: cc8fd0a3a230ddd401f326489c99990b013f0c4f
bitbox_flutter: 9505732798041c413152669751beeaecc5fe400f
breez_sdk_spark_flutter: ed3b6709b8ce9b40309d8728640eade5b19e8a0d
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
@@ -308,24 +359,30 @@ SPEC CHECKSUMS:
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
dnssec_proof: d461cac7bd3301eb7447f87936745a0c1ae0a67e
fast_scanner: 44c00940355a51258cd6c2085734193cd23d95bc
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4
flutter_local_authentication: 1172a4dd88f6306dadce067454e2c4caf07977bb
flutter_local_notifications: ff50f8405aaa0ccdc7dcfb9022ca192e8ad9688f
flutter_mailer: 2ef5a67087bc8c6c4cefd04a178bf1ae2c94cd83
flutter_native_splash: df59bb2e1421aa0282cb2e95618af4dcb0c56c29
flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be
fluttertoast: 21eecd6935e7064cc1fcb733a4c5a428f3f24f0f
image_picker_ios: 4f2f91b01abdb52842a8e277617df877e40f905b
in_app_review: 436034b18594851a7328d7f1c2ed5ec235b79cfc
integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573
local_auth_darwin: 63c73d6d28cc3e239be2b6aa460ea6e317cd5100
fast_scanner: 2cb1ad3e69e645e9980fb4961396ce5804caa3e3
file_picker: 9b3292d7c8bc68c8a7bf8eb78f730e49c8efc517
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
flutter_local_authentication: 989278c681612f1ee0e36019e149137f114b9d7f
flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb
flutter_mailer: 3a8cd4f36c960fb04528d5471097270c19fec1c4
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
flutter_secure_storage: 2c2ff13db9e0a5647389bff88b0ecac56e3f3418
fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1
google_sign_in_ios: b48bb9af78576358a168361173155596c845f0b9
GoogleSignIn: ce8c89bb9b37fb624b92e7514cc67335d1e277e4
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
GTMAppAuth: f69bd07d68cd3b766125f7e072c45d7340dea0de
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
in_app_review: 7dd1ea365263f834b8464673f9df72c80c17c937
integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
local_auth_darwin: d2e8c53ef0c4f43c646462e3415432c4dab3ae19
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
payjoin_flutter: d9d4c8aa16bd5dfedb9b21d0edc8199e0187d96e
permission_handler_apple: 92d754bbaa7361d436db2d6c3c1c2a0fdcec462e
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
quick_actions_ios: 4b07fb49d8d8f3518d7565fbb7a91014067a7d82
reown_yttrium: cee334ade64725b1d83f7b34c706a6aae2696d58
rive_common: dd421daaf9ae69f0125aa761dd96abd278399952
@@ -344,7 +401,6 @@ SPEC CHECKSUMS:
universal_ble: 45519b2aeafe62761e2c6309f8927edb5288b914
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
warp_api: 997d8fb3e777669bc6260d0c12e56cd40b9db1d1
YttriumWrapper: 31e937fe9fbe0f1314d2ca6be9ce9b379a059966
PODFILE CHECKSUM: 5296465b1c6d14d506230356756826012f65d97a
-4
View File
@@ -26,7 +26,6 @@
A1B4A70C9CFA13AB71662216 /* LnurlPay.swift in Resources */ = {isa = PBXBuildFile; fileRef = 7D3364C03978A8A74B6D586E /* LnurlPay.swift */; settings = {ASSET_TAGS = (BreezSDK, ); }; };
A3D5E17CC53DF13FA740DEFA /* RedeemSwap.swift in Resources */ = {isa = PBXBuildFile; fileRef = 9D2F2C9F2555316C95EE7EA3 /* RedeemSwap.swift */; settings = {ASSET_TAGS = (BreezSDK, ); }; };
B6C6E59403ACDE44724C12F4 /* ServiceConfig.swift in Resources */ = {isa = PBXBuildFile; fileRef = B3D5E78267F5F18D882FDC3B /* ServiceConfig.swift */; settings = {ASSET_TAGS = (BreezSDK, ); }; };
CE3D5B792F142305004D767D /* WarpApiFFI.xcframework in CopyFiles */ = {isa = PBXBuildFile; fileRef = CE3D5B782F142305004D767D /* WarpApiFFI.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
CE918BF82D533ECE007F186E /* MoneroWallet.xcframework in CopyFiles */ = {isa = PBXBuildFile; fileRef = CE918BF72D533ECE007F186E /* MoneroWallet.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
CE918BFA2D533ED4007F186E /* WowneroWallet.xcframework in CopyFiles */ = {isa = PBXBuildFile; fileRef = CE918BF92D533ED4007F186E /* WowneroWallet.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
CE918BFC2D533ED8007F186E /* ZanoWallet.xcframework in CopyFiles */ = {isa = PBXBuildFile; fileRef = CE918BFB2D533ED8007F186E /* ZanoWallet.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
@@ -48,7 +47,6 @@
CE918BF82D533ECE007F186E /* MoneroWallet.xcframework in CopyFiles */,
CE918BFA2D533ED4007F186E /* WowneroWallet.xcframework in CopyFiles */,
CE918BFC2D533ED8007F186E /* ZanoWallet.xcframework in CopyFiles */,
CE3D5B792F142305004D767D /* WarpApiFFI.xcframework in CopyFiles */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -89,7 +87,6 @@
B3D5E78267F5F18D882FDC3B /* ServiceConfig.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ServiceConfig.swift; path = "../.symlinks/plugins/breez_sdk/ios/bindings-swift/Sources/BreezSDK/ServiceConfig.swift"; sourceTree = "<group>"; };
B478EFCC3C0760ED97FFE561 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
C58D93382C00FAC6004BCF69 /* libresolv.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libresolv.tbd; path = usr/lib/libresolv.tbd; sourceTree = SDKROOT; };
CE3D5B782F142305004D767D /* WarpApiFFI.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = WarpApiFFI.xcframework; path = ../scripts/zcash_lib/WarpApiFFI.xcframework; sourceTree = "<group>"; };
CE918BF72D533ECE007F186E /* MoneroWallet.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = MoneroWallet.xcframework; sourceTree = "<group>"; };
CE918BF92D533ED4007F186E /* WowneroWallet.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = WowneroWallet.xcframework; sourceTree = "<group>"; };
CE918BFB2D533ED8007F186E /* ZanoWallet.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = ZanoWallet.xcframework; sourceTree = "<group>"; };
@@ -170,7 +167,6 @@
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
CE3D5B782F142305004D767D /* WarpApiFFI.xcframework */,
0C44A7182518EF4A00B570ED /* CakeWallet */,
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
@@ -15,14 +15,14 @@ class RoundedOverlayCards extends StatelessWidget {
return ClipRRect(
borderRadius:
BorderRadius.only(bottomLeft: Radius.circular(25.0), bottomRight: Radius.circular(25.0)),
BorderRadius.only(bottomLeft: Radius.circular(25), bottomRight: Radius.circular(25)),
child: Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(24),
bottomRight: Radius.circular(24),
),
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(24),
bottomRight: Radius.circular(24),
),
color: Theme.of(context).colorScheme.surfaceContainer,
),
child: Column(
mainAxisSize: MainAxisSize.min,
@@ -30,8 +30,8 @@ class RoundedOverlayCards extends StatelessWidget {
Flexible(
child: ClipRRect(
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(25.0),
bottomRight: Radius.circular(25.0),
bottomLeft: Radius.circular(25),
bottomRight: Radius.circular(25),
),
child: Container(
decoration: BoxDecoration(
+5 -1
View File
@@ -210,7 +210,11 @@ class BackgroundSync {
}
await sharedPreferences.setString(PreferencesKey.backgroundSyncLastTrigger(wallet.name),
tx.date.add(Duration(minutes: 1)).toIso8601String());
final action = tx.direction == TransactionDirection.incoming ? "Received" : "Sent";
final action = tx.additionalInfo['isAutoShield'] == true
? "Autoshield"
: tx.direction == TransactionDirection.incoming
? "Received"
: "Sent";
if (sharedPreferences.getBool(PreferencesKey.backgroundSyncNotificationsEnabled) ?? false) {
await showNotification(
"$action ${wallet.currency.fullName} in ${wallet.name}", "${tx.amount.toString()}");
+21 -15
View File
@@ -1,6 +1,6 @@
import 'dart:convert';
import "dart:convert";
import 'package:cw_core/utils/proxy_wrapper.dart';
import "package:cw_core/utils/proxy_wrapper.dart";
Future<String?> getBolt11FromLightingAddress(String lightningAddress, {int amount = 0}) async {
try {
@@ -31,28 +31,34 @@ Uri getURlOfLightningAddress(String lightningAddress) {
}
class _LNURLPResponseDTO {
const _LNURLPResponseDTO(
this.callback,
this.maxSendable,
this.minSendable,
this.tag,
this.metadata,
);
factory _LNURLPResponseDTO.fromJson(Map<String, dynamic> map) => _LNURLPResponseDTO(
map["callback"] as String,
map["maxSendable"] as int,
map["minSendable"] as int,
map["tag"] as String,
map["metadata"] as String,
);
final String callback;
final int maxSendable;
final int minSendable;
final String tag;
final String metadata;
const _LNURLPResponseDTO(
this.callback, this.maxSendable, this.minSendable, this.tag, this.metadata);
static _LNURLPResponseDTO fromJson(Map<String, dynamic> map) => _LNURLPResponseDTO(
map["callback"] as String,
map["maxSendable"] as int,
map["minSendable"] as int,
map["tag"] as String,
map["metadata"] as String);
}
class _LNURLPCallbackResponseDTO {
final String pr;
const _LNURLPCallbackResponseDTO(this.pr);
static _LNURLPCallbackResponseDTO fromJson(Map<String, dynamic> map) =>
factory _LNURLPCallbackResponseDTO.fromJson(Map<String, dynamic> map) =>
_LNURLPCallbackResponseDTO(map["pr"] as String);
final String pr;
}
+1 -1
View File
@@ -56,7 +56,7 @@ const dogecoinDefaultNodeUri = 'dogecoin.stackwallet.com:50022';
const baseDefaultNodeUri = 'base-rpc.publicnode.com';
const arbitrumDefaultNodeUri = 'arbitrum.nownodes.io';
const bscDefaultNodeUri = 'bsc-dataseed.bnbchain.org';
const zcashDefaultNodeUri = 'zec-node.cakewallet.com:443';
const zcashDefaultNodeUri = 'zec.rocks:443';
Future<void> defaultSettingsMigration(
{required int version,
+2
View File
@@ -236,12 +236,14 @@ class CWMonero extends Monero {
required String password,
required int height,
required HardwareWalletService hardwareWalletService,
required String? passphrase,
}) =>
MoneroRestoreWalletFromHardwareCredentials(
name: name,
password: password,
height: height,
hardwareWalletService: hardwareWalletService,
passphrase: passphrase,
);
@override
+121 -115
View File
@@ -1,54 +1,54 @@
import 'package:cake_wallet/core/utilities.dart';
import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/widgets/modern_button.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/payjoin_copy_modal.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_address_type.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_address_widget.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_amount_display.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_amount_modal.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_bottom_buttons.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_info_box.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_label_modal.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_label_widget.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_large_amount_preview.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_qr_code.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_token_display.dart';
import 'package:cake_wallet/utils/share_util.dart';
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/receive_option_view_model.dart';
import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart';
import 'package:cake_wallet/zcash/zcash.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/payment_uris.dart';
import 'package:cw_core/receive_page_option.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:mobx/mobx.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/anonpay/anonpay_donation_link_info.dart';
import 'package:cake_wallet/entities/preferences_key.dart';
import 'package:cake_wallet/src/screens/receive/anonpay_receive_page.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cake_wallet/routes.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
import "package:cake_wallet/anonpay/anonpay_donation_link_info.dart";
import "package:cake_wallet/bitcoin/bitcoin.dart";
import "package:cake_wallet/core/utilities.dart";
import "package:cake_wallet/di.dart";
import "package:cake_wallet/entities/auto_generate_subaddress_status.dart";
import "package:cake_wallet/entities/preferences_key.dart";
import "package:cake_wallet/generated/i18n.dart";
import "package:cake_wallet/new-ui/widgets/modern_button.dart";
import "package:cake_wallet/new-ui/widgets/receive_page/payjoin_copy_modal.dart";
import "package:cake_wallet/new-ui/widgets/receive_page/receive_address_type.dart";
import "package:cake_wallet/new-ui/widgets/receive_page/receive_address_widget.dart";
import "package:cake_wallet/new-ui/widgets/receive_page/receive_amount_display.dart";
import "package:cake_wallet/new-ui/widgets/receive_page/receive_amount_modal.dart";
import "package:cake_wallet/new-ui/widgets/receive_page/receive_bottom_buttons.dart";
import "package:cake_wallet/new-ui/widgets/receive_page/receive_info_box.dart";
import "package:cake_wallet/new-ui/widgets/receive_page/receive_label_modal.dart";
import "package:cake_wallet/new-ui/widgets/receive_page/receive_label_widget.dart";
import "package:cake_wallet/new-ui/widgets/receive_page/receive_large_amount_preview.dart";
import "package:cake_wallet/new-ui/widgets/receive_page/receive_qr_code.dart";
import "package:cake_wallet/new-ui/widgets/receive_page/receive_token_display.dart";
import "package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
import "package:cake_wallet/routes.dart";
import "package:cake_wallet/src/screens/receive/anonpay_receive_page.dart";
import "package:cake_wallet/utils/share_util.dart";
import "package:cake_wallet/view_model/dashboard/dashboard_view_model.dart";
import "package:cake_wallet/view_model/dashboard/receive_option_view_model.dart";
import "package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart";
import "package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart";
import "package:cake_wallet/zcash/zcash.dart";
import "package:cw_core/crypto_currency.dart";
import "package:cw_core/payment_uris.dart";
import "package:cw_core/receive_page_option.dart";
import "package:cw_core/utils/print_verbose.dart";
import "package:cw_core/wallet_type.dart";
import "package:flutter/cupertino.dart";
import "package:flutter/material.dart";
import "package:flutter/services.dart";
import "package:flutter_mobx/flutter_mobx.dart";
import "package:mobx/mobx.dart";
import "package:modal_bottom_sheet/modal_bottom_sheet.dart";
import "package:shared_preferences/shared_preferences.dart";
class NewReceivePage extends StatefulWidget {
NewReceivePage(
{super.key,
required this.addressListViewModel,
required this.receiveOptionViewModel,
required this.dashboardViewModel,
required this.lightningMode,
CryptoCurrency? initialCurrency}) {
NewReceivePage({
required this.addressListViewModel,
required this.receiveOptionViewModel,
required this.dashboardViewModel,
required this.lightningMode,
super.key,
CryptoCurrency? initialCurrency,
}) {
if (initialCurrency != null && initialCurrency != addressListViewModel.selectedCurrency) {
addressListViewModel.setTokenCurrency(initialCurrency);
}
@@ -78,31 +78,33 @@ class _NewReceivePageState extends State<NewReceivePage> {
.firstWhereOrNull((item) => item.value.contains("Lightning")) ??
ReceivePageOption.mainnet,
);
widget.addressListViewModel.setTokenCurrency(CryptoCurrency.btcln);
widget.addressListViewModel.selectedCurrency = CryptoCurrency.btcln;
} else if (widget.addressListViewModel.wallet.type == WalletType.bitcoin) {
widget.receiveOptionViewModel.selectReceiveOption(
widget.receiveOptionViewModel.options
.firstWhereOrNull((item) => item.value.contains("Standard")) ??
ReceivePageOption.mainnet,
);
widget.addressListViewModel.selectedCurrency = CryptoCurrency.btc;
}
});
reaction((_) => widget.addressListViewModel.uri, (newAddress) {
_reloadAddressWithLabel(newAddress);
});
reaction((_) => widget.addressListViewModel.uri, _reloadAddressWithLabel);
_addressItemWithLabel =
widget.addressListViewModel.forceRecomputeItems.firstWhereOrNull((item) {
return (item is WalletAddressListItem &&
item.address == widget.addressListViewModel.uri.address);
}) as WalletAddressListItem?;
_addressItemWithLabel = widget.addressListViewModel.forceRecomputeItems.firstWhereOrNull(
(item) =>
item is WalletAddressListItem && item.address == widget.addressListViewModel.uri.address,
) as WalletAddressListItem?;
reaction((_) => widget.receiveOptionViewModel.selectedReceiveOption,
(ReceivePageOption option) {
reaction((_) => widget.receiveOptionViewModel.selectedReceiveOption, (option) {
if (widget.dashboardViewModel.type == WalletType.bitcoin &&
bitcoin!.isBitcoinReceivePageOption(option)) {
widget.addressListViewModel.setAddressType(bitcoin!.getOptionToType(option));
if (option.value.contains("Lightning")) {
widget.addressListViewModel.selectedCurrency = CryptoCurrency.btcln;
} else {
widget.addressListViewModel.selectedCurrency = CryptoCurrency.btc;
}
return;
}
if (widget.dashboardViewModel.type == WalletType.zcash) {
@@ -166,16 +168,18 @@ class _NewReceivePageState extends State<NewReceivePage> {
final hasAddressTypeSelector = widget.receiveOptionViewModel.options.length > 1;
final hasLabel = _addressItemWithLabel?.name != null && _addressItemWithLabel!.name!.isNotEmpty;
final infoboxDismissed = widget.addressListViewModel.wallet.walletInfo.receiveInfoboxDismissed;
final infobox = ReceiveInfoBox.forWalletType(widget.addressListViewModel.type,
supportedCurrencies: widget.addressListViewModel.tokenCurrencies
.whereType<CryptoCurrency>()
.toList(), onDismissed: () {
widget.addressListViewModel.dismissInfobox();
setState(() {});
},
autoGenerateSubaddressStatus: widget.lightningMode
? AutoGenerateSubaddressStatus.disabled
: widget.dashboardViewModel.settingsStore.autoGenerateSubaddressStatus);
final infobox = ReceiveInfoBox.forWalletType(
widget.addressListViewModel.type,
supportedCurrencies:
widget.addressListViewModel.tokenCurrencies.whereType<CryptoCurrency>().toList(),
onDismissed: () {
widget.addressListViewModel.dismissInfobox();
setState(() {});
},
autoGenerateSubaddressStatus: widget.lightningMode
? AutoGenerateSubaddressStatus.disabled
: widget.dashboardViewModel.settingsStore.autoGenerateSubaddressStatus,
);
return Container(
decoration: BoxDecoration(
@@ -187,9 +191,7 @@ class _NewReceivePageState extends State<NewReceivePage> {
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
borderRadius: BorderRadius.vertical(
top: Radius.circular(24),
),
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
),
child: SafeArea(
child: Column(
@@ -198,13 +200,13 @@ class _NewReceivePageState extends State<NewReceivePage> {
children: [
ModalTopBar(
title: _largeQrMode ? "" : S.of(context).receive,
leadingIcon: Icon(Icons.close),
leadingIcon: const Icon(Icons.close),
onLeadingPressed: () {
Navigator.of(context, rootNavigator: true).pop();
},
trailingWidget: Observer(
builder: (_) => AnimatedSwitcher(
duration: Duration(milliseconds: 300),
duration: const Duration(milliseconds: 300),
child: _largeQrMode ||
widget.addressListViewModel.hasAddressRotation
/* TODO rotating is broken on mweb, disabling for now, fix after mvp*/
@@ -217,10 +219,10 @@ class _NewReceivePageState extends State<NewReceivePage> {
key: ValueKey(_largeQrMode),
size: 36,
icon: _largeQrMode
? Icon(Icons.share)
? const Icon(Icons.share)
: widget.addressListViewModel.isRotatingAddress
? CupertinoActivityIndicator()
: Icon(Icons.refresh),
? const CupertinoActivityIndicator()
: const Icon(Icons.refresh),
onPressed: () {
if (_largeQrMode) {
ShareUtil.share(
@@ -232,8 +234,9 @@ class _NewReceivePageState extends State<NewReceivePage> {
widget.addressListViewModel.rotateAddress();
}
}
})
: SizedBox.shrink(),
},
)
: const SizedBox.shrink(),
),
),
),
@@ -269,11 +272,12 @@ class _NewReceivePageState extends State<NewReceivePage> {
addressListViewModel: widget.addressListViewModel,
),
GestureDetector(
onTap: _showLabelModal,
child: ReceiveLabelWidget(
name: _addressItemWithLabel?.name ?? "",
largeQrMode: _largeQrMode,
)),
onTap: _showLabelModal,
child: ReceiveLabelWidget(
name: _addressItemWithLabel?.name ?? "",
largeQrMode: _largeQrMode,
),
),
Observer(
builder: (_) => ReceiveBottomButtons(
key: const ValueKey(0),
@@ -285,14 +289,16 @@ class _NewReceivePageState extends State<NewReceivePage> {
: ClipboardData(
text: widget.addressListViewModel.displayAmount.isEmpty
? widget.addressListViewModel.uri.address
: widget.addressListViewModel.uri.toString()),
: widget.addressListViewModel.uri.toString(),
),
onCopyButtonPressed: () {
if (widget.addressListViewModel.hasPayjoin) {
showModalBottomSheet(
isScrollControlled: true,
context: context,
builder: (context) =>
PayjoinCopyModal(uri: widget.addressListViewModel.uri));
isScrollControlled: true,
context: context,
builder: (context) =>
PayjoinCopyModal(uri: widget.addressListViewModel.uri),
);
}
},
onAmountButtonPressed: () {
@@ -300,12 +306,10 @@ class _NewReceivePageState extends State<NewReceivePage> {
context: context,
backgroundColor: Colors.transparent,
barrierColor: Colors.black.withAlpha(80),
builder: (context) {
return ReceiveAmountModal(
walletAddressListViewModel: widget.addressListViewModel,
onSubmitted: (amount) {},
);
},
builder: (context) => ReceiveAmountModal(
walletAddressListViewModel: widget.addressListViewModel,
onSubmitted: (amount) {},
),
);
},
onLabelButtonPressed: _showLabelModal,
@@ -318,22 +322,25 @@ class _NewReceivePageState extends State<NewReceivePage> {
),
),
ReceiveLargeAmountPreview(
amount: widget.addressListViewModel.displayAmount,
currency: widget.addressListViewModel.cryptoCurrencySymbol,
largeQrMode: _largeQrMode),
amount: widget.addressListViewModel.displayAmount,
currency: widget.addressListViewModel.cryptoCurrencySymbol,
largeQrMode: _largeQrMode,
),
if (infobox != null && !widget.addressListViewModel.isLightning)
ClipRect(
child: AnimatedAlign(
duration: const Duration(milliseconds: 200),
curve: Curves.easeOutCubic,
heightFactor: infoboxDismissed ? 0 : 1,
alignment: Alignment.center,
child: AnimatedOpacity(
child: AnimatedAlign(
duration: const Duration(milliseconds: 200),
curve: Curves.easeOutCubic,
heightFactor: infoboxDismissed ? 0 : 1,
alignment: Alignment.center,
child: AnimatedOpacity(
duration: const Duration(milliseconds: 200),
opacity: infoboxDismissed ? 0 : 1,
curve: Curves.easeOutCubic,
child: infobox),
))
child: infobox,
),
),
),
],
),
),
@@ -345,12 +352,11 @@ class _NewReceivePageState extends State<NewReceivePage> {
void _showLabelModal() {
showMaterialModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
barrierColor: Colors.black.withAlpha(80),
builder: (context) {
return getIt.get<ReceiveLabelModal>(param1: _addressItemWithLabel);
}).then((value) {
context: context,
backgroundColor: Colors.transparent,
barrierColor: Colors.black.withAlpha(80),
builder: (_) => getIt.get<ReceiveLabelModal>(param1: _addressItemWithLabel),
).then((value) {
_reloadAddressWithLabel(widget.addressListViewModel.uri);
});
}
@@ -359,8 +365,8 @@ class _NewReceivePageState extends State<NewReceivePage> {
// FIXME: viewmodel doesn't want to load address name here, so we make it. investigate why later
setState(() {
_addressItemWithLabel = widget.addressListViewModel.forceRecomputeItems.firstWhereOrNull(
(item) => (item is WalletAddressListItem && item.address == newAddress.address))
as WalletAddressListItem?;
(item) => item is WalletAddressListItem && item.address == newAddress.address,
) as WalletAddressListItem?;
});
}
}
+2 -2
View File
@@ -594,7 +594,7 @@ class _NewSendPageState extends State<NewSendPage> {
builder: (context) => Material(
child: L2ActionWalletSelector(
showOtherWallets: false,
action: l2actions.deposit,
action: L2Actions.deposit,
sendViewModel: widget.sendViewModel,
contactListViewModel:
widget.contactListViewModel,
@@ -609,7 +609,7 @@ class _NewSendPageState extends State<NewSendPage> {
builder: (context) => Material(
child: L2ActionWalletSelector(
showOtherWallets: false,
action: l2actions.withdraw,
action: L2Actions.withdraw,
sendViewModel: widget.sendViewModel,
contactListViewModel:
widget.contactListViewModel,
@@ -59,14 +59,13 @@ class _AssetsHistorySectionState extends State<AssetsHistorySection> {
AssetsSection(
dashboardViewModel: widget.dashboardViewModel,
),
hasAssetsButton ?
AssetsHistorySectionActionButton(S.current.tokens, "assets/new-ui/options_slider.svg",
() {
Navigator.of(context).pushNamed(
hasAssetsButton ? AssetsHistorySectionActionButton(S.current.tokens, "assets/new-ui/options_slider.svg",
() {Navigator.of(context).pushNamed(
Routes.homeSettings,
arguments: widget.dashboardViewModel.balanceViewModel,
);
}) : null),
}): null),
AssetsHistorySectionTab(
S.current.history,
HistorySection(
@@ -1,24 +1,28 @@
import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item.dart';
import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item_regular_row.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/token_image_widget.dart';
import 'package:cake_wallet/new-ui/widgets/copy_wrapper.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/transaction_details/confirmations_list_item.dart';
import 'package:cake_wallet/src/screens/transaction_details/transaction_details_list_item.dart';
import 'package:cake_wallet/src/screens/transaction_details/address_list_item.dart';
import 'package:cake_wallet/src/widgets/new_list_row/new_list_section.dart';
import 'package:cake_wallet/utils/address_formatter.dart';
import 'package:cake_wallet/view_model/transaction_details_view_model.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
import "package:cake_wallet/entities/new_ui_entities/list_item/list_item.dart";
import "package:cake_wallet/entities/new_ui_entities/list_item/list_item_regular_row.dart";
import "package:cake_wallet/generated/i18n.dart";
import "package:cake_wallet/new-ui/widgets/coins_page/token_image_widget.dart";
import "package:cake_wallet/new-ui/widgets/copy_wrapper.dart";
import "package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
import "package:cake_wallet/routes.dart";
import "package:cake_wallet/src/screens/transaction_details/address_list_item.dart";
import "package:cake_wallet/src/screens/transaction_details/confirmations_list_item.dart";
import "package:cake_wallet/src/screens/transaction_details/transaction_details_list_item.dart";
import "package:cake_wallet/src/widgets/new_list_row/new_list_section.dart";
import "package:cake_wallet/utils/address_formatter.dart";
import "package:cake_wallet/view_model/transaction_details_view_model.dart";
import "package:flutter/material.dart";
import "package:flutter/services.dart";
import "package:flutter_mobx/flutter_mobx.dart";
import "package:modal_bottom_sheet/modal_bottom_sheet.dart";
class TransactionDetailsModal extends StatefulWidget {
const TransactionDetailsModal(
{super.key, required this.transactionDetailsViewModel, this.highlightNoteField = false});
{
required this.transactionDetailsViewModel,
this.highlightNoteField = false,
super.key,
});
final TransactionDetailsViewModel transactionDetailsViewModel;
final bool highlightNoteField;
@@ -48,190 +52,229 @@ class _TransactionDetailsModalState extends State<TransactionDetailsModal> {
}
@override
Widget build(BuildContext context) {
final transactionInfoAmount = widget.transactionDetailsViewModel.transactionInfo.amount;
return SafeArea(
bottom: false,
child: Padding(
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
child: GestureDetector(
onTap: FocusScope.of(context).unfocus,
child: Container(
decoration: BoxDecoration(
Widget build(BuildContext context) => SafeArea(
bottom: false,
child: Padding(
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
child: GestureDetector(
onTap: FocusScope.of(context).unfocus,
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.vertical(top: Radius.circular(25))),
child: Column(
children: [
ModalTopBar(
title: S.of(context).transaction,
leadingIcon: Icon(Icons.close),
onLeadingPressed: Navigator.of(context).pop,
),
Expanded(
child: SingleChildScrollView(
controller: ModalScrollController.of(context),
child: Column(
children: [
TokenImageWidget(
imageUrl:
widget.transactionDetailsViewModel.transactionAsset.iconPath ?? "",
size: 64,
),
SizedBox(height: 10),
Text(
widget.transactionDetailsViewModel.formattedTitle +
widget.transactionDetailsViewModel.formattedStatus,
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w500),
),
CopyWrapper(
requireLongPress: true,
data: ClipboardData(text: transactionInfoAmount.toString()),
builder: (context, copied) => AnimatedSwitcher(
duration: Duration(milliseconds: 300),
child: Text(
key: ValueKey(copied),
copied
? S.of(context).copied
: transactionInfoAmount.toStringWithSymbol(),
style: TextStyle(
fontSize: 28,
color: copied
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurface),
borderRadius: const BorderRadius.vertical(top: Radius.circular(25)),
),
child: Column(
children: [
ModalTopBar(
title: S.of(context).transaction,
leadingIcon: const Icon(Icons.close),
onLeadingPressed: Navigator.of(context).pop,
),
Expanded(
child: SingleChildScrollView(
controller: ModalScrollController.of(context),
child: Column(
children: [
TokenImageWidget(
imageUrl:
widget.transactionDetailsViewModel.transactionAsset.iconPath ?? "",
size: 64,
),
const SizedBox(height: 10),
Text(
widget.transactionDetailsViewModel.formattedTitle +
widget.transactionDetailsViewModel.formattedStatus,
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w500),
),
Observer(
builder: (_) => CopyWrapper(
requireLongPress: true,
data: ClipboardData(
text: widget.transactionDetailsViewModel.transactionCopyAmount,
),
builder: (context, copied) => AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: Text(
key: ValueKey(copied),
copied
? S.of(context).copied
: widget.transactionDetailsViewModel.transactionAmount,
style: TextStyle(
fontSize: 28,
color: copied
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurface,
),
),
),
),
),
),
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
spacing: 12,
children: [
NewListSections(sections: {
"": widget.transactionDetailsViewModel.items
.map((item) {
if (item.value.isEmpty) return null;
Padding(
padding: const EdgeInsets.all(16),
child: Column(
spacing: 12,
children: [
NewListSections(
sections: {
"": widget.transactionDetailsViewModel.items
.map((item) {
if (item.value.isEmpty) {
return null;
}
final shouldBuildBottomWidget = item.value.length > 25;
final shouldBuildBottomWidget = item.value.length > 25;
return ListItemRegularRow(
copyableText: item.value,
showArrow: false,
keyValue: ((item.key as ValueKey?)?.value as String?) ??
item.title,
label: item.title,
trailingWidget: shouldBuildBottomWidget
? null
: _buildTrailingWIdget(item),
bottomWidget: shouldBuildBottomWidget
? _buildBottomWidget(item)
: null);
})
.whereType<ListItem>()
.toList(),
}),
Container(
decoration: BoxDecoration(
return ListItemRegularRow(
copyableText: item.value,
showArrow: false,
keyValue: ((item.key as ValueKey?)?.value as String?) ??
item.title,
label: item.title,
trailingWidget: shouldBuildBottomWidget
? null
: _buildTrailingWidget(item),
bottomWidget: shouldBuildBottomWidget
? _buildBottomWidget(item)
: null,
);
})
.whereType<ListItem>()
.toList(),
},
),
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: Theme.of(context).colorScheme.surfaceContainer),
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
spacing: 8,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(S.of(context).note),
TextField(
focusNode: noteFocusNode,
controller: noteController,
decoration: InputDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
spacing: 8,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(S.of(context).note),
TextField(
focusNode: noteFocusNode,
controller: noteController,
decoration: InputDecoration(
hintText: S.of(context).add_a_note,
border: InputBorder.none,
focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none,
contentPadding: EdgeInsets.zero,
isDense: true),
)
],
isDense: true,
),
),
],
),
),
),
),
Observer(
builder: (_) => NewListSections(sections: {
"view tx": [
ListItemRegularRow(
keyValue: "view tx on",
label:
widget.transactionDetailsViewModel.explorerDescription,
onTap: widget.transactionDetailsViewModel.launchExplorer,
foregroundColor: Theme.of(context).colorScheme.primary,
trailingIconPath: "assets/new-ui/link_arrow.svg",
trailingIconSize: 8)
],
if (widget.transactionDetailsViewModel.canReplaceByFee)
"rbf": [
ListItemRegularRow(
keyValue: "replace by fee",
label: S.of(context).bump_fee,
onTap: () {
Navigator.of(context)
.pushNamed(Routes.bumpFeePage, arguments: [
widget.transactionDetailsViewModel.transactionInfo,
widget.transactionDetailsViewModel.rawTransaction
]);
})
]
}),
)
],
Observer(
builder: (_) => NewListSections(
sections: {
"view tx": [
ListItemRegularRow(
keyValue: "view tx on",
label: widget
.transactionDetailsViewModel.explorerDescription,
onTap: widget.transactionDetailsViewModel.launchExplorer,
foregroundColor: Theme.of(context).colorScheme.primary,
trailingIconPath: "assets/new-ui/link_arrow.svg",
trailingIconSize: 8,
),
],
if (widget.transactionDetailsViewModel.canReplaceByFee)
"rbf": [
ListItemRegularRow(
keyValue: "replace by fee",
label: S.of(context).bump_fee,
onTap: () {
Navigator.of(context).pushNamed(
Routes.bumpFeePage,
arguments: [
widget
.transactionDetailsViewModel.transactionInfo,
widget.transactionDetailsViewModel.rawTransaction,
],
);
},
),
],
},
),
),
],
),
),
),
SizedBox(height: MediaQuery.of(context).viewPadding.bottom)
],
SizedBox(height: MediaQuery.of(context).viewPadding.bottom),
],
),
),
),
)
],
],
),
),
),
),
),
);
}
);
Widget _buildTrailingWIdget(TransactionDetailsListItem item) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: switch (item.runtimeType) {
ConfirmationsListItem => Row(
children: [
Text((item as ConfirmationsListItem).current.toString(),
style: TextStyle(color: Theme.of(context).colorScheme.primary)),
if (item.needed > 0)
Text("/${item.needed}",
style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant))
],
),
_ => Text(
item.value,
style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant),
)
},
);
}
Widget _buildTrailingWidget(TransactionDetailsListItem item) => Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: switch (item.runtimeType) {
ConfirmationsListItem => Row(
children: [
Text(
(item as ConfirmationsListItem).current.toString(),
style: TextStyle(color: Theme.of(context).colorScheme.primary),
),
if (item.needed > 0)
Text(
"/${item.needed}",
style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant),
),
],
),
_ => Text(
item.value,
style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant),
)
},
);
Widget _buildBottomWidget(TransactionDetailsListItem item) {
return switch (item.runtimeType) {
AddressListItem => AddressFormatter.buildSegmentedAddress(
address: item.value,
evenTextStyle: TextStyle(
fontSize: 12,
fontFamily: "IBM Plex Mono",
color: Theme.of(context).colorScheme.onSurface)),
AddressListItem => _segmentedAddressList(item.value),
_ => Text(
item.value,
style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant),
)
};
}
Widget _segmentedAddressList(String value) {
final style = TextStyle(
fontSize: 12,
fontFamily: "IBM Plex Mono",
color: Theme.of(context).colorScheme.onSurface,
);
final lines = value
.split(RegExp(r'[,\n]'))
.map((final line) => line.trim())
.where((final line) => line.isNotEmpty)
.toList();
if (lines.length <= 1) {
return AddressFormatter.buildSegmentedAddress(
address: lines.isNotEmpty ? lines.first : value.trim(),
evenTextStyle: style,
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
for (final line in lines)
AddressFormatter.buildSegmentedAddress(address: line, evenTextStyle: style),
],
);
}
}
+42 -46
View File
@@ -1,5 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import "dart:io";
import "package:flutter/material.dart";
import "package:flutter/services.dart";
class DigitInputController {
String _text = "";
@@ -19,38 +21,37 @@ class DigitInputController {
}
class DigitInputPill extends StatelessWidget {
const DigitInputPill({super.key, this.digit, required this.highlighted});
const DigitInputPill({required this.highlighted, super.key, this.digit});
final String? digit;
final bool highlighted;
@override
Widget build(BuildContext context) {
return Container(
width: 51,
height: 84,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(16),
border:
highlighted ? Border.all(color: Theme.of(context).colorScheme.primary, width: 2) : null,
),
child: Center(
child: Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: Text(digit ?? " ", style: TextStyle(fontSize: 40)),
Widget build(BuildContext context) => Container(
width: 51,
height: 84,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(16),
border: highlighted
? Border.all(color: Theme.of(context).colorScheme.primary, width: 2)
: null,
),
),
);
}
child: Center(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Text(digit ?? " ", style: const TextStyle(fontSize: 40)),
),
),
);
}
class DigitInput extends StatefulWidget {
const DigitInput({
super.key,
required this.controller,
required this.desiredLength,
this.breakAt = 3,
super.key,
});
final DigitInputController controller;
@@ -74,12 +75,9 @@ class _DigitInputState extends State<DigitInput> implements TextInputClient {
if (_textInputConnection == null || !_textInputConnection!.attached) {
_textInputConnection = TextInput.attach(
this,
const TextInputConfiguration(
inputType: TextInputType.numberWithOptions(
signed: false,
decimal: false,
),
inputAction: TextInputAction.none,
TextInputConfiguration(
inputType: TextInputType.number,
inputAction: Platform.isIOS ? TextInputAction.done : TextInputAction.none,
),
);
}
@@ -102,30 +100,28 @@ class _DigitInputState extends State<DigitInput> implements TextInputClient {
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: _openVirtualKeyboard,
child: SizedBox(
height: 84,
child: ListView.separated(
scrollDirection: Axis.horizontal,
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => DigitInputPill(
digit: widget.controller.text.length > index ? widget.controller.text[index] : null,
highlighted: widget.controller.text.length == index,
Widget build(BuildContext context) => GestureDetector(
onTap: _openVirtualKeyboard,
child: SizedBox(
height: 84,
child: ListView.separated(
scrollDirection: Axis.horizontal,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => DigitInputPill(
digit: widget.controller.text.length > index ? widget.controller.text[index] : null,
highlighted: widget.controller.text.length == index,
),
separatorBuilder: (context, index) =>
SizedBox(width: (index + 1) % widget.breakAt == 0 ? 16 : 4),
itemCount: widget.desiredLength,
),
separatorBuilder: (context, index) =>
SizedBox(width: (index + 1) % widget.breakAt == 0 ? 16 : 4),
itemCount: widget.desiredLength,
),
),
);
}
);
@override
void updateEditingValue(TextEditingValue value) {
final RegExp numberRegExp = RegExp(r'^\d{0,6}$');
final RegExp numberRegExp = RegExp(r"^\d{0,6}$");
if (numberRegExp.hasMatch(value.text)) {
setState(() => widget.controller.text = value.text);
@@ -1,41 +1,41 @@
import 'dart:async';
import "dart:async";
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/main.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/token_image_widget.dart';
import 'package:cake_wallet/new-ui/widgets/new_primary_button.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
import 'package:cake_wallet/new-ui/widgets/send_page/l2_send_external_modal.dart';
import 'package:cake_wallet/new-ui/widgets/send_page/send_address_input.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:cake_wallet/src/widgets/new_list_row/new_simple_checkbox.dart';
import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart';
import 'package:cake_wallet/view_model/send/send_view_model.dart';
import 'package:cake_wallet/view_model/wallet_switcher_view_model.dart';
import 'package:cw_core/currency_for_wallet_type.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
import "package:cake_wallet/bitcoin/bitcoin.dart";
import "package:cake_wallet/generated/i18n.dart";
import "package:cake_wallet/main.dart";
import "package:cake_wallet/new-ui/widgets/coins_page/token_image_widget.dart";
import "package:cake_wallet/new-ui/widgets/new_primary_button.dart";
import "package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
import "package:cake_wallet/new-ui/widgets/send_page/l2_send_external_modal.dart";
import "package:cake_wallet/new-ui/widgets/send_page/send_address_input.dart";
import "package:cake_wallet/src/widgets/cake_image_widget.dart";
import "package:cake_wallet/src/widgets/new_list_row/new_simple_checkbox.dart";
import "package:cake_wallet/view_model/contact_list/contact_list_view_model.dart";
import "package:cake_wallet/view_model/send/send_view_model.dart";
import "package:cake_wallet/view_model/wallet_switcher_view_model.dart";
import "package:cw_core/currency_for_wallet_type.dart";
import "package:cw_core/wallet_info.dart";
import "package:cw_core/wallet_type.dart";
import "package:flutter/cupertino.dart";
import "package:flutter/material.dart";
import "package:modal_bottom_sheet/modal_bottom_sheet.dart";
enum l2actions { deposit, withdraw }
enum L2Actions { deposit, withdraw }
class L2ActionWalletSelector extends StatefulWidget {
const L2ActionWalletSelector({
super.key,
required this.showOtherWallets,
required this.sendViewModel,
required this.action,
required this.onSendInitiated,
required this.contactListViewModel,
required this.walletSwitcherViewModel,
super.key,
});
final bool showOtherWallets;
final SendViewModel sendViewModel;
final l2actions action;
final L2Actions action;
final VoidCallback onSendInitiated;
final ContactListViewModel contactListViewModel;
final WalletSwitcherViewModel walletSwitcherViewModel;
@@ -56,12 +56,18 @@ class _L2ActionWalletSelectorState extends State<L2ActionWalletSelector> {
super.initState();
if (widget.showOtherWallets) {
() async {
items.addAll((await WalletInfo.getAll()).where((item) =>
item.type == widget.sendViewModel.walletType && item.hardwareWalletType == null));
items.addAll(
(await WalletInfo.getAll()).where(
(item) =>
item.type == widget.sendViewModel.walletType && item.hardwareWalletType == null,
),
);
items.sort((a, b) {
if (a.name == widget.sendViewModel.wallet.name)
if (a.name == widget.sendViewModel.wallet.name) {
return -1;
else if (b.name == widget.sendViewModel.wallet.name) return 1;
} else if (b.name == widget.sendViewModel.wallet.name) {
return 1;
}
return 0;
});
setState(() {});
@@ -80,16 +86,16 @@ class _L2ActionWalletSelectorState extends State<L2ActionWalletSelector> {
mainAxisSize: MainAxisSize.min,
children: [
ModalTopBar(
title: widget.action == l2actions.deposit
title: widget.action == L2Actions.deposit
? "${S.of(context).send_from}..."
: "${S.of(context).receive_to}...",
leadingIcon: Icon(Icons.arrow_back_ios_new),
leadingIcon: const Icon(Icons.arrow_back_ios_new),
onLeadingPressed: Navigator.of(context).pop,
),
Flexible(
child: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
spacing: 12,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
@@ -123,29 +129,17 @@ class _L2ActionWalletSelectorState extends State<L2ActionWalletSelector> {
itemBuilder: (context, index) {
final item = items[index];
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
padding: const EdgeInsets.symmetric(vertical: 4),
child: WalletRow(
currencyIconPath: getCryptoCurrencyIconForWalletListItem(item.type),
walletName: item.name,
isCurrent: item.name == widget.sendViewModel.wallet.name,
isSelected: _selectedWalletIndex == index && !textEntered,
onTap: () async {
onTap: () {
setState(() {
addressController.text = "";
_selectedWalletIndex = index;
});
// if (widget.action == l2actions.withdraw) {
// widget.sendViewModel.outputs.first.address = item.address;
// widget.onSendInitiated();
// } else if (widget.action == l2actions.deposit) {
// setState(() {
// loadingWalletName = item.name;
// });
// await _handleChangeWallet(item);
// widget.onSendInitiated();
// setState(() {
// loadingWalletName = null;
// });
},
),
);
@@ -198,7 +192,7 @@ class _L2ActionWalletSelectorState extends State<L2ActionWalletSelector> {
// ),
// ),
// ),
if (widget.action == l2actions.withdraw)
if (widget.action == L2Actions.withdraw)
Row(
children: [
Flexible(
@@ -215,12 +209,12 @@ class _L2ActionWalletSelectorState extends State<L2ActionWalletSelector> {
Column(
spacing: 8,
children: [
if (widget.action == l2actions.deposit && widget.showOtherWallets) ...[
if (widget.action == L2Actions.deposit && widget.showOtherWallets) ...[
Container(
height: 1,
color: Theme.of(context).colorScheme.onSurfaceVariant.withAlpha(64),
),
SizedBox(),
const SizedBox(),
Container(
height: 52,
decoration: BoxDecoration(
@@ -232,7 +226,7 @@ class _L2ActionWalletSelectorState extends State<L2ActionWalletSelector> {
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(16),
onTap: () async {
onTap: () {
Navigator.of(context, rootNavigator: true).pop();
showCupertinoModalBottomSheet(
context: navigatorKey.currentContext ?? context,
@@ -246,7 +240,7 @@ class _L2ActionWalletSelectorState extends State<L2ActionWalletSelector> {
);
},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 10,
@@ -275,19 +269,22 @@ class _L2ActionWalletSelectorState extends State<L2ActionWalletSelector> {
onPressed: () async {
if (widget.sendViewModel.wallet.type == WalletType.bitcoin ||
widget.sendViewModel.wallet.type == WalletType.litecoin) {
if (widget.action == l2actions.withdraw) {
if (widget.action == L2Actions.withdraw) {
widget.sendViewModel.outputs.first.address =
bitcoin!.getUnusedSegwitAddress(widget.sendViewModel.wallet)!;
}
if (widget.showOtherWallets) {
if (widget.action == l2actions.deposit) {
if (widget.action == L2Actions.deposit) {
await _handleChangeWallet(items[_selectedWalletIndex]);
} else {
widget.sendViewModel.outputs.first.address =
addressController.text.isNotEmpty
? addressController.text
: items[_selectedWalletIndex].address;
if (items[_selectedWalletIndex].name !=
widget.sendViewModel.wallet.name) {
widget.sendViewModel.outputs.first.address =
addressController.text.isNotEmpty
? addressController.text
: items[_selectedWalletIndex].address;
}
}
}
widget.onSendInitiated();
@@ -298,7 +295,7 @@ class _L2ActionWalletSelectorState extends State<L2ActionWalletSelector> {
textColor: Theme.of(context).colorScheme.onPrimary,
isLoading: _isLoading,
),
SizedBox()
const SizedBox()
],
),
],
@@ -339,13 +336,13 @@ class _L2ActionWalletSelectorState extends State<L2ActionWalletSelector> {
class WalletRow extends StatelessWidget {
const WalletRow({
super.key,
required this.currencyIconPath,
required this.walletName,
required this.onTap,
this.isLoading = false,
this.isCurrent = false,
this.isSelected,
super.key,
});
final String currencyIconPath;
@@ -370,7 +367,7 @@ class WalletRow extends StatelessWidget {
borderRadius: BorderRadius.circular(16),
onTap: onTap,
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 12),
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@@ -513,7 +513,8 @@ class _TransactionCommitedScreenState extends State<TransactionCommitedScreen> {
showModalBottomSheet(
isScrollControlled: true,
context: context,
builder: (context) => page);
builder: (context) =>
FractionallySizedBox(heightFactor: 0.9, child: page));
}),
],
),
@@ -142,12 +142,22 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
super.dispose();
}
var _isRefreshingUsb = false;
Future<void> _refreshUsbDevices() async {
final dev = await widget.hardwareWalletVM.getAllUsbDevices();
if (usbDevices.length != dev.length) {
setState(() => usbDevices = dev);
if (_isRefreshingUsb) {
return;
}
_isRefreshingUsb = true;
try {
final dev = await widget.hardwareWalletVM.getAllUsbDevices();
if (usbDevices.length != dev.length) {
setState(() => usbDevices = dev);
}
} catch(e) {
printV(e);
}
_isRefreshingUsb = false;
}
Future<void> _refreshBleDevices() async {
@@ -1,19 +1,22 @@
import 'package:cake_wallet/core/wallet_name_validator.dart';
import 'package:cw_core/generate_name.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
import 'package:cake_wallet/src/widgets/blockchain_height_widget.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/widgets/scrollable_with_bottom_section.dart';
import 'package:cake_wallet/utils/responsive_layout_util.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/wallet_hardware_restore_view_model.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:mobx/mobx.dart';
import "dart:async";
import "package:cake_wallet/core/wallet_name_validator.dart";
import "package:cake_wallet/generated/i18n.dart";
import "package:cake_wallet/src/screens/base_page.dart";
import "package:cake_wallet/src/screens/connect_device/monero_hardware_wallet_passphrase_input.dart";
import "package:cake_wallet/src/widgets/alert_with_one_action.dart";
import "package:cake_wallet/src/widgets/base_text_form_field.dart";
import "package:cake_wallet/src/widgets/blockchain_height_widget.dart";
import "package:cake_wallet/src/widgets/primary_button.dart";
import "package:cake_wallet/src/widgets/scrollable_with_bottom_section.dart";
import "package:cake_wallet/utils/responsive_layout_util.dart";
import "package:cake_wallet/utils/show_pop_up.dart";
import "package:cake_wallet/view_model/wallet_hardware_restore_view_model.dart";
import "package:cw_core/generate_name.dart";
import "package:cw_core/wallet_type.dart";
import "package:flutter/material.dart";
import "package:flutter_mobx/flutter_mobx.dart";
import "package:mobx/mobx.dart";
class MoneroHardwareWalletOptionsPage extends BasePage {
MoneroHardwareWalletOptionsPage(this._walletHardwareRestoreVM);
@@ -42,13 +45,15 @@ class _MoneroHardwareWalletOptionsFormState extends State<_MoneroHardwareWalletO
: _formKey = GlobalKey<FormState>(),
_blockchainHeightKey = GlobalKey<BlockchainHeightState>(),
_blockHeightFocusNode = FocusNode(),
_controller = TextEditingController();
_walletNameController = TextEditingController(),
_passphraseController = TextEditingController();
final GlobalKey<FormState> _formKey;
final GlobalKey<BlockchainHeightState> _blockchainHeightKey;
final FocusNode _blockHeightFocusNode;
final WalletHardwareRestoreViewModel _walletHardwareRestoreVM;
final TextEditingController _controller;
final TextEditingController _walletNameController;
final TextEditingController _passphraseController;
@override
void initState() {
@@ -57,131 +62,159 @@ class _MoneroHardwareWalletOptionsFormState extends State<_MoneroHardwareWalletO
}
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.only(top: 24),
child: ScrollableWithBottomSection(
contentPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24),
content: Center(
child: ConstrainedBox(
constraints:
BoxConstraints(maxWidth: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Padding(
padding: EdgeInsets.only(top: 0),
child: Form(
key: _formKey,
child: Stack(
alignment: Alignment.centerRight,
children: [
BaseTextFormField(
onChanged: (value) => _walletHardwareRestoreVM.name = value,
controller: _controller,
textStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(
fontSize: 20.0,
fontWeight: FontWeight.w600,
),
placeholderTextStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(
fontSize: 18.0,
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
hintText: S.of(context).wallet_name,
suffixIcon: Semantics(
label: S.of(context).generate_name,
child: IconButton(
onPressed: _onGenerateName,
icon: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(6.0),
color: Theme.of(context).colorScheme.surfaceContainerHighest,
Widget build(BuildContext context) => Padding(
padding: const EdgeInsets.only(top: 24),
child: ScrollableWithBottomSection(
contentPadding: const EdgeInsets.only(left: 24, right: 24, bottom: 24),
content: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Padding(
padding: EdgeInsets.zero,
child: Form(
key: _formKey,
child: Stack(
alignment: Alignment.centerRight,
children: [
BaseTextFormField(
onChanged: (value) => _walletHardwareRestoreVM.name = value,
controller: _walletNameController,
textStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(
fontSize: 20,
fontWeight: FontWeight.w600,
),
width: 34,
height: 34,
child: Image.asset(
'assets/images/refresh_icon.png',
color: Theme.of(context).colorScheme.primary,
placeholderTextStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(
fontSize: 18,
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
hintText: S.of(context).wallet_name,
suffixIcon: Semantics(
label: S.of(context).generate_name,
child: IconButton(
onPressed: _onGenerateName,
icon: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(6),
color: Theme.of(context).colorScheme.surfaceContainerHighest,
),
width: 34,
height: 34,
child: Image.asset(
"assets/images/refresh_icon.png",
color: Theme.of(context).colorScheme.primary,
),
),
),
),
validator: WalletNameValidator(),
),
validator: WalletNameValidator(),
),
],
],
),
),
),
),
Padding(
padding: EdgeInsets.only(top: 20),
child: BlockchainHeightWidget(
focusNode: _blockHeightFocusNode,
key: _blockchainHeightKey,
hasDatePicker: true,
walletType: WalletType.monero,
Padding(
padding: const EdgeInsets.only(top: 20),
child: BlockchainHeightWidget(
focusNode: _blockHeightFocusNode,
key: _blockchainHeightKey,
hasDatePicker: true,
walletType: WalletType.monero,
),
),
],
),
),
),
bottomSectionPadding: const EdgeInsets.all(24),
bottomSection: Observer(
builder: (_) => Column(
children: [
if (_walletHardwareRestoreVM.passphraseAvailable) ...[
TextButton(
onPressed: () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => MoneroHardwareWalletPassphraseInputModal(
controller: _passphraseController,
),
);
},
child: Text(S.of(context).add_a_passphrase),
),
],
LoadingPrimaryButton(
onPressed: _confirmForm,
text: S.of(context).seed_language_next,
color: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,
isDisabled: _walletHardwareRestoreVM.name.isEmpty,
),
],
),
),
),
bottomSectionPadding: EdgeInsets.all(24),
bottomSection: Observer(
builder: (context) => LoadingPrimaryButton(
onPressed: _confirmForm,
text: S.of(context).seed_language_next,
color: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,
isDisabled: _walletHardwareRestoreVM.name.isEmpty,
),
),
),
);
}
);
Future<void> _onGenerateName() async {
final rName = await generateName();
FocusManager.instance.primaryFocus?.unfocus();
setState(() {
_controller.text = rName;
_walletNameController.text = rName;
_walletHardwareRestoreVM.name = rName;
_controller.selection =
TextSelection.fromPosition(TextPosition(offset: _controller.text.length));
_walletNameController.selection =
TextSelection.fromPosition(TextPosition(offset: _walletNameController.text.length));
});
}
Future<void> _confirmForm() async {
showPopUp<void>(
context: context,
builder: (BuildContext context) => AlertWithOneAction(
alertTitle: S.of(context).proceed_on_device,
alertContent: S.of(context).proceed_on_device_description,
buttonText: S.of(context).cancel,
alertBarrierDismissible: false,
buttonAction: () => Navigator.of(context).pop(),
unawaited(
showPopUp<void>(
context: context,
builder: (context) => AlertWithOneAction(
alertTitle: S.of(context).proceed_on_device,
alertContent: S.of(context).proceed_on_device_description,
buttonText: S.of(context).cancel,
alertBarrierDismissible: false,
buttonAction: () => Navigator.of(context).pop(),
),
),
);
final options = {'height': _blockchainHeightKey.currentState?.height ?? -1};
final options = <String, dynamic>{"height": _blockchainHeightKey.currentState?.height ?? -1};
if (_walletHardwareRestoreVM.passphraseAvailable && _passphraseController.text.isNotEmpty) {
options["passphrase"] = _passphraseController.text;
}
await _walletHardwareRestoreVM.create(options: options);
}
bool _effectsInstalled = false;
void _setEffects(BuildContext context) {
if (_effectsInstalled) return;
if (_effectsInstalled) {
return;
}
reaction((_) => _walletHardwareRestoreVM.error, (String? error) {
reaction((_) => _walletHardwareRestoreVM.error, (error) {
if (error != null) {
if (error == S.current.ledger_connection_error) Navigator.of(context).pop();
if (error == S.current.ledger_connection_error) {
Navigator.of(context).pop();
}
WidgetsBinding.instance.addPostFrameCallback((_) {
showPopUp<void>(
context: context,
builder: (BuildContext context) => AlertWithOneAction(
builder: (context) => AlertWithOneAction(
alertTitle: S.of(context).error,
alertContent: error,
buttonText: S.of(context).ok,
@@ -0,0 +1,54 @@
import "package:cake_wallet/generated/i18n.dart";
import "package:cake_wallet/new-ui/widgets/new_primary_button.dart";
import "package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
import "package:cake_wallet/src/widgets/base_text_form_field.dart";
import "package:flutter/material.dart";
class MoneroHardwareWalletPassphraseInputModal extends StatelessWidget {
const MoneroHardwareWalletPassphraseInputModal({required this.controller, super.key});
final TextEditingController controller;
@override
Widget build(BuildContext context) => Padding(
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
child: Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.vertical(top: Radius.circular(18)),
color: Theme.of(context).colorScheme.surface,
),
child: SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ModalTopBar(
title: S.of(context).passphrase_entry,
leadingIcon: const Icon(Icons.close),
onLeadingPressed: Navigator.of(context).pop,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 18),
child: Column(
spacing: 12,
children: [
BaseTextFormField(
controller: controller,
hintText: S.of(context).passphrase_raw,
),
const SizedBox(),
NewPrimaryButton(
onPressed: Navigator.of(context).pop,
text: S.of(context).restore_next,
color: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,
),
const SizedBox(),
],
),
),
],
),
),
),
);
}
@@ -334,37 +334,6 @@ class CryptoBalanceWidget extends StatelessWidget {
),
),
],
if (dashboardViewModel.showZcashMissingFundsCard) ...[
SizedBox(height: 10),
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
child: InfoCard(
title: S.of(context).zcash_card_missing_funds,
description: S.of(context).zcash_card_description,
leftButtonTitle: S.of(context).zcash_card_dismiss,
rightButtonTitle: S.of(context).zcash_card_scan,
image: 'assets/new-ui/crypto_full_icons/zcash.svg',
leftButtonAction: () => _dismissZcash(context),
rightButtonAction: () => _rescanInternalChangeZcash(context),
hintWidget: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => launchUrl(
Uri.parse(
"https://docs.cakewallet.com/tutorials/zashi/#creating-a-new-zcash-wallet-on-cake"),
mode: LaunchMode.externalApplication,
),
child: Text(
S.of(context).learn_more,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
height: 1,
),
softWrap: true,
),
),
),
),
],
if (dashboardViewModel.showDecredInfoCard) ...[
SizedBox(height: 10),
Padding(
@@ -494,21 +463,6 @@ class CryptoBalanceWidget extends StatelessWidget {
dashboardViewModel.setMwebEnabled();
}
Future<void> _rescanInternalChangeZcash(BuildContext context) async {
dashboardViewModel.rescanInternalChangeZcash();
await showPopUp<void>(
context: context,
builder: (BuildContext context) => AlertWithOneAction(
alertTitle: S.of(context).alert_notice,
alertContent: S.of(context).zcash_card_warning,
buttonText: S.of(context).understand,
buttonAction: () {
Navigator.of(context).pop();
},
),
);
}
Future<void> _dismissMweb(BuildContext context) async {
await showPopUp<void>(
context: context,
@@ -522,18 +476,4 @@ class CryptoBalanceWidget extends StatelessWidget {
));
dashboardViewModel.dismissMweb();
}
Future<void> _dismissZcash(BuildContext context) async {
await showPopUp<void>(
context: context,
builder: (BuildContext context) => AlertWithOneAction(
alertTitle: S.of(context).alert_notice,
alertContent: S.of(context).zcash_card_enable_later,
buttonText: S.of(context).understand,
buttonAction: () {
Navigator.of(context).pop();
},
));
dashboardViewModel.dismissZcash();
}
}
@@ -111,7 +111,7 @@ class TransactionsPage extends StatelessWidget {
key: item.key,
onTap: () => Navigator.of(context)
.pushNamed(Routes.transactionDetails, arguments: transaction),
isShield: transaction.additionalInfo['autoShield'] == true,
isShield: transaction.additionalInfo['isAutoShield'] == true,
direction: transaction.direction,
formattedDate: DateFormat('HH:mm').format(transaction.date),
formattedAmount: item.formattedCryptoAmount,
@@ -99,7 +99,7 @@ class WalletRestorePage extends BasePage {
children: [
Observer(
builder: (context) {
return walletRestoreViewModel.mode == WalletRestoreMode.seed
return walletRestoreViewModel.passphraseAvailable
? StandardCheckbox(
captionColor: Theme.of(context).colorScheme.onSecondaryContainer,
value: walletRestoreViewModel.hasPassphrase,
@@ -115,21 +115,12 @@ class DisplaySettingsPage extends StatelessWidget {
onChanged: (val) {
_displaySettingsViewModel.setDisableTradeOption(val);
}),
if (_displaySettingsViewModel.showZcashCardSetting)
ListItemToggle(
keyValue: "display_settings_show_zcashcard",
label: S.of(context).show_zcash_card,
value: _displaySettingsViewModel.showZcashCard,
onChanged: (val) {
_displaySettingsViewModel.setShowZcashCard(val);
}),
ListItemSelector(
keyValue: "display_settings_sync_status_display",
label: S.of(context).sync_status_display_mode,
options: [_displaySettingsViewModel.syncStatusDisplayMode.title],
onTap: () async {
final items = SyncStatusDisplayMode.values.toList();
final selectedAtIndex =
items.indexOf(_displaySettingsViewModel.syncStatusDisplayMode);
@@ -1,34 +1,36 @@
import 'package:cake_wallet/core/execution_state.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/transaction_details/rbf_details_list_fee_picker_item.dart';
import 'package:cake_wallet/src/screens/transaction_details/standart_list_item.dart';
import 'package:cake_wallet/src/screens/transaction_details/textfield_list_item.dart';
import 'package:cake_wallet/src/screens/transaction_details/transaction_expandable_list_item.dart';
import 'package:cake_wallet/src/screens/transaction_details/widgets/textfield_list_row.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
import 'package:cake_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart';
import 'package:cake_wallet/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart';
import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart';
import 'package:cake_wallet/src/widgets/list_row.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/widgets/standard_expandable_list.dart';
import 'package:cake_wallet/src/widgets/standard_list.dart';
import 'package:cake_wallet/src/widgets/standard_picker_list.dart';
import 'package:cake_wallet/utils/show_bar.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/send/send_view_model_state.dart';
import 'package:cake_wallet/view_model/transaction_details_view_model.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:mobx/mobx.dart';
import "package:cake_wallet/core/execution_state.dart";
import "package:cake_wallet/generated/i18n.dart";
import "package:cake_wallet/src/screens/base_page.dart";
import "package:cake_wallet/src/screens/transaction_details/rbf_details_list_fee_picker_item.dart";
import "package:cake_wallet/src/screens/transaction_details/standart_list_item.dart";
import "package:cake_wallet/src/screens/transaction_details/textfield_list_item.dart";
import "package:cake_wallet/src/screens/transaction_details/transaction_expandable_list_item.dart";
import "package:cake_wallet/src/screens/transaction_details/widgets/textfield_list_row.dart";
import "package:cake_wallet/src/widgets/alert_with_one_action.dart";
import "package:cake_wallet/src/widgets/alert_with_two_actions.dart";
import "package:cake_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart";
import "package:cake_wallet/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart";
import "package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart";
import "package:cake_wallet/src/widgets/list_row.dart";
import "package:cake_wallet/src/widgets/primary_button.dart";
import "package:cake_wallet/src/widgets/standard_expandable_list.dart";
import "package:cake_wallet/src/widgets/standard_list.dart";
import "package:cake_wallet/src/widgets/standard_picker_list.dart";
import "package:cake_wallet/utils/show_bar.dart";
import "package:cake_wallet/utils/show_pop_up.dart";
import "package:cake_wallet/view_model/send/send_view_model_state.dart";
import "package:cake_wallet/view_model/transaction_details_view_model.dart";
import "package:flutter/material.dart";
import "package:flutter/services.dart";
import "package:flutter_mobx/flutter_mobx.dart";
import "package:mobx/mobx.dart";
class RBFDetailsPage extends BasePage {
RBFDetailsPage({required this.transactionDetailsViewModel, required this.rawTransaction}) {
transactionDetailsViewModel.addBumpFeesListItems(
transactionDetailsViewModel.transactionInfo, rawTransaction);
transactionDetailsViewModel.transactionInfo,
rawTransaction,
);
}
@override
@@ -46,70 +48,70 @@ class RBFDetailsPage extends BasePage {
children: [
Expanded(
child: SectionStandardList(
sectionCount: 1,
itemCounter: (int _) => transactionDetailsViewModel.RBFListItems.length,
itemBuilder: (__, index) {
final item = transactionDetailsViewModel.RBFListItems[index];
sectionCount: 1,
itemCounter: (_) => transactionDetailsViewModel.rbfListItems.length,
itemBuilder: (__, index) {
final item = transactionDetailsViewModel.rbfListItems[index];
if (item is StandartListItem) {
return GestureDetector(
onTap: () {
Clipboard.setData(ClipboardData(text: item.value));
showBar<void>(context, S.of(context).transaction_details_copied(item.title));
},
child: ListRow(title: '${item.title}:', value: item.value),
);
}
if (item is StandartListItem) {
return GestureDetector(
onTap: () {
Clipboard.setData(ClipboardData(text: item.value));
showBar<void>(context, S.of(context).transaction_details_copied(item.title));
},
child: ListRow(title: "${item.title}:", value: item.value),
);
}
if (item is StandardExpandableListItem) {
return StandardExpandableList(
title: '${item.title}: ${item.expandableItems.length}',
expandableItems: item.expandableItems,
);
}
if (item is StandardExpandableListItem) {
return StandardExpandableList(
title: "${item.title}: ${item.expandableItems.length}",
expandableItems: item.expandableItems,
);
}
if (item is StandardPickerListItem) {
return StandardPickerList(
title: item.title,
value: item.value,
items: item.items,
displayItem: item.displayItem,
onSliderChanged: item.onSliderChanged,
onItemSelected: item.onItemSelected,
selectedIdx: item.selectedIdx,
customItemIndex: item.customItemIndex,
customValue: item.customValue,
maxValue: item.maxValue,
);
}
if (item is StandardPickerListItem) {
return StandardPickerList(
title: item.title,
value: item.value,
items: item.items,
displayItem: item.displayItem,
onSliderChanged: item.onSliderChanged,
onItemSelected: item.onItemSelected,
selectedIdx: item.selectedIdx,
customItemIndex: item.customItemIndex,
customValue: item.customValue,
maxValue: item.maxValue,
);
}
if (item is TextFieldListItem) {
return TextFieldListRow(
title: item.title,
value: item.value,
onSubmitted: item.onSubmitted,
);
}
if (item is TextFieldListItem) {
return TextFieldListRow(
title: item.title,
value: item.value,
onSubmitted: item.onSubmitted,
);
}
return Container();
}),
return Container();
},
),
),
Padding(
padding: const EdgeInsets.all(24),
child: Observer(
builder: (_) => LoadingPrimaryButton(
onPressed: () async {
transactionDetailsViewModel
.replaceByFee(transactionDetailsViewModel.newFee.toString());
},
text: S.of(context).send,
isLoading:
transactionDetailsViewModel.sendViewModel.state is IsExecutingState,
isDisabled: transactionDetailsViewModel.sendViewModel.state
is ExecutedSuccessfullyState,
color: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,
))),
padding: const EdgeInsets.all(24),
child: Observer(
builder: (_) => LoadingPrimaryButton(
onPressed: () => transactionDetailsViewModel
.replaceByFee(transactionDetailsViewModel.newFee.toString()),
text: S.of(context).send,
isLoading: transactionDetailsViewModel.sendViewModel.state is IsExecutingState,
isDisabled:
transactionDetailsViewModel.sendViewModel.state is ExecutedSuccessfullyState,
color: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,
),
),
),
],
);
}
@@ -121,7 +123,7 @@ class RBFDetailsPage extends BasePage {
return;
}
reaction((_) => transactionDetailsViewModel.sendViewModel.state, (ExecutionState state) {
reaction((_) => transactionDetailsViewModel.sendViewModel.state, (state) {
if (state is! IsExecutingState &&
loadingBottomSheetContext != null &&
loadingBottomSheetContext!.mounted) {
@@ -131,35 +133,35 @@ class RBFDetailsPage extends BasePage {
if (state is FailureState) {
WidgetsBinding.instance.addPostFrameCallback((_) {
showPopUp<void>(
context: context,
builder: (BuildContext popupContext) {
return AlertWithOneAction(
alertTitle: S.of(popupContext).error,
alertContent: state.error,
buttonText: S.of(popupContext).ok,
buttonAction: () => Navigator.of(popupContext).pop());
});
context: context,
builder: (popupContext) => AlertWithOneAction(
alertTitle: S.of(popupContext).error,
alertContent: state.error,
buttonText: S.of(popupContext).ok,
buttonAction: () => Navigator.of(popupContext).pop(),
),
);
});
}
if (state is AwaitingConfirmationState) {
WidgetsBinding.instance.addPostFrameCallback((_) {
showPopUp<void>(
context: context,
builder: (BuildContext popupContext) {
return AlertWithTwoActions(
alertTitle: state.title ?? '',
alertContent: state.message ?? '',
rightButtonText: S.of(context).ok,
leftButtonText: S.of(context).cancel,
actionRightButton: () {
state.onConfirm?.call();
Navigator.of(popupContext).pop();
},
actionLeftButton: () {
state.onCancel?.call();
Navigator.of(popupContext).pop();
});
});
context: context,
builder: (popupContext) => AlertWithTwoActions(
alertTitle: state.title ?? "",
alertContent: state.message ?? "",
rightButtonText: S.of(context).ok,
leftButtonText: S.of(context).cancel,
actionRightButton: () {
state.onConfirm?.call();
Navigator.of(popupContext).pop();
},
actionLeftButton: () {
state.onCancel?.call();
Navigator.of(popupContext).pop();
},
),
);
});
}
@@ -169,7 +171,7 @@ class RBFDetailsPage extends BasePage {
showModalBottomSheet<void>(
context: context,
isDismissible: false,
builder: (BuildContext context) {
builder: (context) {
loadingBottomSheetContext = context;
return LoadingBottomSheet(
titleText: S.of(context).generating_transaction,
@@ -187,42 +189,42 @@ class RBFDetailsPage extends BasePage {
context: context,
isDismissible: false,
isScrollControlled: true,
builder: (BuildContext bottomSheetContext) {
return ConfirmSendingBottomSheet(
key: ValueKey('rbf_confirm_sending_bottom_sheet'),
titleText: S.of(bottomSheetContext).confirm_transaction,
isSlideActionEnabled: transactionDetailsViewModel.sendViewModel.isReadyForSend,
walletType: transactionDetailsViewModel.sendViewModel.walletType,
titleIconPath:
transactionDetailsViewModel.sendViewModel.selectedCryptoCurrency.iconPath,
currency: transactionDetailsViewModel.sendViewModel.selectedCryptoCurrency,
amount: S.of(bottomSheetContext).send_amount,
amountValue:
transactionDetailsViewModel.sendViewModel.pendingTransaction!.amountFormatted,
fiatAmountValue: transactionDetailsViewModel
.sendViewModel.pendingTransactionFiatAmountFormatted,
fee: S.of(bottomSheetContext).send_fee,
feeValue:
transactionDetailsViewModel.sendViewModel.pendingTransaction!.feeFormatted,
feeFiatAmount: transactionDetailsViewModel
.sendViewModel.pendingTransactionFeeFiatAmountFormatted,
outputs: transactionDetailsViewModel.sendViewModel.outputs,
footerType: FooterType.slideActionButton,
accessibleNavigationModeSlideActionButtonText: S.of(context).send,
onSlideActionComplete: () async {
Navigator.of(bottomSheetContext).pop();
await transactionDetailsViewModel.sendViewModel.commitTransaction(context);
try {
builder: (bottomSheetContext) => ConfirmSendingBottomSheet(
key: const ValueKey("rbf_confirm_sending_bottom_sheet"),
titleText: S.of(bottomSheetContext).confirm_transaction,
isSlideActionEnabled: transactionDetailsViewModel.sendViewModel.isReadyForSend,
walletType: transactionDetailsViewModel.sendViewModel.walletType,
titleIconPath:
transactionDetailsViewModel.sendViewModel.selectedCryptoCurrency.iconPath,
currency: transactionDetailsViewModel.sendViewModel.selectedCryptoCurrency,
amount: S.of(bottomSheetContext).send_amount,
amountValue:
transactionDetailsViewModel.sendViewModel.pendingTransaction!.amountFormatted,
fiatAmountValue:
transactionDetailsViewModel.sendViewModel.pendingTransactionFiatAmountFormatted,
fee: S.of(bottomSheetContext).send_fee,
feeValue:
transactionDetailsViewModel.sendViewModel.pendingTransaction!.feeFormatted,
feeFiatAmount: transactionDetailsViewModel
.sendViewModel.pendingTransactionFeeFiatAmountFormatted,
outputs: transactionDetailsViewModel.sendViewModel.outputs,
footerType: FooterType.slideActionButton,
accessibleNavigationModeSlideActionButtonText: S.of(context).send,
onSlideActionComplete: () async {
Navigator.of(bottomSheetContext).pop();
await transactionDetailsViewModel.sendViewModel.commitTransaction(context);
try {
if (bottomSheetContext.mounted) {
Navigator.of(bottomSheetContext).pop();
} catch (_) {}
},
change: transactionDetailsViewModel.sendViewModel.pendingTransaction!.change,
amountParsingProxy: transactionDetailsViewModel.sendViewModel.amountParsingProxy,
);
},
}
} catch (_) {}
},
change: transactionDetailsViewModel.sendViewModel.pendingTransaction!.change,
amountParsingProxy: transactionDetailsViewModel.sendViewModel.amountParsingProxy,
),
);
if (result == null) {
transactionDetailsViewModel.sendViewModel.dismissTransaction();
await transactionDetailsViewModel.sendViewModel.dismissTransaction();
}
}
});
@@ -232,14 +234,14 @@ class RBFDetailsPage extends BasePage {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (context.mounted) {
showPopUp<void>(
context: context,
builder: (BuildContext popupContext) {
return AlertWithOneAction(
alertTitle: S.of(popupContext).sending,
alertContent: S.of(popupContext).transaction_sent,
buttonText: S.of(popupContext).ok,
buttonAction: () => Navigator.of(popupContext).pop());
});
context: context,
builder: (popupContext) => AlertWithOneAction(
alertTitle: S.of(popupContext).sending,
alertContent: S.of(popupContext).transaction_sent,
buttonText: S.of(popupContext).ok,
buttonAction: () => Navigator.of(popupContext).pop(),
),
);
}
});
}
@@ -60,8 +60,13 @@ class TransactionListItem extends ActionListItem with Keyable {
return 'Transaction has missing data';
}
if (transaction.additionalInfo['autoShield'] == true) {
return "Autoshield";
if (transaction.additionalInfo['isAutoShield'] == true) {
if (transaction.isPending) {
final status = formattedStatus;
final baseString = S.current.shielding;
return status.isNotEmpty ? "$baseString $status" : "$baseString...";
}
return S.current.shielding;
}
if (transaction.isPending) {
final status = formattedStatus;
+4 -2
View File
@@ -21,6 +21,7 @@ import 'package:cw_core/balance.dart';
import 'package:cw_core/crypto_amount_format.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/currency_for_wallet_type.dart';
import 'package:cw_core/sync_status.dart';
import 'package:cw_core/transaction_history.dart';
import 'package:cw_core/transaction_info.dart';
import 'package:cw_core/utils/print_verbose.dart';
@@ -50,8 +51,9 @@ abstract class OutputBase with Store {
parsedAddress = ParsedAddress(parsedAddressByCurrencyMap: {}) {
autorun((_) {
final status = _wallet.syncStatus;
printV("Sync status changed to $status. Recalculating fees");
if (status is! SyncedSyncStatus) {
return;
}
calculateEstimatedFee();
});
}
+277 -253
View File
@@ -1,50 +1,50 @@
import 'package:cake_wallet/reactions/wallet_connect.dart';
import 'package:cake_wallet/solana/solana.dart';
import 'package:cake_wallet/src/screens/transaction_details/address_list_item.dart';
import 'package:cake_wallet/src/screens/transaction_details/confirmations_list_item.dart';
import 'package:cake_wallet/store/app_store.dart';
import 'package:cake_wallet/core/address_validator.dart';
import 'package:cake_wallet/tron/tron.dart';
import 'package:cake_wallet/zano/zano.dart';
import 'package:cw_core/crypto_amount_format.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/currency_for_wallet_type.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/transaction_info.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cake_wallet/evm/evm.dart';
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/entities/priority_for_wallet_type.dart';
import 'package:cake_wallet/entities/transaction_description.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/monero/monero.dart';
import 'package:cake_wallet/src/screens/transaction_details/rbf_details_list_fee_picker_item.dart';
import 'package:cake_wallet/src/screens/transaction_details/standart_list_item.dart';
import 'package:cake_wallet/src/screens/transaction_details/transaction_details_list_item.dart';
import 'package:cake_wallet/src/screens/transaction_details/transaction_expandable_list_item.dart';
import 'package:cake_wallet/utils/date_formatter.dart';
import 'package:cake_wallet/view_model/send/send_view_model.dart';
import 'package:collection/collection.dart';
import 'package:cw_core/transaction_direction.dart';
import 'package:cw_core/transaction_priority.dart';
import 'package:flutter/foundation.dart';
import 'package:hive/hive.dart';
import 'package:intl/intl.dart';
import 'package:mobx/mobx.dart';
import 'package:url_launcher/url_launcher.dart';
import "package:cake_wallet/bitcoin/bitcoin.dart";
import "package:cake_wallet/core/address_validator.dart";
import "package:cake_wallet/entities/priority_for_wallet_type.dart";
import "package:cake_wallet/entities/transaction_description.dart";
import "package:cake_wallet/evm/evm.dart";
import "package:cake_wallet/generated/i18n.dart";
import "package:cake_wallet/monero/monero.dart";
import "package:cake_wallet/reactions/wallet_connect.dart";
import "package:cake_wallet/solana/solana.dart";
import "package:cake_wallet/src/screens/transaction_details/address_list_item.dart";
import "package:cake_wallet/src/screens/transaction_details/confirmations_list_item.dart";
import "package:cake_wallet/src/screens/transaction_details/rbf_details_list_fee_picker_item.dart";
import "package:cake_wallet/src/screens/transaction_details/standart_list_item.dart";
import "package:cake_wallet/src/screens/transaction_details/transaction_details_list_item.dart";
import "package:cake_wallet/src/screens/transaction_details/transaction_expandable_list_item.dart";
import "package:cake_wallet/store/app_store.dart";
import "package:cake_wallet/tron/tron.dart";
import "package:cake_wallet/view_model/send/send_view_model.dart";
import "package:cake_wallet/zano/zano.dart";
import "package:collection/collection.dart";
import "package:cw_core/crypto_currency.dart";
import "package:cw_core/currency_for_wallet_type.dart";
import "package:cw_core/transaction_direction.dart";
import "package:cw_core/transaction_info.dart";
import "package:cw_core/transaction_priority.dart";
import "package:cw_core/wallet_base.dart";
import "package:cw_core/wallet_type.dart";
import "package:flutter/foundation.dart";
import "package:hive/hive.dart";
import "package:intl/intl.dart";
import "package:mobx/mobx.dart";
import "package:url_launcher/url_launcher.dart";
part 'transaction_details_view_model.g.dart';
part "transaction_details_view_model.g.dart";
bool _trueFunc(_) => true;
/// We're adding a regex here so we can remove any already saved address that has the account in it.
/// In the refactor, we will make another separate variable for accounts and the UI would handle it as needed.
String _moneroRecipientAddressForDisplay(String raw, WalletType walletType) {
if (walletType != WalletType.monero || raw.isEmpty) return raw;
if (walletType != WalletType.monero || raw.isEmpty) {
return raw;
}
final compact = raw.replaceAll(RegExp(r'\s'), '');
final compact = raw.replaceAll(RegExp(r"\s"), "");
final match = RegExp(
r'4[0-9a-zA-Z]{94}|8[0-9a-zA-Z]{94}|[0-9a-zA-Z]{106}',
r"4[0-9a-zA-Z]{94}|8[0-9a-zA-Z]{94}|[0-9a-zA-Z]{106}",
caseSensitive: false,
).firstMatch(compact);
return match?.group(0) ?? raw.trim();
@@ -55,6 +55,14 @@ bool isLightning(TransactionInfo tx) => (tx.additionalInfo["isLightning"] as boo
bool hasLightningPreimage(TransactionInfo tx) => (tx.additionalInfo["preimage"] as String?) != null;
class TxDetailRowDefinition {
TxDetailRowDefinition({
required this.keyString,
required this.title,
required this.valueGetter,
this.applicable = _trueFunc,
this.listItemBuilder = StandartListItem.new,
});
final String keyString;
final String title;
final String Function(TransactionDetailsViewModelBase) valueGetter;
@@ -65,20 +73,13 @@ class TxDetailRowDefinition {
required Key key,
}) listItemBuilder;
TxDetailRowDefinition(
{required this.keyString,
required this.title,
required this.valueGetter,
this.applicable = _trueFunc,
this.listItemBuilder = StandartListItem.new});
static final List<TxDetailRowDefinition> defs = [
TxDetailRowDefinition(
keyString: "standard_list_item_transaction_details_date_key",
title: S.current.transaction_details_date,
valueGetter: (vm) =>
DateFormat("d MMMM yyyy, HH:mm", vm._appStore.settingsStore.languageCode)
.format(vm.transactionInfo.date)),
keyString: "standard_list_item_transaction_details_date_key",
title: S.current.transaction_details_date,
valueGetter: (vm) => DateFormat("d MMMM yyyy, HH:mm", vm._appStore.settingsStore.languageCode)
.format(vm.transactionInfo.date),
),
TxDetailRowDefinition(
keyString: "standard_list_item_transaction_details_height_key",
title: S.current.transaction_details_height,
@@ -88,125 +89,137 @@ class TxDetailRowDefinition {
!isLightning(vm.transactionInfo),
),
TxDetailRowDefinition(
keyString: "standard_list_item_transaction_details_fee_key",
title: S.current.transaction_details_fee,
valueGetter: (vm) => vm.transactionInfo.fee!.toStringWithSymbol(),
applicable: (vm) =>
vm.wallet.type != WalletType.nano &&
(vm.transactionInfo.fee?.toStringWithSymbol() ?? "").isNotEmpty),
keyString: "standard_list_item_transaction_details_fee_key",
title: S.current.transaction_details_fee,
valueGetter: (vm) => vm.feeAmount,
applicable: (vm) =>
vm.wallet.type != WalletType.nano &&
(vm.transactionInfo.fee?.toStringWithSymbol() ?? "").isNotEmpty,
),
TxDetailRowDefinition(
keyString: "standard_list_item_transaction_confirmations_key",
title: S.current.confirmations,
valueGetter: (vm) => "${vm.transactionInfo.confirmations}/${vm.neededConfirmations}",
applicable: (vm) =>
[...electrumWalletTypes, ...evmWalletTypes, WalletType.zcash, WalletType.monero]
.contains(vm.wallet.type) &&
!isLightning(vm.transactionInfo),
listItemBuilder: ConfirmationsListItem.new),
keyString: "standard_list_item_transaction_confirmations_key",
title: S.current.confirmations,
valueGetter: (vm) => "${vm.transactionInfo.confirmations}/${vm.neededConfirmations}",
applicable: (vm) =>
[...electrumWalletTypes, ...evmWalletTypes, WalletType.zcash, WalletType.monero]
.contains(vm.wallet.type) &&
!isLightning(vm.transactionInfo),
listItemBuilder: ConfirmationsListItem.new,
),
TxDetailRowDefinition(
keyString: "standard_list_item_transaction_details_recipient_address_key",
title: S.current.transaction_details_recipient_address,
valueGetter: (vm) {
String? ret = null;
keyString: "standard_list_item_transaction_details_recipient_address_key",
title: S.current.transaction_details_recipient_address,
valueGetter: (vm) {
String? ret;
switch (vm.wallet.type) {
case WalletType.monero:
if (vm.transactionInfo.direction == TransactionDirection.incoming) {
ret = monero!.getTransactionAddress(
vm.wallet,
vm.transactionInfo.additionalInfo['accountIndex'] as int,
vm.transactionInfo.additionalInfo['addressIndex'] as int);
}
case WalletType.bitcoin:
ret = (bitcoin!.getTransactionAddresses(vm.wallet, vm.transactionInfo) ?? [])
.firstOrNull ??
"";
case WalletType.tron:
if (vm.transactionInfo.to != null)
ret = tron!.getTronBase58Address(vm.transactionInfo.to!, vm.wallet);
default:
break;
}
if (ret == null) {
ret = vm.transactionInfo.to ?? "";
}
final resolvedAddress = _moneroRecipientAddressForDisplay(ret, vm.wallet.type);
vm.isRecipientAddressShown = resolvedAddress.isNotEmpty;
return resolvedAddress;
},
applicable: (vm) =>
vm.showRecipientAddress &&
(vm.transactionInfo.to != null ||
[WalletType.monero, WalletType.tron].contains(vm.wallet.type) ||
vm.wallet.type == WalletType.bitcoin &&
vm.transactionInfo.direction == TransactionDirection.incoming),
listItemBuilder: AddressListItem.new),
TxDetailRowDefinition(
keyString: "standard_list_item_transaction_details_source_address_key",
title: S.current.transaction_details_source_address,
valueGetter: (vm) {
switch (vm.wallet.type) {
case WalletType.tron:
return tron!.getTronBase58Address(vm.transactionInfo.from!, vm.wallet);
default:
return vm.transactionInfo.from!;
}
},
applicable: (vm) => vm.transactionInfo.from != null,
listItemBuilder: AddressListItem.new),
TxDetailRowDefinition(
keyString: "standard_list_item_address_label_key",
title: S.current.address_label,
valueGetter: (vm) => monero!.getSubaddressLabel(
vm.wallet,
vm.transactionInfo.additionalInfo['accountIndex'] as int,
vm.transactionInfo.additionalInfo['addressIndex'] as int),
applicable: (vm) => vm.wallet.type == WalletType.monero),
TxDetailRowDefinition(
keyString: "standard_list_item_transaction_key",
title: S.current.transaction_key,
valueGetter: (vm) {
final descriptionKey =
'${vm.transactionInfo.txHash}_${vm.wallet.walletAddresses.primaryAddress}';
switch (vm.wallet.type) {
case WalletType.monero:
if (vm.transactionInfo.direction == TransactionDirection.incoming) {
ret = monero!.getTransactionAddress(
vm.wallet,
vm.transactionInfo.additionalInfo["accountIndex"] as int,
vm.transactionInfo.additionalInfo["addressIndex"] as int,
);
}
case WalletType.bitcoin:
ret = (bitcoin!.getTransactionAddresses(vm.wallet, vm.transactionInfo) ?? [])
.firstOrNull ??
"";
case WalletType.tron:
if (vm.transactionInfo.to != null) {
ret = tron!.getTronBase58Address(vm.transactionInfo.to!, vm.wallet);
}
default:
break;
}
ret ??= vm.transactionInfo.to ?? "";
final description = vm.transactionDescriptionBox.values.firstWhere(
(val) => val.id == descriptionKey || val.id == vm.transactionInfo.txHash,
orElse: () => TransactionDescription(id: descriptionKey));
return vm.transactionInfo.additionalInfo['key'] as String? ??
description.transactionKey ??
"";
},
applicable: (vm) => vm.wallet.type == WalletType.monero),
final resolvedAddress = _moneroRecipientAddressForDisplay(ret, vm.wallet.type);
vm.isRecipientAddressShown = resolvedAddress.isNotEmpty;
return resolvedAddress;
},
applicable: (vm) =>
vm.showRecipientAddress &&
(vm.transactionInfo.to != null ||
[WalletType.monero, WalletType.tron].contains(vm.wallet.type) ||
vm.wallet.type == WalletType.bitcoin &&
vm.transactionInfo.direction == TransactionDirection.incoming),
listItemBuilder: AddressListItem.new,
),
TxDetailRowDefinition(
keyString: "standard_list_item_transaction_details_source_address_key",
title: S.current.transaction_details_source_address,
valueGetter: (vm) {
switch (vm.wallet.type) {
case WalletType.tron:
return tron!.getTronBase58Address(vm.transactionInfo.from!, vm.wallet);
default:
return vm.transactionInfo.from!;
}
},
applicable: (vm) => vm.transactionInfo.from != null,
listItemBuilder: AddressListItem.new,
),
TxDetailRowDefinition(
keyString: "standard_list_item_address_label_key",
title: S.current.address_label,
valueGetter: (vm) => monero!.getSubaddressLabel(
vm.wallet,
vm.transactionInfo.additionalInfo["accountIndex"] as int,
vm.transactionInfo.additionalInfo["addressIndex"] as int,
),
applicable: (vm) => vm.wallet.type == WalletType.monero,
),
TxDetailRowDefinition(
keyString: "standard_list_item_transaction_key",
title: S.current.transaction_key,
valueGetter: (vm) {
final descriptionKey =
"${vm.transactionInfo.txHash}_${vm.wallet.walletAddresses.primaryAddress}";
final description = vm.transactionDescriptionBox.values.firstWhere(
(val) => val.id == descriptionKey || val.id == vm.transactionInfo.txHash,
orElse: () => TransactionDescription(id: descriptionKey),
);
return vm.transactionInfo.additionalInfo["key"] as String? ??
description.transactionKey ??
"";
},
applicable: (vm) => vm.wallet.type == WalletType.monero,
),
TxDetailRowDefinition(
keyString: "standard_list_item_lightning_preimage",
title: S.current.transaction_preimage,
valueGetter: (vm) => vm.transactionInfo.additionalInfo['preimage'] as String? ?? "",
valueGetter: (vm) => vm.transactionInfo.additionalInfo["preimage"] as String? ?? "",
applicable: (vm) =>
hasLightningPreimage(vm.transactionInfo) && isLightning(vm.transactionInfo),
),
TxDetailRowDefinition(
keyString: "standard_list_item_transaction_confirmed_key",
title: S.current.confirmed_tx,
valueGetter: (vm) => (vm.transactionInfo.confirmations > 0).toString(),
applicable: (vm) => vm.wallet.type == WalletType.nano),
keyString: "standard_list_item_transaction_confirmed_key",
title: S.current.confirmed_tx,
valueGetter: (vm) => (vm.transactionInfo.confirmations > 0).toString(),
applicable: (vm) => vm.wallet.type == WalletType.nano,
),
TxDetailRowDefinition(
keyString: "standard_list_item_transaction_details_memo_key",
title: S.current.memo,
valueGetter: (vm) => vm.transactionInfo.additionalInfo['memo'] as String,
applicable: (vm) =>
vm.wallet.type == WalletType.zcash &&
vm.transactionInfo.additionalInfo["memo"] != null),
keyString: "standard_list_item_transaction_details_memo_key",
title: S.current.memo,
valueGetter: (vm) => vm.transactionInfo.additionalInfo["memo"] as String,
applicable: (vm) =>
vm.wallet.type == WalletType.zcash && vm.transactionInfo.additionalInfo["memo"] != null,
),
TxDetailRowDefinition(
keyString: "standard_list_item_transaction_details_asset_id_key",
title: "Asset ID",
valueGetter: (vm) =>
vm.transactionInfo.additionalInfo["assetId"] as String? ?? "Unknown asset id",
applicable: (vm) => vm.wallet.type == WalletType.zano),
keyString: "standard_list_item_transaction_details_asset_id_key",
title: "Asset ID",
valueGetter: (vm) =>
vm.transactionInfo.additionalInfo["assetId"] as String? ?? "Unknown asset id",
applicable: (vm) => vm.wallet.type == WalletType.zano,
),
TxDetailRowDefinition(
keyString: "standard_list_item_transaction_details_comment_key",
title: S.current.transaction_details_title,
valueGetter: (vm) => vm.transactionInfo.additionalInfo['comment'] as String? ?? "",
applicable: (vm) => vm.wallet.type == WalletType.zano),
keyString: "standard_list_item_transaction_details_comment_key",
title: S.current.transaction_details_title,
valueGetter: (vm) => vm.transactionInfo.additionalInfo["comment"] as String? ?? "",
applicable: (vm) => vm.wallet.type == WalletType.zano,
),
TxDetailRowDefinition(
keyString: "standard_list_item_transaction_details_id_key",
title: S.current.transaction_details_transaction_id,
@@ -227,7 +240,7 @@ abstract class TransactionDetailsViewModelBase with Store {
required this.sendViewModel,
this.canReplaceByFee = false,
}) : items = [],
RBFListItems = [],
rbfListItems = [],
newFee = 0,
isRecipientAddressShown = false,
_appStore = appStore,
@@ -236,19 +249,23 @@ abstract class TransactionDetailsViewModelBase with Store {
for (final def in TxDetailRowDefinition.defs) {
if (def.applicable(this)) {
items.add(def.listItemBuilder(
items.add(
def.listItemBuilder(
title: def.title,
value: def.valueGetter(this),
key: ValueKey(def.keyString)) as TransactionDetailsListItem);
key: ValueKey(def.keyString),
) as TransactionDetailsListItem,
);
}
}
_checkForRBF(tx);
final descriptionKey = '${transactionInfo.txHash}_${wallet.walletAddresses.primaryAddress}';
final descriptionKey = "${transactionInfo.txHash}_${wallet.walletAddresses.primaryAddress}";
final description = transactionDescriptionBox.values.firstWhere(
(val) => val.id == descriptionKey || val.id == transactionInfo.txHash,
orElse: () => TransactionDescription(id: descriptionKey));
(val) => val.id == descriptionKey || val.id == transactionInfo.txHash,
orElse: () => TransactionDescription(id: descriptionKey),
);
if (showRecipientAddress && !isRecipientAddressShown) {
final recipientAddress = description.recipientAddress;
@@ -260,7 +277,7 @@ abstract class TransactionDetailsViewModelBase with Store {
AddressListItem(
title: S.current.transaction_details_recipient_address,
value: recipientAddressForDisplay,
key: ValueKey('standard_list_item_${recipientAddressForDisplay}_key'),
key: ValueKey("standard_list_item_${recipientAddressForDisplay}_key"),
),
);
}
@@ -268,10 +285,11 @@ abstract class TransactionDetailsViewModelBase with Store {
}
void updateNote(String note) {
final descriptionKey = '${transactionInfo.txHash}_${wallet.walletAddresses.primaryAddress}';
final descriptionKey = "${transactionInfo.txHash}_${wallet.walletAddresses.primaryAddress}";
final description = transactionDescriptionBox.values.firstWhere(
(val) => val.id == descriptionKey || val.id == transactionInfo.txHash,
orElse: () => TransactionDescription(id: descriptionKey));
(val) => val.id == descriptionKey || val.id == transactionInfo.txHash,
orElse: () => TransactionDescription(id: descriptionKey),
);
description.transactionNote = note;
@@ -283,9 +301,10 @@ abstract class TransactionDetailsViewModelBase with Store {
}
String get note {
final descriptionKey = '${transactionInfo.txHash}_${wallet.walletAddresses.primaryAddress}';
final descriptionKey = "${transactionInfo.txHash}_${wallet.walletAddresses.primaryAddress}";
final description = transactionDescriptionBox.values
.firstWhereOrNull((val) => val.id == descriptionKey || val.id == transactionInfo.txHash);
.firstWhereOrNull((val) => val.id == descriptionKey || val.id == transactionInfo.txHash,
);
return description?.transactionNote ?? "";
}
@@ -296,7 +315,7 @@ abstract class TransactionDetailsViewModelBase with Store {
final AppStore _appStore;
final List<TransactionDetailsListItem> items;
final List<TransactionDetailsListItem> RBFListItems;
final List<TransactionDetailsListItem> rbfListItems;
bool showRecipientAddress;
bool isRecipientAddressShown;
int newFee;
@@ -304,9 +323,13 @@ abstract class TransactionDetailsViewModelBase with Store {
TransactionPriority? transactionPriority;
CryptoCurrency get transactionAsset {
if (isEVMCompatibleChain(wallet.type)) return evm!.assetOfTransaction(wallet, transactionInfo);
if (isEVMCompatibleChain(wallet.type)) {
return evm!.assetOfTransaction(wallet, transactionInfo);
}
if (isLightning(transactionInfo)) return CryptoCurrency.btcln;
if (isLightning(transactionInfo)) {
return CryptoCurrency.btcln;
}
return switch (wallet.type) {
WalletType.solana => solana!.assetOfTransaction(wallet, transactionInfo),
@@ -316,26 +339,38 @@ abstract class TransactionDetailsViewModelBase with Store {
};
}
// TODO integrate these getters with the TransactionInfo object
@computed
String get transactionAmount =>
_appStore.amountParsingProxy.asDisplayStringWithSymbol(transactionInfo.amount);
@computed
String get feeAmount =>
_appStore.amountParsingProxy.asDisplayStringWithSymbol(transactionInfo.fee!);
@computed
String get transactionCopyAmount =>
_appStore.amountParsingProxy.asDisplayString(transactionInfo.amount);
// TODO(malik1004x): integrate these getters with the TransactionInfo object
String get formattedPendingStatus {
switch (wallet.type) {
case WalletType.monero:
case WalletType.haven:
case WalletType.zano:
if (transactionInfo.confirmations >= 0 && transactionInfo.confirmations < 10) {
return ' (${transactionInfo.confirmations}/10)';
return " (${transactionInfo.confirmations}/10)";
}
break;
case WalletType.wownero:
if (transactionInfo.confirmations >= 0 && transactionInfo.confirmations < 3) {
return ' (${transactionInfo.confirmations}/3)';
return " (${transactionInfo.confirmations}/3)";
}
break;
case WalletType.litecoin:
bool isPegIn = (transactionInfo.additionalInfo["isPegIn"] as bool?) ?? false;
bool isPegOut = (transactionInfo.additionalInfo["isPegOut"] as bool?) ?? false;
bool fromPegOut = (transactionInfo.additionalInfo["fromPegOut"] as bool?) ?? false;
String str = '';
final isPegIn = (transactionInfo.additionalInfo["isPegIn"] as bool?) ?? false;
final isPegOut = (transactionInfo.additionalInfo["isPegOut"] as bool?) ?? false;
final fromPegOut = (transactionInfo.additionalInfo["fromPegOut"] as bool?) ?? false;
String str = "";
if (transactionInfo.confirmations <= 0) {
str = S.current.pending;
}
@@ -352,10 +387,10 @@ abstract class TransactionDetailsViewModelBase with Store {
}
return str;
default:
return '';
return "";
}
return '';
return "";
}
String get formattedStatus {
@@ -369,7 +404,7 @@ abstract class TransactionDetailsViewModelBase with Store {
return formattedPendingStatus;
}
return transactionInfo.isPending ? S.current.pending : '';
return transactionInfo.isPending ? S.current.pending : "";
}
int get neededConfirmations {
@@ -381,9 +416,11 @@ abstract class TransactionDetailsViewModelBase with Store {
case WalletType.wownero:
return 3;
case WalletType.litecoin:
bool isPegOut = (transactionInfo.additionalInfo["isPegOut"] as bool?) ?? false;
bool fromPegOut = (transactionInfo.additionalInfo["fromPegOut"] as bool?) ?? false;
if (isPegOut || fromPegOut) return 6;
final isPegOut = (transactionInfo.additionalInfo["isPegOut"] as bool?) ?? false;
final fromPegOut = (transactionInfo.additionalInfo["fromPegOut"] as bool?) ?? false;
if (isPegOut || fromPegOut) {
return 6;
}
default:
return 0;
}
@@ -391,8 +428,8 @@ abstract class TransactionDetailsViewModelBase with Store {
}
String get formattedTitle {
if (transactionInfo.additionalInfo['autoShield'] == true) {
return "Autoshield";
if (transactionInfo.additionalInfo['isAutoShield'] == true) {
return S.current.shielding;
}
if (transactionInfo.direction == TransactionDirection.incoming) {
return S.current.received;
@@ -408,52 +445,56 @@ abstract class TransactionDetailsViewModelBase with Store {
final txId = transactionInfo.txHash;
if (wallet.chainId != null) {
final explorerUrl = evm!.getExplorerUrlForChainId(wallet.chainId!);
if (explorerUrl != null) return '$explorerUrl/tx/${txId}';
if (explorerUrl != null) {
return "$explorerUrl/tx/${txId}";
}
}
switch (wallet.type) {
case WalletType.monero:
return 'https://monero.com/tx/${txId}';
return "https://monero.com/tx/${txId}";
case WalletType.bitcoin:
return 'https://mempool.cakewallet.com/${wallet.isTestnet ? "testnet/" : ""}tx/${txId}';
return isLightning(transactionInfo)
? "https://sparkscan.io/tx/${txId}"
: 'https://mempool.cakewallet.com/${wallet.isTestnet ? "testnet/" : ""}tx/${txId}';
case WalletType.litecoin:
return bitcoin!.txIsMweb(transactionInfo)
? "https://www.mwebexplorer.com/blocks/block/${transactionInfo.height}"
: 'https://blockchair.com/litecoin/transaction/${txId}';
: "https://blockchair.com/litecoin/transaction/${txId}";
case WalletType.bitcoinCash:
return 'https://blockchair.com/bitcoin-cash/transaction/${txId}';
return "https://blockchair.com/bitcoin-cash/transaction/${txId}";
case WalletType.haven:
return 'https://explorer.havenprotocol.org/search?value=${txId}';
return "https://explorer.havenprotocol.org/search?value=${txId}";
case WalletType.ethereum:
return 'https://etherscan.io/tx/${txId}';
return "https://etherscan.io/tx/${txId}";
case WalletType.base:
return 'https://basescan.org/tx/${txId}';
return "https://basescan.org/tx/${txId}";
case WalletType.arbitrum:
return 'https://arbiscan.io/tx/${txId}';
return "https://arbiscan.io/tx/${txId}";
case WalletType.bsc:
return 'https://bscscan.com/tx/${txId}';
return "https://bscscan.com/tx/${txId}";
case WalletType.polygon:
return 'https://polygonscan.com/tx/${txId}';
return "https://polygonscan.com/tx/${txId}";
case WalletType.nano:
return 'https://nanexplorer.com/nano/block/${txId}';
return "https://nanexplorer.com/nano/block/${txId}";
case WalletType.banano:
return 'https://nanexplorer.com/banano/block/${txId}';
return "https://nanexplorer.com/banano/block/${txId}";
case WalletType.solana:
return 'https://solscan.io/tx/${txId}';
return "https://solscan.io/tx/${txId}";
case WalletType.tron:
return 'https://tronscan.org/#/transaction/${txId}';
return "https://tronscan.org/#/transaction/${txId}";
case WalletType.wownero:
return 'https://explore.wownero.com/tx/${txId}';
return "https://explore.wownero.com/tx/${txId}";
case WalletType.zano:
return 'https://explorer.zano.org/transaction/${txId}';
return "https://explorer.zano.org/transaction/${txId}";
case WalletType.decred:
return 'https://${wallet.isTestnet ? "testnet" : "dcrdata"}.decred.org/tx/${txId.split(':')[0]}';
case WalletType.dogecoin:
return 'https://blockchair.com/dogecoin/transaction/${txId}';
return "https://blockchair.com/dogecoin/transaction/${txId}";
case WalletType.zcash:
return 'https://blockchair.com/zcash/transaction/${txId}';
return "https://blockchair.com/zcash/transaction/${txId}";
case WalletType.none:
return '';
return "";
}
}
@@ -473,13 +514,17 @@ abstract class TransactionDetailsViewModelBase with Store {
: transactionInfo.outputAddresses!.length;
newFee = bitcoin!.getFeeAmountForPriority(
wallet, bitcoin!.getBitcoinTransactionPriorityMedium(), inputsCount, outputsCount);
wallet,
bitcoin!.getBitcoinTransactionPriorityMedium(),
inputsCount,
outputsCount,
);
RBFListItems.add(
rbfListItems.add(
StandartListItem(
title: S.current.old_fee,
value: tx.fee?.toStringWithSymbol() ?? '0.0',
key: ValueKey('standard_list_item_rbf_old_fee_key'),
value: tx.fee?.toStringWithSymbol() ?? "0.0",
key: const ValueKey("standard_list_item_rbf_old_fee_key"),
),
);
@@ -488,32 +533,33 @@ abstract class TransactionDetailsViewModelBase with Store {
final recommendedRate = (transactionInfo.fee! / BigInt.from(size)) +
transactionInfo.fee!.copyWith(amount: BigInt.one);
RBFListItems.add(
StandartListItem(title: 'New recommended fee rate', value: '$recommendedRate sat/byte'));
rbfListItems.add(
StandartListItem(title: "New recommended fee rate", value: "$recommendedRate sat/byte"),
);
}
final priorities = priorityForWalletType(wallet.type);
final selectedItem = priorities.indexOf(sendViewModel.feesViewModel.transactionPriority);
final customItem = priorities.firstWhereOrNull(
(element) => element == sendViewModel.feesViewModel.bitcoinTransactionPriorityCustom);
(element) => element == sendViewModel.feesViewModel.bitcoinTransactionPriorityCustom,
);
final customItemIndex = customItem != null ? priorities.indexOf(customItem) : null;
final maxCustomFeeRate = sendViewModel.feesViewModel.maxCustomFeeRate?.toDouble();
RBFListItems.add(
rbfListItems.add(
StandardPickerListItem(
key: ValueKey('standard_picker_list_item_transaction_priorities_key'),
key: const ValueKey("standard_picker_list_item_transaction_priorities_key"),
title: S.current.estimated_new_fee,
value: bitcoin!.formatterBitcoinAmountToString(amount: newFee) + ' ${wallet.currency}',
value: "${bitcoin!.formatterBitcoinAmountToString(amount: newFee)} ${wallet.currency}",
items: priorityForWalletType(wallet.type),
customValue: _appStore.settingsStore.customBitcoinFeeRate.toDouble(),
maxValue: maxCustomFeeRate,
selectedIdx: selectedItem,
customItemIndex: customItemIndex ?? 0,
displayItem: (dynamic priority, double sliderValue) =>
displayItem: (dynamic priority, sliderValue) =>
sendViewModel.feesViewModel.displayFeeRate(priority, sliderValue.round()),
onSliderChanged: (double newValue) =>
setNewFee(value: newValue, priority: transactionPriority!),
onItemSelected: (dynamic item, double sliderValue) {
onSliderChanged: (newValue) => setNewFee(value: newValue, priority: transactionPriority!),
onItemSelected: (dynamic item, sliderValue) {
transactionPriority = item as TransactionPriority;
return setNewFee(value: sliderValue, priority: transactionPriority!);
},
@@ -521,9 +567,9 @@ abstract class TransactionDetailsViewModelBase with Store {
);
if (transactionInfo.inputAddresses != null && transactionInfo.inputAddresses!.isNotEmpty) {
RBFListItems.add(
rbfListItems.add(
StandardExpandableListItem(
key: ValueKey('standard_expandable_list_item_transaction_input_addresses_key'),
key: const ValueKey("standard_expandable_list_item_transaction_input_addresses_key"),
title: S.current.inputs,
expandableItems: transactionInfo.inputAddresses!,
),
@@ -532,17 +578,17 @@ abstract class TransactionDetailsViewModelBase with Store {
if (transactionInfo.outputAddresses != null && transactionInfo.outputAddresses!.isNotEmpty) {
final outputAddresses = transactionInfo.outputAddresses!.map((element) {
if (element.contains('OP_RETURN:') && element.length > 40) {
return element.substring(0, 40) + '...';
if (element.contains("OP_RETURN:") && element.length > 40) {
return "${element.substring(0, 40)}...";
}
return element;
}).toList();
RBFListItems.add(
rbfListItems.add(
StandardExpandableListItem(
title: S.current.outputs,
expandableItems: outputAddresses,
key: ValueKey('standard_expandable_list_item_transaction_output_addresses_key'),
key: const ValueKey("standard_expandable_list_item_transaction_output_addresses_key"),
),
);
}
@@ -552,7 +598,7 @@ abstract class TransactionDetailsViewModelBase with Store {
Future<void> _checkForRBF(TransactionInfo tx) async {
if (wallet.type == WalletType.bitcoin &&
transactionInfo.direction == TransactionDirection.outgoing) {
final descriptionKey = '${transactionInfo.txHash}_${wallet.walletAddresses.primaryAddress}';
final descriptionKey = "${transactionInfo.txHash}_${wallet.walletAddresses.primaryAddress}";
final description = transactionDescriptionBox.values
.firstWhereOrNull((val) => val.id == descriptionKey || val.id == transactionInfo.txHash);
@@ -569,45 +615,23 @@ abstract class TransactionDetailsViewModelBase with Store {
}
}
String setNewFee({double? value, required TransactionPriority priority}) {
String setNewFee({required TransactionPriority priority, double? value}) {
newFee = priority == bitcoin!.getBitcoinTransactionPriorityCustom() && value != null
? bitcoin!.feeAmountWithFeeRate(
wallet,
value.round(),
transactionInfo.inputAddresses?.length ?? 1,
transactionInfo.outputAddresses?.length ?? 1)
transactionInfo.outputAddresses?.length ?? 1,
)
: bitcoin!.getFeeAmountForPriority(
wallet,
priority,
transactionInfo.inputAddresses?.length ?? 1,
transactionInfo.outputAddresses?.length ?? 1);
transactionInfo.outputAddresses?.length ?? 1,
);
return bitcoin!.formatterBitcoinAmountToString(amount: newFee);
}
String get formattedCryptoAmount {
if (wallet.type == WalletType.bitcoin) {
final crypto = isLightning(transactionInfo) ? CryptoCurrency.btcln : CryptoCurrency.btc;
final amount = _appStore.amountParsingProxy
.asDisplayString(transactionInfo.amount)
.withMaxDecimals(8)
.withLocalSeperator(_appStore.settingsStore.languageCode);
return '$amount ${_appStore.amountParsingProxy.getCryptoSymbol(crypto)}';
}
return transactionInfo.amount.toStringWithSymbol();
}
void replaceByFee(String newFee) => sendViewModel.replaceByFee(transactionInfo, newFee);
@computed
String get pendingTransactionFiatAmountValueFormatted => sendViewModel.isFiatDisabled
? ''
: sendViewModel.pendingTransactionFiatAmount + ' ' + sendViewModel.fiat.title;
@computed
String get pendingTransactionFeeFiatAmountFormatted => sendViewModel.isFiatDisabled
? ''
: sendViewModel.pendingTransactionFeeFiatAmount + ' ' + sendViewModel.fiat.title;
}
@@ -1,59 +1,58 @@
import 'dart:core';
import 'dart:developer' as dev;
import "dart:core";
import "dart:developer" as dev;
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/core/address_resolver/yat/yat_store.dart';
import 'package:cake_wallet/core/amount_parsing_proxy.dart';
import 'package:cake_wallet/core/fiat_conversion_service.dart';
import 'package:cake_wallet/core/wallet_change_listener_view_model.dart';
import 'package:cake_wallet/decred/decred.dart';
import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart';
import 'package:cake_wallet/entities/fiat_api_mode.dart';
import 'package:cake_wallet/entities/fiat_currency.dart';
import 'package:cake_wallet/evm/evm.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/monero/monero.dart';
import 'package:cake_wallet/reactions/wallet_connect.dart';
import 'package:cake_wallet/reactions/wallet_utils.dart';
import 'package:cake_wallet/solana/solana.dart';
import 'package:cake_wallet/store/app_store.dart';
import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart';
import 'package:cake_wallet/tron/tron.dart';
import 'package:cake_wallet/utils/list_item.dart';
import 'package:cake_wallet/utils/qr_util.dart';
import 'package:cake_wallet/view_model/wallet_address_list/wallet_account_list_header.dart';
import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_hidden_list_header.dart';
import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_header.dart';
import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart';
import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_util.dart';
import 'package:cake_wallet/wownero/wownero.dart';
import 'package:cake_wallet/zcash/zcash.dart';
import 'package:cake_wallet/zano/zano.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/currency.dart';
import 'package:cw_core/currency_for_wallet_type.dart';
import 'package:cw_core/erc20_token.dart';
import 'package:cw_core/payment_uris.dart';
import 'package:cw_core/spl_token.dart';
import 'package:cw_core/tron_token.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:mobx/mobx.dart';
import "package:cake_wallet/bitcoin/bitcoin.dart";
import "package:cake_wallet/core/address_resolver/yat/yat_store.dart";
import "package:cake_wallet/core/amount_parsing_proxy.dart";
import "package:cake_wallet/core/fiat_conversion_service.dart";
import "package:cake_wallet/core/wallet_change_listener_view_model.dart";
import "package:cake_wallet/decred/decred.dart";
import "package:cake_wallet/entities/auto_generate_subaddress_status.dart";
import "package:cake_wallet/entities/fiat_api_mode.dart";
import "package:cake_wallet/entities/fiat_currency.dart";
import "package:cake_wallet/evm/evm.dart";
import "package:cake_wallet/generated/i18n.dart";
import "package:cake_wallet/monero/monero.dart";
import "package:cake_wallet/reactions/wallet_connect.dart";
import "package:cake_wallet/reactions/wallet_utils.dart";
import "package:cake_wallet/solana/solana.dart";
import "package:cake_wallet/store/app_store.dart";
import "package:cake_wallet/store/dashboard/fiat_conversion_store.dart";
import "package:cake_wallet/tron/tron.dart";
import "package:cake_wallet/utils/list_item.dart";
import "package:cake_wallet/utils/qr_util.dart";
import "package:cake_wallet/view_model/wallet_address_list/wallet_account_list_header.dart";
import "package:cake_wallet/view_model/wallet_address_list/wallet_address_hidden_list_header.dart";
import "package:cake_wallet/view_model/wallet_address_list/wallet_address_list_header.dart";
import "package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart";
import "package:cake_wallet/view_model/wallet_address_list/wallet_address_util.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/crypto_currency.dart";
import "package:cw_core/currency.dart";
import "package:cw_core/currency_for_wallet_type.dart";
import "package:cw_core/erc20_token.dart";
import "package:cw_core/payment_uris.dart";
import "package:cw_core/spl_token.dart";
import "package:cw_core/tron_token.dart";
import "package:cw_core/wallet_type.dart";
import "package:mobx/mobx.dart";
part 'wallet_address_list_view_model.g.dart';
part "wallet_address_list_view_model.g.dart";
class WalletAddressListViewModel = WalletAddressListViewModelBase with _$WalletAddressListViewModel;
abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewModel with Store {
WalletAddressListViewModelBase({
required AppStore appStore,
required super.appStore,
required this.yatStore,
required this.fiatConversionStore,
}) : _baseItems = <ListItem>[],
selectedCurrency = appStore.wallet!.currency,
hasAccounts = [WalletType.monero, WalletType.wownero].contains(appStore.wallet!.type),
_appStore = appStore,
receivePageOption = appStore.wallet!.walletAddresses.walletInfo.addressPageType ?? '',
super(appStore: appStore) {
receivePageOption = appStore.wallet!.walletAddresses.walletInfo.addressPageType ?? "" {
_init();
}
@@ -81,8 +80,12 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
CryptoCurrency? tokenCurrency;
@computed
String get cryptoCurrencySymbol =>
_appStore.amountParsingProxy.getCryptoSymbol(tokenCurrency ?? wallet.currency);
String get cryptoCurrencySymbol => _appStore.amountParsingProxy.getCryptoSymbol(
tokenCurrency ??
(selectedCurrency is CryptoCurrency
? selectedCurrency as CryptoCurrency
: wallet.currency),
);
void setTokenCurrency(Currency curr) {
if (curr == wallet.currency || curr == CryptoCurrency.btcln) {
@@ -122,7 +125,7 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
_appStore.amountParsingProxy.useSatoshi(selectedCurrency as CryptoCurrency);
@observable
String searchText = '';
String searchText = "";
@computed
int get selectedCurrencyIndex => currencies.indexOf(selectedCurrency);
@@ -131,30 +134,45 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
int get tokenCurrencyIndex => tokenCurrency == null ? 0 : tokenCurrencies.indexOf(tokenCurrency!);
@observable
String _amount = '';
String _amount = "";
@computed
String get displayAmount => _appStore.amountParsingProxy
.getDisplayCryptoAmount(_amount, tokenCurrency ?? wallet.currency);
String get displayAmount => _appStore.amountParsingProxy.getDisplayCryptoAmount(
_amount,
tokenCurrency ??
(selectedCurrency is CryptoCurrency
? selectedCurrency as CryptoCurrency
: wallet.currency),
);
// NOT PRECISE! just for display purposes.
@computed
String get fiatAmount {
if (_amount.isEmpty) return "";
if (_amount.isEmpty) {
return "";
}
var cryptoCurrency = tokenCurrency ?? wallet.currency;
if (cryptoCurrency == CryptoCurrency.btcln) cryptoCurrency = CryptoCurrency.btc;
if (cryptoCurrency == CryptoCurrency.btcln) {
cryptoCurrency = CryptoCurrency.btc;
}
if (selectedCurrency is FiatCurrency && _fiatRate != null) {
return selectedCurrencyFiatAmount;
}
if (!fiatConversionStore.prices.containsKey(cryptoCurrency)) return "";
if (!fiatConversionStore.prices.containsKey(cryptoCurrency)) {
return "";
}
final amount = double.tryParse(_amount) ?? 0;
return (amount * fiatConversionStore.prices[cryptoCurrency]!).toStringAsFixed(2);
}
@computed
String get selectedCurrencyFiatAmount {
if (_fiatRate == null) return "";
if (_fiatRate == null) {
return "";
}
final amount = double.tryParse(_amount) ?? 0;
return (amount * _fiatRate!).toStringAsFixed(2);
}
@@ -201,26 +219,29 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
PaymentURI get uri {
if (tokenCurrency != null && isEVMCompatibleChain(wallet.type)) {
return ERC681URI(
chainId: wallet.chainId ?? 1,
address: wallet.walletAddresses.address,
amount: _amount,
contractAddress: (tokenCurrency as Erc20Token).contractAddress);
chainId: wallet.chainId ?? 1,
address: wallet.walletAddresses.address,
amount: _amount,
contractAddress: (tokenCurrency! as Erc20Token).contractAddress,
);
}
if (tokenCurrency is TronToken && wallet.type == WalletType.tron) {
return TronURI(
amount: _amount,
address: wallet.walletAddresses.address,
contractAddress: (tokenCurrency as TronToken).contractAddress,
contractAddress: (tokenCurrency! as TronToken).contractAddress,
);
}
if (tokenCurrency is SPLToken && wallet.type == WalletType.solana) {
return SolanaURI(
amount: _amount,
address: wallet.walletAddresses.address,
contractAddress: (tokenCurrency as SPLToken).mintAddress,
contractAddress: (tokenCurrency! as SPLToken).mintAddress,
);
}
if (isLightning && _lnPaymentRequest != null) return _lnPaymentRequest!;
if (isLightning && _lnPaymentRequest != null) {
return _lnPaymentRequest!;
}
return wallet.walletAddresses.getPaymentUri(_amount);
}
@@ -257,10 +278,11 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
final isPrimary = subaddress == primaryAddress;
return WalletAddressListItem(
id: subaddress.id,
isPrimary: isPrimary,
name: subaddress.label,
address: subaddress.address);
id: subaddress.id,
isPrimary: isPrimary,
name: subaddress.label,
address: subaddress.address,
);
});
addressList.addAll(addressItems);
}
@@ -285,37 +307,37 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
addressList.addAll(addressItems);
addressList.add(WalletAddressListHeader(title: S.current.received));
final receivedAddressItems =
bitcoin!.getSilentPaymentReceivedAddresses(wallet).map((address) {
return WalletAddressListItem(
id: address.id,
isPrimary: false,
name: address.name,
address: address.address,
txCount: address.txCount,
balance: _appStore.amountParsingProxy
.getDisplayCryptoString(address.balance, walletTypeToCryptoCurrency(type)),
isChange: address.isChange,
isOneTimeReceiveAddress: true,
derivationPath: address.derivationPath,
);
});
final receivedAddressItems = bitcoin!.getSilentPaymentReceivedAddresses(wallet).map(
(address) => WalletAddressListItem(
id: address.id,
isPrimary: false,
name: address.name,
address: address.address,
txCount: address.txCount,
balance: _appStore.amountParsingProxy
.getDisplayCryptoString(address.balance, walletTypeToCryptoCurrency(type)),
isChange: address.isChange,
isOneTimeReceiveAddress: true,
derivationPath: address.derivationPath,
),
);
addressList.addAll(receivedAddressItems);
} else {
var addressItems = bitcoin!.getSubAddresses(wallet).map((subaddress) {
final isPrimary = subaddress.id == 0;
return WalletAddressListItem(
id: subaddress.id,
isPrimary: isPrimary,
name: subaddress.name,
address: subaddress.address,
txCount: subaddress.txCount,
balance: _appStore.amountParsingProxy
.getDisplayCryptoString(subaddress.balance, walletTypeToCryptoCurrency(type)),
isChange: subaddress.isChange,
isLegacyDerivation: subaddress.isLegacyDerivation,
derivationPath: subaddress.derivationPath);
id: subaddress.id,
isPrimary: isPrimary,
name: subaddress.name,
address: subaddress.address,
txCount: subaddress.txCount,
balance: _appStore.amountParsingProxy
.getDisplayCryptoString(subaddress.balance, walletTypeToCryptoCurrency(type)),
isChange: subaddress.isChange,
isLegacyDerivation: subaddress.isLegacyDerivation,
derivationPath: subaddress.derivationPath,
);
});
// don't show all 1000+ mweb addresses:
@@ -346,11 +368,13 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
}
if (wallet.type == WalletType.nano) {
addressList.add(WalletAddressListItem(
isPrimary: true,
name: null,
address: wallet.walletAddresses.address,
));
addressList.add(
WalletAddressListItem(
isPrimary: true,
name: null,
address: wallet.walletAddresses.address,
),
);
}
if (wallet.type == WalletType.tron) {
@@ -361,29 +385,35 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
if (wallet.type == WalletType.decred) {
final addrInfos = decred!.getAddressInfos(wallet);
addrInfos.forEach((info) {
for (final info in addrInfos) {
addressList.add(
new WalletAddressListItem(isPrimary: false, address: info.address, name: info.label));
});
WalletAddressListItem(isPrimary: false, address: info.address, name: info.label),
);
}
}
if (wallet.type == WalletType.zcash) {
final addrInfos = zcash!.getAddressInfos(wallet);
addrInfos.forEach((info) {
for (final info in addrInfos) {
addressList.add(
new WalletAddressListItem(isPrimary: false, address: info.address, name: info.label));
});
WalletAddressListItem(isPrimary: false, address: info.address, name: info.label),
);
}
}
for (var i = 0; i < addressList.length; i++) {
if (!(addressList[i] is WalletAddressListItem)) continue;
if (addressList[i] is! WalletAddressListItem) {
continue;
}
final item = addressList[i] as WalletAddressListItem;
item.isHidden = wallet.walletAddresses.hiddenAddresses.contains(item.address) ||
(isElectrumWallet && item.isLegacyDerivation);
}
for (var i = 0; i < addressList.length; i++) {
if (!(addressList[i] is WalletAddressListItem)) continue;
if (addressList[i] is! WalletAddressListItem) {
continue;
}
(addressList[i] as WalletAddressListItem).isManual = wallet.walletAddresses.manualAddresses
.contains((addressList[i] as WalletAddressListItem).address);
}
@@ -395,25 +425,25 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
}
if (searchText.isNotEmpty) {
return ObservableList.of(addressList.where((item) {
if (item is WalletAddressListItem) {
return item.address.toLowerCase().contains(searchText.toLowerCase());
}
return false;
}));
return ObservableList.of(
addressList.where((item) {
if (item is WalletAddressListItem) {
return item.address.toLowerCase().contains(searchText.toLowerCase());
}
return false;
}),
);
}
return addressList;
}
@computed
ObservableList<ListItem> get addressList {
return _computeAddressList();
}
ObservableList<ListItem> get addressList => _computeAddressList();
List<ListItem> get forceRecomputeItems {
// necessary because the addressList contains non-observable items
List<ListItem> recomputed = [];
final recomputed = <ListItem>[];
recomputed.addAll(_baseItems);
recomputed.addAll(_computeAddressList());
return recomputed;
@@ -451,9 +481,9 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
case WalletType.wownero:
wownero!.getCurrentAccount(wallet).label;
default:
return '';
return "";
}
return '';
return "";
}
@computed
@@ -473,7 +503,7 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
WalletType.litecoin,
WalletType.decred,
WalletType.dogecoin,
WalletType.zcash
WalletType.zcash,
].contains(wallet.type) &&
!isLightning &&
isZCashTransparent;
@@ -486,7 +516,7 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
WalletType.bitcoin,
WalletType.litecoin,
WalletType.bitcoinCash,
WalletType.dogecoin
WalletType.dogecoin,
].contains(wallet.type);
List<String> getWalletImages(int? chainId) {
@@ -494,44 +524,44 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
switch (chainId) {
case 1:
return [
'assets/new-ui/crypto_full_icons/ethereum.svg',
'assets/images/usdc_icon.svg',
'assets/images/usdt_wallet_icon.svg',
'assets/images/deuro_icon.svg',
'assets/images/more_tokens.svg',
"assets/new-ui/crypto_full_icons/ethereum.svg",
"assets/images/usdc_icon.svg",
"assets/images/usdt_wallet_icon.svg",
"assets/images/deuro_icon.svg",
"assets/images/more_tokens.svg",
];
case 137:
return [
'assets/new-ui/crypto_full_icons/polygon.svg',
'assets/images/eth_pol_icon.svg',
'assets/images/usdc_icon.svg',
'assets/images/usdt_wallet_icon.svg',
'assets/images/more_tokens.svg',
"assets/new-ui/crypto_full_icons/polygon.svg",
"assets/images/eth_pol_icon.svg",
"assets/images/usdc_icon.svg",
"assets/images/usdt_wallet_icon.svg",
"assets/images/more_tokens.svg",
];
case 8453:
return [
'assets/new-ui/crypto_full_icons/ethereum.svg',
'assets/images/usdc_icon.svg',
'assets/images/more_tokens.svg',
"assets/new-ui/crypto_full_icons/ethereum.svg",
"assets/images/usdc_icon.svg",
"assets/images/more_tokens.svg",
];
case 42161:
return [
'assets/new-ui/crypto_full_icons/arbitrum.svg',
'assets/images/usdc_icon.svg',
'assets/images/more_tokens.svg',
"assets/new-ui/crypto_full_icons/arbitrum.svg",
"assets/images/usdc_icon.svg",
"assets/images/more_tokens.svg",
];
case 56:
return [
'assets/new-ui/crypto_full_icons/bnb.svg',
'assets/images/usdc_icon.svg',
'assets/images/usdt_wallet_icon.svg',
'assets/images/more_tokens.svg',
"assets/new-ui/crypto_full_icons/bnb.svg",
"assets/images/usdc_icon.svg",
"assets/images/usdt_wallet_icon.svg",
"assets/images/more_tokens.svg",
];
default:
return [
'assets/new-ui/crypto_full_icons/ethereum.svg',
'assets/images/usdc_icon.svg',
'assets/images/usdt_wallet_icon.svg',
"assets/new-ui/crypto_full_icons/ethereum.svg",
"assets/images/usdc_icon.svg",
"assets/images/usdt_wallet_icon.svg",
];
}
}
@@ -539,22 +569,22 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
switch (wallet.type) {
case WalletType.solana:
return [
'assets/images/sol_icon.svg',
'assets/images/usdc_icon.svg',
'assets/images/usdt_wallet_icon.svg',
'assets/images/more_tokens.svg',
"assets/images/sol_icon.svg",
"assets/images/usdc_icon.svg",
"assets/images/usdt_wallet_icon.svg",
"assets/images/more_tokens.svg",
];
case WalletType.tron:
return [
'assets/images/trx_icon.svg',
'assets/images/usdc_icon.svg',
'assets/images/usdt_wallet_icon.svg',
'assets/images/more_tokens.svg',
"assets/images/trx_icon.svg",
"assets/images/usdc_icon.svg",
"assets/images/usdt_wallet_icon.svg",
"assets/images/more_tokens.svg",
];
case WalletType.zano:
return [
'assets/images/zano_icon.svg',
'assets/images/more_tokens.svg',
"assets/images/zano_icon.svg",
"assets/images/more_tokens.svg",
];
default:
return [];
@@ -563,7 +593,9 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
@computed
String get qrImage {
if (isLightning) return 'assets/images/btc_chain_qr_lightning.svg';
if (isLightning) {
return "assets/images/btc_chain_qr_lightning.svg";
}
return getQrImage(type);
}
@@ -582,8 +614,7 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
@computed
bool get isLightning =>
wallet.type == WalletType.bitcoin &&
(wallet.walletAddresses.getPaymentUri(_amount) is LightningPaymentRequest);
wallet.type == WalletType.bitcoin && selectedCurrency == CryptoCurrency.btcln;
@computed
bool get isZCashTransparent {
@@ -698,16 +729,14 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
@action
void changeAmount(String amount) {
if (selectedCurrency is FiatCurrency) {
this._amount = amount;
_amount = amount;
_convertAmountToCrypto();
} else if (selectedCurrency is CryptoCurrency) {
this._amount = _appStore.amountParsingProxy
_amount = _appStore.amountParsingProxy
.getCanonicalCryptoAmount(amount, selectedCurrency as CryptoCurrency);
}
if (isLightning) {
wallet.walletAddresses
.getPaymentRequestUri(this._amount)
.then((uri) => _lnPaymentRequest = uri);
wallet.walletAddresses.getPaymentRequestUri(_amount).then((uri) => _lnPaymentRequest = uri);
}
}
@@ -719,22 +748,24 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
@action
void _convertAmountToCrypto() {
var cryptoCurrency = tokenCurrency ?? wallet.currency;
if (cryptoCurrency == CryptoCurrency.btcln) cryptoCurrency = CryptoCurrency.btc;
if (cryptoCurrency == CryptoCurrency.btcln) {
cryptoCurrency = CryptoCurrency.btc;
}
final fiatRate = _fiatRate ?? (fiatConversionStore.prices[cryptoCurrency] ?? 0.0);
if (fiatRate <= 0.0) {
dev.log("Invalid Fiat Rate $fiatRate");
_amount = '';
_amount = "";
return;
}
try {
final crypto = (double.parse(_amount.replaceAll(',', '.')) / fiatRate).toStringAsFixed(8);
final crypto = (double.parse(_amount.replaceAll(",", ".")) / fiatRate).toStringAsFixed(8);
if (_amount != crypto) {
_amount = crypto;
}
} catch (e) {
_amount = '';
_amount = "";
}
}
@@ -6,13 +6,13 @@ import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/monero/monero.dart';
import 'package:cake_wallet/store/app_store.dart';
import 'package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart';
import 'package:cake_wallet/view_model/hardware_wallet/ledger_view_model.dart';
import 'package:cake_wallet/view_model/seed_settings_view_model.dart';
import 'package:cake_wallet/view_model/wallet_creation_vm.dart';
import 'package:cw_core/hardware/hardware_account_data.dart';
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_type.dart';
import 'package:mobx/mobx.dart';
@@ -43,6 +43,8 @@ abstract class WalletHardwareRestoreViewModelBase extends WalletCreationVM with
@observable
String? error = null;
bool get passphraseAvailable => hardwareWalletVM.hardwareWalletType == HardwareWalletType.trezor;
// @observable
ObservableList<HardwareAccountData> availableAccounts = ObservableList();
@@ -87,6 +89,7 @@ abstract class WalletHardwareRestoreViewModelBase extends WalletCreationVM with
hardwareWalletService: hardwareWalletVM.getHardwareWalletService(type),
password: password,
height: _options['height'] as int? ?? 0,
passphrase: _options['passphrase'] as String?,
);
default:
throw Exception('Unexpected type: ${type.toString()}');
@@ -108,6 +108,10 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store {
@observable
WalletRestoreMode mode;
@computed
bool get passphraseAvailable =>
mode == WalletRestoreMode.seed || hardwareWalletType == HardwareWalletType.trezor;
@observable
bool hasPassphrase;
+2 -8
View File
@@ -209,16 +209,10 @@ class CWZcash extends Zcash {
}
@override
bool showMissingFundsCard(WalletBase wallet) {
final zcashWallet = wallet as ZcashWallet;
return zcashWallet.couldBeZashiWallet();
}
bool showMissingFundsCard(WalletBase wallet) => false;
@override
Future<void> rescanInternalChange(WalletBase wallet) {
final zcashWallet = wallet as ZcashWallet;
return zcashWallet.rescanInternalChange();
}
Future<void> rescanInternalChange(WalletBase wallet) async {}
}
const wordList = [
-2
View File
@@ -268,8 +268,6 @@ flutter:
- assets/text/
- assets/faq/
- assets/animation/
- scripts/zcash_lib/assets/sapling-spend.params
- scripts/zcash_lib/assets/sapling-output.params
- assets/new-ui/
- assets/new-ui/icons/
- assets/new-ui/hero/
+6
View File
@@ -14,6 +14,7 @@
"activeConnectionsPrompt": "Active connections will appear here",
"add": "Add",
"add_a_note": "Add a note",
"add_a_passphrase": "Add a passphrase",
"add_account": "Add Account",
"add_contact": "Add contact",
"add_contact_to_address_book": "Would you like to add this contact to your address book?",
@@ -402,6 +403,7 @@
"enter_code": "Enter code",
"enter_greater_than": "Please enter an amount greater than",
"enter_less_than": "Please enter an amount less than",
"enter_on_device_instead": "Enter on device instead",
"enter_recipient_address": "Please enter recipient address",
"enter_seed_phrase": "Enter your seed phrase",
"enter_totp_code": "Please enter the TOTP Code.",
@@ -745,6 +747,8 @@
"pairing_error": "Pairing Error",
"pairingInvalidEvent": "Pairing Invalid Event",
"passphrase": "Passphrase (Optional)",
"passphrase_raw": "Passphrase",
"passphrase_entry": "Passphrase entry",
"passphrase_view_keys": "Passphrase",
"passphrases_doesnt_match": "Passphrases do not match, please try again",
"password": "Password",
@@ -1071,6 +1075,8 @@
"share_this_node": "Share this node",
"shared_seed_wallet_groups": "Shared Seed Wallet Groups",
"shared_seed_wallets": "Shared Seed Wallets",
"share_this_node": "Share this node",
"shielding": "Autoshield",
"show": "Show",
"show_address_book_popup": "Show Address Book popup",
"show_balance": "Long Press to Show Balance",
+1 -1
View File
@@ -455,7 +455,7 @@ abstract class Monero {
HardwareWalletType? hardwareWalletType,
required int height});
WalletCredentials createMoneroRestoreWalletFromSeedCredentials({required String name, required String password, required String passphrase, required int height, required String mnemonic});
WalletCredentials createMoneroRestoreWalletFromHardwareCredentials({required String name, required String password, required int height, required HardwareWalletService hardwareWalletService});
WalletCredentials createMoneroRestoreWalletFromHardwareCredentials({required String name, required String password, required int height, required HardwareWalletService hardwareWalletService, required String? passphrase});
WalletCredentials createMoneroNewWalletCredentials({required String name, required String language, required int seedType, required String? passphrase, String? password, String? mnemonic});
Map<String, String> getKeys(Object wallet);
int? getRestoreHeight(Object wallet);