zec ironwood (#3425)

* initial * wip * ironwood detection, migration * dev: zec address validation on rt * fix: ironwood spend * ironwood regressions * fix: tx history * bump: zkool2 * fix: tx amounts on pending fix: ironwood pending balance fix: orchard->ironwood migration fix: orchard->any pre ironwood txs fix: ironwood -> any txs fix: WrongSpendAuthorizingKey fix: tx history fix: autoshield to ironwood fix: zkool to latest version break: my sleep schedule :sweating: :worksonmymachine: * thx fable * downgrade frb to 2.11.1 * fix: tx amounts in history * Migating... / migration label for O->I txs

cyan committed Jul 27, 2026 at 10:20 UTC 92c2c5e043b60fc437ea9c749230c76b0fbdab9e
38 files changed +1067 -206
cw_core/lib/wallet_info.dart
+5 -2
@@ -586,11 +586,12 @@ class WalletInfo {
586 "addressPageType": addressPageType,
587 "receiveInfoboxDismissed": receiveInfoboxDismissed ? 1 : 0,
588 "showCombinedBalance": showCombinedBalance ? 1 : 0,
589 - "favoriteTokenAddress": favoriteTokenAddress
589 + "favoriteTokenAddress": favoriteTokenAddress,
590 + "network": network,
591 };
592
593 factory WalletInfo.fromJson(Map<String, dynamic> json) {
593 - return WalletInfo(
594 + final info = WalletInfo(
595 json[selfIdColumn] as int,
596 json['id'] as String,
597 json['name'] as String,
@@ -616,6 +617,8 @@ class WalletInfo {
617 json['receiveInfoboxDismissed'] != 0,
618 json["showCombinedBalance"] != 0,
619 json["favoriteTokenAddress"] as String? ?? null);
620 + info.network = json['network'] as String?;
621 + return info;
622 }
623
624 Future<int> save() async {
cw_core/lib/wallet_info_legacy.dart
+2 -1
@@ -274,7 +274,8 @@ class WalletInfo extends HiveObject {
274 addressPageType,
275 false,
276 true,
277 - null);
277 + null)
278 + ..network = network;
279 final wiId = await walletInfo.save();
280 for (final address in usedAddresses ?? <String>[]) {
281 await newWi.WalletInfoAddress.insert(wiId, newWi.WalletInfoAddressType.used, address);
cw_custom_lints/pubspec.lock new
+429
@@ -0,0 +1,429 @@
1 +# Generated by pub
2 +# See https://dart.dev/tools/pub/glossary#lockfile
3 +packages:
4 + _fe_analyzer_shared:
5 + dependency: transitive
6 + description:
7 + name: _fe_analyzer_shared
8 + sha256: cd6add6f846f35fb79f3c315296703c1a24f3cfd7f4739d91a74961c1c7e9f1b
9 + url: "https://pub.dev"
10 + source: hosted
11 + version: "100.0.0"
12 + analysis_server_plugin:
13 + dependency: "direct main"
14 + description:
15 + name: analysis_server_plugin
16 + sha256: c4021359e7a673ecd9c6db5f11fea12fabb55e0ba96fa08c78920ae81936f1d1
17 + url: "https://pub.dev"
18 + source: hosted
19 + version: "0.3.15"
20 + analyzer:
21 + dependency: "direct main"
22 + description:
23 + name: analyzer
24 + sha256: "6ba98576948803398b69e3a444df24eacdbe12ed699c7014e120ea38552debbf"
25 + url: "https://pub.dev"
26 + source: hosted
27 + version: "13.0.0"
28 + analyzer_plugin:
29 + dependency: "direct main"
30 + description:
31 + name: analyzer_plugin
32 + sha256: "9a81dc36f7f71890d7cf83960a3a7df1e6c9a24035f98eb848e91ad48b1d21c6"
33 + url: "https://pub.dev"
34 + source: hosted
35 + version: "0.14.9"
36 + analyzer_testing:
37 + dependency: "direct dev"
38 + description:
39 + name: analyzer_testing
40 + sha256: da27ae9bd4bf3ce4ebab6295cf922ca21a2b14ea8fcacc859342aca796a48a2d
41 + url: "https://pub.dev"
42 + source: hosted
43 + version: "0.2.6"
44 + args:
45 + dependency: transitive
46 + description:
47 + name: args
48 + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
49 + url: "https://pub.dev"
50 + source: hosted
51 + version: "2.7.0"
52 + async:
53 + dependency: transitive
54 + description:
55 + name: async
56 + sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
57 + url: "https://pub.dev"
58 + source: hosted
59 + version: "2.13.1"
60 + boolean_selector:
61 + dependency: transitive
62 + description:
63 + name: boolean_selector
64 + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
65 + url: "https://pub.dev"
66 + source: hosted
67 + version: "2.1.2"
68 + cli_config:
69 + dependency: transitive
70 + description:
71 + name: cli_config
72 + sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec
73 + url: "https://pub.dev"
74 + source: hosted
75 + version: "0.2.0"
76 + collection:
77 + dependency: transitive
78 + description:
79 + name: collection
80 + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
81 + url: "https://pub.dev"
82 + source: hosted
83 + version: "1.19.1"
84 + convert:
85 + dependency: transitive
86 + description:
87 + name: convert
88 + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
89 + url: "https://pub.dev"
90 + source: hosted
91 + version: "3.1.2"
92 + coverage:
93 + dependency: transitive
94 + description:
95 + name: coverage
96 + sha256: "956a3de0725ca232ad353565a8290d3357592bf4250f6f298a185e2d949c5d3d"
97 + url: "https://pub.dev"
98 + source: hosted
99 + version: "1.15.1"
100 + crypto:
101 + dependency: transitive
102 + description:
103 + name: crypto
104 + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
105 + url: "https://pub.dev"
106 + source: hosted
107 + version: "3.0.7"
108 + dart_style:
109 + dependency: transitive
110 + description:
111 + name: dart_style
112 + sha256: "59d53ef8eaed9d288ed9767618e2b31c4fa0383a127db59d5eb2e737a7638a60"
113 + url: "https://pub.dev"
114 + source: hosted
115 + version: "3.1.9"
116 + file:
117 + dependency: transitive
118 + description:
119 + name: file
120 + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
121 + url: "https://pub.dev"
122 + source: hosted
123 + version: "7.0.1"
124 + frontend_server_client:
125 + dependency: transitive
126 + description:
127 + name: frontend_server_client
128 + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
129 + url: "https://pub.dev"
130 + source: hosted
131 + version: "4.0.0"
132 + glob:
133 + dependency: transitive
134 + description:
135 + name: glob
136 + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
137 + url: "https://pub.dev"
138 + source: hosted
139 + version: "2.1.3"
140 + http_multi_server:
141 + dependency: transitive
142 + description:
143 + name: http_multi_server
144 + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8
145 + url: "https://pub.dev"
146 + source: hosted
147 + version: "3.2.2"
148 + http_parser:
149 + dependency: transitive
150 + description:
151 + name: http_parser
152 + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
153 + url: "https://pub.dev"
154 + source: hosted
155 + version: "4.1.2"
156 + io:
157 + dependency: transitive
158 + description:
159 + name: io
160 + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b
161 + url: "https://pub.dev"
162 + source: hosted
163 + version: "1.0.5"
164 + logging:
165 + dependency: transitive
166 + description:
167 + name: logging
168 + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
169 + url: "https://pub.dev"
170 + source: hosted
171 + version: "1.3.0"
172 + matcher:
173 + dependency: transitive
174 + description:
175 + name: matcher
176 + sha256: "31bd099b47c10cd1aeb55146a2d46ce0277630ecef3f7dae54ad7873f36696cd"
177 + url: "https://pub.dev"
178 + source: hosted
179 + version: "0.12.20"
180 + meta:
181 + dependency: transitive
182 + description:
183 + name: meta
184 + sha256: "307249ce4ff29d58a18e97f6345f539382eb9c9c29ecda628900f31de0443dd9"
185 + url: "https://pub.dev"
186 + source: hosted
187 + version: "1.19.0"
188 + mime:
189 + dependency: transitive
190 + description:
191 + name: mime
192 + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
193 + url: "https://pub.dev"
194 + source: hosted
195 + version: "2.0.0"
196 + node_preamble:
197 + dependency: transitive
198 + description:
199 + name: node_preamble
200 + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db"
201 + url: "https://pub.dev"
202 + source: hosted
203 + version: "2.0.2"
204 + package_config:
205 + dependency: transitive
206 + description:
207 + name: package_config
208 + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
209 + url: "https://pub.dev"
210 + source: hosted
211 + version: "2.2.0"
212 + path:
213 + dependency: transitive
214 + description:
215 + name: path
216 + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
217 + url: "https://pub.dev"
218 + source: hosted
219 + version: "1.9.1"
220 + pool:
221 + dependency: transitive
222 + description:
223 + name: pool
224 + sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d"
225 + url: "https://pub.dev"
226 + source: hosted
227 + version: "1.5.2"
228 + pub_semver:
229 + dependency: transitive
230 + description:
231 + name: pub_semver
232 + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
233 + url: "https://pub.dev"
234 + source: hosted
235 + version: "2.2.0"
236 + shelf:
237 + dependency: transitive
238 + description:
239 + name: shelf
240 + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
241 + url: "https://pub.dev"
242 + source: hosted
243 + version: "1.4.2"
244 + shelf_packages_handler:
245 + dependency: transitive
246 + description:
247 + name: shelf_packages_handler
248 + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e"
249 + url: "https://pub.dev"
250 + source: hosted
251 + version: "3.0.2"
252 + shelf_static:
253 + dependency: transitive
254 + description:
255 + name: shelf_static
256 + sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3
257 + url: "https://pub.dev"
258 + source: hosted
259 + version: "1.1.3"
260 + shelf_web_socket:
261 + dependency: transitive
262 + description:
263 + name: shelf_web_socket
264 + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925"
265 + url: "https://pub.dev"
266 + source: hosted
267 + version: "3.0.0"
268 + source_map_stack_trace:
269 + dependency: transitive
270 + description:
271 + name: source_map_stack_trace
272 + sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b
273 + url: "https://pub.dev"
274 + source: hosted
275 + version: "2.1.2"
276 + source_maps:
277 + dependency: transitive
278 + description:
279 + name: source_maps
280 + sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812"
281 + url: "https://pub.dev"
282 + source: hosted
283 + version: "0.10.13"
284 + source_span:
285 + dependency: transitive
286 + description:
287 + name: source_span
288 + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
289 + url: "https://pub.dev"
290 + source: hosted
291 + version: "1.10.2"
292 + stack_trace:
293 + dependency: transitive
294 + description:
295 + name: stack_trace
296 + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
297 + url: "https://pub.dev"
298 + source: hosted
299 + version: "1.12.1"
300 + stream_channel:
301 + dependency: transitive
302 + description:
303 + name: stream_channel
304 + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
305 + url: "https://pub.dev"
306 + source: hosted
307 + version: "2.1.4"
308 + string_scanner:
309 + dependency: transitive
310 + description:
311 + name: string_scanner
312 + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
313 + url: "https://pub.dev"
314 + source: hosted
315 + version: "1.4.1"
316 + term_glyph:
317 + dependency: transitive
318 + description:
319 + name: term_glyph
320 + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
321 + url: "https://pub.dev"
322 + source: hosted
323 + version: "1.2.2"
324 + test:
325 + dependency: "direct dev"
326 + description:
327 + name: test
328 + sha256: "0d5ba5602ec3baa28c8ce365e1efc5575969c765f45c554a3e167dc7945b9c30"
329 + url: "https://pub.dev"
330 + source: hosted
331 + version: "1.31.2"
332 + test_api:
333 + dependency: transitive
334 + description:
335 + name: test_api
336 + sha256: "475610b2aa23c19687cce2961e44b0cc57cafe220f67c2b80201231b2a07fbe7"
337 + url: "https://pub.dev"
338 + source: hosted
339 + version: "0.7.13"
340 + test_core:
341 + dependency: transitive
342 + description:
343 + name: test_core
344 + sha256: a39c204a4fc7a7ccb04a2b985e359fda3cc37e45e0b8ac61c3fb1a05aa832132
345 + url: "https://pub.dev"
346 + source: hosted
347 + version: "0.6.19"
348 + test_reflective_loader:
349 + dependency: "direct dev"
350 + description:
351 + name: test_reflective_loader
352 + sha256: d828d5ca15179aaac2aaf8f510cf0a52ec28e0031681b044ec5e581a4b8002e7
353 + url: "https://pub.dev"
354 + source: hosted
355 + version: "0.4.0"
356 + typed_data:
357 + dependency: transitive
358 + description:
359 + name: typed_data
360 + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
361 + url: "https://pub.dev"
362 + source: hosted
363 + version: "1.4.0"
364 + vm_service:
365 + dependency: transitive
366 + description:
367 + name: vm_service
368 + sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360"
369 + url: "https://pub.dev"
370 + source: hosted
371 + version: "15.2.0"
372 + watcher:
373 + dependency: transitive
374 + description:
375 + name: watcher
376 + sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635"
377 + url: "https://pub.dev"
378 + source: hosted
379 + version: "1.2.1"
380 + web:
381 + dependency: transitive
382 + description:
383 + name: web
384 + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
385 + url: "https://pub.dev"
386 + source: hosted
387 + version: "1.1.1"
388 + web_socket:
389 + dependency: transitive
390 + description:
391 + name: web_socket
392 + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c"
393 + url: "https://pub.dev"
394 + source: hosted
395 + version: "1.0.1"
396 + web_socket_channel:
397 + dependency: transitive
398 + description:
399 + name: web_socket_channel
400 + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8
401 + url: "https://pub.dev"
402 + source: hosted
403 + version: "3.0.3"
404 + webkit_inspection_protocol:
405 + dependency: transitive
406 + description:
407 + name: webkit_inspection_protocol
408 + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572"
409 + url: "https://pub.dev"
410 + source: hosted
411 + version: "1.2.1"
412 + yaml:
413 + dependency: transitive
414 + description:
415 + name: yaml
416 + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
417 + url: "https://pub.dev"
418 + source: hosted
419 + version: "3.1.3"
420 + yaml_edit:
421 + dependency: transitive
422 + description:
423 + name: yaml_edit
424 + sha256: "07c9e63ba42519745182b88ca12264a7ba2484d8239958778dfe4d44fe760488"
425 + url: "https://pub.dev"
426 + source: hosted
427 + version: "2.2.4"
428 +sdks:
429 + dart: ">=3.11.0 <4.0.0"
cw_zcash/lib/cw_zcash.dart
+2
@@ -11,4 +11,6 @@ export 'src/zcash_wallet_service.dart';
11 export 'src/models.dart';
12 export 'src/zcash_transaction_credentials.dart';
13 export 'src/pending_zcash_transaction.dart';
14 +export 'src/zcash_network.dart';
15 export 'src/zcash_receive_page_options.dart';
16 +export 'src/zcash_wallet_addresses.dart';
cw_zcash/lib/src/models.dart
+6
@@ -7,6 +7,7 @@ class ZcashNewWalletCredentials extends WalletCredentials {
7 required final String? passphrase,
8 final String? mnemonic,
9 final int? seedPhraseLength,
10 + this.network = 0,
11 }) : super(
12 name: name,
13 password: password,
@@ -17,6 +18,7 @@ class ZcashNewWalletCredentials extends WalletCredentials {
18 }
19
20 String? mnemonic;
21 + int network;
22 }
23
24 class ZcashFromSeedWalletCredentials extends WalletCredentials {
@@ -26,8 +28,10 @@ class ZcashFromSeedWalletCredentials extends WalletCredentials {
28 required final String? passphrase,
29 required this.seed,
30 required super.height,
31 + this.network = 0,
32 }) : super(name: name, password: password, passphrase: passphrase);
33 final String? seed;
34 + int network;
35 }
36
37 class ZcashFromKeysWalletCredentials extends WalletCredentials {
@@ -36,6 +40,8 @@ class ZcashFromKeysWalletCredentials extends WalletCredentials {
40 final String? password,
41 required final int? height,
42 required this.privateKey,
43 + this.network = 0,
44 }) : super(name: name, password: password, height: height);
45 final String? privateKey;
46 + int network;
47 }
cw_zcash/lib/src/pending_zcash_transaction.dart
+1
@@ -63,6 +63,7 @@ class PendingZcashTransaction with PendingTransaction {
63 throw TransactionCommitFailed(errorMessage: result);
64 }
65 _txId = txId;
66 + zcashWallet.rememberPendingOutgoingAmount(txId, amount);
67 },
68 );
69 await zcashWallet.updateTransactions();
cw_zcash/lib/src/zcash_mempool.dart
+20 -3
@@ -21,8 +21,19 @@ class ZcashMempoolService {
21
22 bool _loopRunning = false;
23 String? _nodeUrl;
24 + int? _lastBlockHeight;
25 StreamSubscription<zkool_mempool.MempoolMsg>? _subscription;
26
27 + void removeTx(final String txId) {
28 + _txsById.remove(ZcashWalletService.normalizeTxId(txId));
29 + }
30 +
31 + void removeKnownTxs(final Iterable<String> txIds) {
32 + for (final txId in txIds) {
33 + removeTx(txId);
34 + }
35 + }
36 +
37 List<zkool_mempool.MempoolTx> txsForAccount(final int accountId) => _txsById.values
38 .where(
39 (final tx) =>
@@ -49,6 +60,7 @@ class ZcashMempoolService {
60 await _subscription?.cancel();
61 _subscription = null;
62 _txsById.clear();
63 + _lastBlockHeight = null;
64 try {
65 await _mempool?.cancel();
66 } catch (e) {
@@ -69,8 +81,8 @@ class ZcashMempoolService {
81 return;
82 }
83 switch (msg) {
72 - case zkool_mempool.MempoolMsg_BlockHeight():
73 - _onBlockMined();
84 + case zkool_mempool.MempoolMsg_BlockHeight(:final field0):
85 + _onBlockHeight(field0);
86 case zkool_mempool.MempoolMsg_TxId(:final field0):
87 _onMempoolTx(field0);
88 }
@@ -97,7 +109,12 @@ class ZcashMempoolService {
109 }
110 }
111
100 - void _onBlockMined() {
112 + void _onBlockHeight(final int height) {
113 + final heightChanged = _lastBlockHeight != null && height > _lastBlockHeight!;
114 + _lastBlockHeight = height;
115 + if (!heightChanged) {
116 + return;
117 + }
118 final affected = _allAffectedAccounts();
119 _txsById.clear();
120 onAccountsUpdated?.call(affected);
cw_zcash/lib/src/zcash_network.dart new
+49
@@ -0,0 +1,49 @@
1 +enum ZcashNetwork {
2 + mainnet,
3 + testnet,
4 + regtest;
5 +
6 + /// NU6.3 / Ironwood activation height on zkool regtest (see zkool2/misc/zebra.toml).
7 + static const int regtestNu63Height = 250;
8 +
9 + String get value => name;
10 +
11 + String get label => switch (this) {
12 + ZcashNetwork.mainnet => 'Mainnet',
13 + ZcashNetwork.testnet => 'Testnet',
14 + ZcashNetwork.regtest => 'Regtest',
15 + };
16 +
17 + String get dbFileName => switch (this) {
18 + ZcashNetwork.mainnet => 'zec.v2.db',
19 + ZcashNetwork.testnet => 'zec.testnet.v2.db',
20 + ZcashNetwork.regtest => 'zec.regtest.v2.db',
21 + };
22 +
23 + /// Default lightwalletd endpoint for dev networks.
24 + String get defaultNodeUri => switch (this) {
25 + ZcashNetwork.mainnet => 'zec.rocks:443',
26 + ZcashNetwork.testnet => 'testnet.lightwalletd.com:9067',
27 + ZcashNetwork.regtest => '10.0.2.2:9067', // 10.0.2.2 - AVD host bridge
28 + };
29 +
30 + bool get useSsl => this == ZcashNetwork.mainnet;
31 +
32 + static ZcashNetwork fromName(final String? name) => switch (name) {
33 + 'testnet' => ZcashNetwork.testnet,
34 + 'regtest' => ZcashNetwork.regtest,
35 + _ => ZcashNetwork.mainnet,
36 + };
37 +
38 + static ZcashNetwork fromIndex(final int index) => switch (index) {
39 + 1 => ZcashNetwork.testnet,
40 + 2 => ZcashNetwork.regtest,
41 + _ => ZcashNetwork.mainnet,
42 + };
43 +
44 + int get networkIndex => switch (this) {
45 + ZcashNetwork.testnet => 1,
46 + ZcashNetwork.regtest => 2,
47 + ZcashNetwork.mainnet => 0,
48 + };
49 +}
cw_zcash/lib/src/zcash_receive_page_options.dart
+22 -15
@@ -19,7 +19,7 @@ class ZcashReceivePageOption implements ReceivePageOption {
19 case ZcashAddressType.shieldedSapling:
20 return shieldedSapling;
21 case ZcashAddressType.shieldedOrchard:
22 - return shieldedOrchard;
22 + return shieldedOrchard();
23 case ZcashAddressType.unifiedType:
24 return unified;
25 }
@@ -57,13 +57,18 @@ class ZcashReceivePageOption implements ReceivePageOption {
57 description: "Sapling",
58 iconPath: "assets/new-ui/address-type-picker-icons/zec/sapling.svg",
59 );
60 - static const shieldedOrchard = ZcashReceivePageOption._(
61 - ZcashAddressType.shieldedOrchard,
62 - "Shielded",
63 - description: "Default (Orchard)",
64 - iconPath: "assets/new-ui/address-type-picker-icons/zec/shielded.svg",
65 - isCommon: true,
66 - );
60 + static const _shieldedOrchardIcon =
61 + "assets/new-ui/address-type-picker-icons/zec/shielded.svg";
62 +
63 + static ZcashReceivePageOption shieldedOrchard({final bool ironwood = false}) {
64 + return ZcashReceivePageOption._(
65 + ZcashAddressType.shieldedOrchard,
66 + "Shielded",
67 + description: ironwood ? "Default (Ironwood)" : "Default (Orchard)",
68 + iconPath: _shieldedOrchardIcon,
69 + isCommon: true,
70 + );
71 + }
72 static const unified = ZcashReceivePageOption._(
73 ZcashAddressType.unifiedType,
74 "Unified",
@@ -78,13 +83,15 @@ class ZcashReceivePageOption implements ReceivePageOption {
83 return value;
84 }
85
81 - static const all = [
82 - ZcashReceivePageOption.shieldedOrchard,
83 - ZcashReceivePageOption.shieldedSapling,
84 - ZcashReceivePageOption.unified,
85 - ZcashReceivePageOption.transparentRotated,
86 - ZcashReceivePageOption.transparent,
87 - ];
86 + static List<ZcashReceivePageOption> allOptionsFor({final bool ironwood = false}) => [
87 + shieldedOrchard(ironwood: ironwood),
88 + shieldedSapling,
89 + unified,
90 + transparentRotated,
91 + transparent,
92 + ];
93 +
94 + static List<ZcashReceivePageOption> get allOptions => allOptionsFor();
95
96 ZcashAddressType toType() {
97 return type;
cw_zcash/lib/src/zcash_taddress_rotation.dart
+27 -2
@@ -34,6 +34,7 @@ class ZcashTaddressRotation {
34 static bool _isStarted = false;
35 static zkool_coin.Coin get c => ZcashWalletBase.c;
36 static const int _sweepThreshold = 30000;
37 + static const int _minSpendableNote = 5000;
38 static const int _lookahead = 5;
39 // Matches transparentLimit in ZcashWalletBase._oneshotSync.
40 static const int _transparentSyncLimit = 100;
@@ -407,18 +408,42 @@ class ZcashTaddressRotation {
408 final result = await ZcashWalletBase.runWithCoin(
409 accountId: rotationAccount,
410 func: (final coin) async {
411 + final height = await zkool_network.getCurrentHeight(c: coin);
412 + final notes = await zkool_account.listNotes(c: coin);
413 + var amount = BigInt.zero;
414 + for (final note in notes) {
415 + if (note.pool != NotePool.transparent.index || note.locked) {
416 + continue;
417 + }
418 + if (note.value < BigInt.from(_minSpendableNote)) {
419 + continue;
420 + }
421 + if (note.height > height) {
422 + continue;
423 + }
424 + amount += note.value;
425 + }
426 + if (amount < BigInt.from(_sweepThreshold)) {
427 + throw Exception('rotation sweep: insufficient spendable transparent notes');
428 + }
429 + final ironwood = await zkool_network.isIronwoodActive(c: coin);
430 final tx = await zkool_pay.prepare(
431 recipients: [
412 - zkool_paydart.Recipient(assetBase: zecBase, address: toAddress, amount: transparentBal),
432 + zkool_paydart.Recipient(
433 + assetBase: zecBase,
434 + address: toAddress,
435 + amount: amount,
436 + pools: ironwood ? ironwoodPoolMask : null,
437 + ),
438 ],
439 options: zkool_pay.PaymentOptions(
440 srcPools: 1,
441 recipientPaysFee: true,
442 smartTransparent: false,
443 + mode: 0,
444 ),
445 c: coin,
446 );
421 - final height = await zkool_network.getCurrentHeight(c: coin);
447 final signTx = await zkool_pay.signTransaction(pczt: tx, c: coin);
448 final txBytes = await zkool_pay.extractTransaction(package: signTx);
449 return zkool_pay.broadcastTransaction(height: height, txBytes: txBytes, c: coin);
cw_zcash/lib/src/zcash_transaction_info.dart
+2
@@ -20,6 +20,7 @@ class ZcashTransactionInfo extends TransactionInfo {
20 final TxType? txType,
21 final bool isRotationReceive = false,
22 final bool isShieldAction = false,
23 + final bool isIronwoodMigration = false,
24 }) {
25 this.id = id;
26 this.amount = amount;
@@ -38,6 +39,7 @@ class ZcashTransactionInfo extends TransactionInfo {
39 }
40 additionalInfo['isRotationReceive'] = isRotationReceive;
41 additionalInfo['isAutoShield'] = isShieldAction || ZcashWalletService.isAutoshieldTx(txHash);
42 + additionalInfo['isIronwoodMigration'] = isIronwoodMigration;
43
44 if (additionalInfo['isAutoShield'] == true) {
45 additionalInfo['memo'] ??= '';
cw_zcash/lib/src/zcash_wallet.dart
+282 -66
@@ -23,6 +23,7 @@ import 'package:cw_zcash/src/util/crc32.dart';
23 import 'package:cw_zcash/src/zcash_mempool.dart';
24 import 'package:cw_zcash/src/zcash_taddress_rotation.dart';
25 import 'package:cw_zcash/src/zcash_wallet_addresses.dart';
26 +import 'package:cw_zcash/src/zcash_network.dart';
27 import 'package:cw_zcash/src/zkool_compat.dart';
28 import 'package:cw_zcash/src/zkooltx.dart';
29 import 'package:mobx/mobx.dart';
@@ -32,6 +33,7 @@ import 'package:zkool/src/rust/api/coin.dart' as zkool_coin;
33 import 'package:zkool/src/rust/api/mempool.dart' as zkool_mempool;
34 import 'package:zkool/src/rust/api/sync.dart' as zkool_sync;
35 import 'package:zkool/src/rust/api/pay.dart' as zkool_pay;
36 +import 'package:zkool/src/rust/api/migrate.dart' as zkool_migrate;
37 import 'package:zkool/src/rust/api/network.dart' as zkool_network;
38 import 'package:zkool/src/rust/pay.dart' as zkool_paydart;
39 import 'package:zkool/src/rust/frb_generated.dart' as zkool_frb;
@@ -61,6 +63,15 @@ abstract class ZcashWalletBase
63
64 int accountId;
65
66 + final Map<String, BigInt> _pendingOutgoingAmounts = {};
67 +
68 + void rememberPendingOutgoingAmount(final String txId, final Money amount) {
69 + if (amount.isZero) {
70 + return;
71 + }
72 + _pendingOutgoingAmounts[ZcashWalletService.normalizeTxId(txId)] = amount.amount;
73 + }
74 +
75 @override
76 @observable
77 SyncStatus syncStatus = NotConnectedSyncStatus();
@@ -147,6 +158,7 @@ abstract class ZcashWalletBase
158 c = c.setLwd(url: lwdUrl, serverType: 0);
159 syncStatus = ConnectedSyncStatus();
160 unawaited(ZcashMempoolService.instance.ensureRunning(c));
161 + unawaited(_updateIronwoodActive());
162 _ensureSyncLoopRunning();
163 unawaited(_refreshSyncStatus());
164 unawaited(_oneshotSync());
@@ -463,12 +475,14 @@ abstract class ZcashWalletBase
475 return await runWithCoin(
476 accountId: accountId,
477 func: (coin) async {
478 + final ironwood = await zkool_network.isIronwoodActive(c: coin);
479 final txPlan = await zkool_pay.prepare(
480 recipients: recipients,
481 options: zkool_pay.PaymentOptions(
469 - srcPools: 7,
482 + srcPools: ironwood ? 15 : 7,
483 recipientPaysFee: receipientPaysFee,
484 smartTransparent: false,
485 + mode: 0,
486 ),
487 c: coin,
488 );
@@ -502,18 +516,57 @@ abstract class ZcashWalletBase
516
517 static const _dispPhrase = "Received to disposable address";
518
519 + bool _hasExternalOutputs(final ZkoolTx tx) =>
520 + tx.outputsWithAddress.any((final o) => !_addressBelongsToWallet(o.address));
521 +
522 + bool _isIronwoodMigrationTx(final ZkoolTx tx) {
523 + // zkool classifies migration as selfTransfer with fee-sized net value.
524 + if (tx.type != TxType.selfTransfer) {
525 + return false;
526 + }
527 + final orchardSpent = tx.orchardSpent;
528 + final spentFromOrchard = orchardSpent > BigInt.zero ||
529 + tx.spendPools.contains(NotePool.orchard.index);
530 + if (!spentFromOrchard) {
531 + return false;
532 + }
533 + if (tx.ironwoodReceived > BigInt.zero ||
534 + tx.notePools.contains(NotePool.ironwood.index)) {
535 + return true;
536 + }
537 + // Orchard → Orchard split step (SD notes created, all outputs are ours).
538 + if (tx.orchardReceived > BigInt.zero && !_hasExternalOutputs(tx)) {
539 + return true;
540 + }
541 + // Orchard → Ironwood before the IW note is attached to tx details.
542 + return orchardSpent > tx.value;
543 + }
544 +
545 + BigInt _migrationDisplayAmount(final ZkoolTx tx) {
546 + if (tx.ironwoodReceived > BigInt.zero) {
547 + return tx.ironwoodReceived;
548 + }
549 + if (tx.orchardReceived > BigInt.zero) {
550 + return tx.orchardReceived;
551 + }
552 + return tx.orchardSpent;
553 + }
554 +
555 ZcashTransactionInfo _zcashInfoFromMempoolTx(
556 final zkool_mempool.MempoolTx tx,
557 final int accountId,
558 ) {
559 final accountNotes = tx.notes.where((final n) => n.account == accountId);
560 + final txHash = ZcashWalletService.normalizeTxId(tx.txid);
561 + final pendingAmount = _pendingOutgoingAmounts[txHash];
562 final netValue = accountNotes.fold<BigInt>(
563 BigInt.zero,
564 (final sum, final note) => sum + BigInt.from(note.value),
565 );
514 - final direction = netValue >= BigInt.zero
515 - ? TransactionDirection.incoming
516 - : TransactionDirection.outgoing;
566 + final direction = pendingAmount != null || netValue < BigInt.zero
567 + ? TransactionDirection.outgoing
568 + : TransactionDirection.incoming;
569 + final displayAmount = pendingAmount ?? netValue.abs();
570 final memo = accountNotes
571 .map((final n) => n.memo)
572 .whereType<String>()
@@ -525,8 +578,8 @@ abstract class ZcashWalletBase
578 );
579
580 final info = ZcashTransactionInfo(
528 - id: ZcashWalletService.normalizeTxId(tx.txid),
529 - amount: Money(netValue.abs(), currency),
581 + id: txHash,
582 + amount: Money(displayAmount, currency),
583 fee: Money.zero(currency),
584 direction: direction,
585 isPending: true,
@@ -555,11 +608,15 @@ abstract class ZcashWalletBase
608 ? currentHeight - tx.height + 1
609 : 0;
610 final memo = extraMemo != null ? "${tx.memo ?? ''}\n$extraMemo".trim() : tx.memo;
558 - final direction = directionOverride ?? tx.direction;
611 + final isMigration = directionOverride == null && _isIronwoodMigrationTx(tx);
612 + final direction = directionOverride ??
613 + (isMigration ? TransactionDirection.outgoing : tx.direction);
614 final recipientAddresses = _recipientAddresses(_paymentOutputAddresses(tx), direction);
615 + final amount = amountOverride ??
616 + (isMigration ? _migrationDisplayAmount(tx) : tx.value);
617 final info = ZcashTransactionInfo(
618 id: tx.txHash,
562 - amount: Money(amountOverride ?? tx.value, currency),
619 + amount: Money(amount, currency),
620 fee: Money.zero(currency),
621 direction: direction,
622 isPending: tx.height == 0,
@@ -571,6 +628,7 @@ abstract class ZcashWalletBase
628 txType: tx.type,
629 isRotationReceive: isRotationReceive,
630 isShieldAction: isShieldAction,
631 + isIronwoodMigration: isMigration,
632 );
633 if (recipientAddresses.isNotEmpty) {
634 info.outputAddresses = recipientAddresses;
@@ -627,6 +685,9 @@ abstract class ZcashWalletBase
685 'tx_$txHash$suffix';
686
687 static int _txDisplayPriority(final ZcashTransactionInfo info) {
688 + if (info.additionalInfo['isIronwoodMigration'] == true) {
689 + return 4;
690 + }
691 if (info.additionalInfo['isAutoShield'] == true) {
692 return 3;
693 }
@@ -650,7 +711,8 @@ abstract class ZcashWalletBase
711 return;
712 }
713 if (infoPriority == existingPriority &&
653 - info.additionalInfo['isAutoShield'] == true &&
714 + (info.additionalInfo['isAutoShield'] == true ||
715 + info.additionalInfo['isIronwoodMigration'] == true) &&
716 info.direction == TransactionDirection.outgoing &&
717 existing.direction == TransactionDirection.incoming) {
718 byHash[hash] = info;
@@ -813,6 +875,10 @@ abstract class ZcashWalletBase
875
876 final Map<String, ZcashTransactionInfo> splitEntries = {};
877 for (final tx in txs) {
878 + _pendingOutgoingAmounts.remove(ZcashWalletService.normalizeTxId(tx.txHash));
879 + if (tx.height > 0) {
880 + ZcashMempoolService.instance.removeTx(tx.txHash);
881 + }
882 final isShield = _isShieldActionTx(tx, rotationSweepHashes: rotationSweepHashes);
883 if (_shouldSplitAutoshieldTx(tx, isShield: isShield)) {
884 byHash.remove(tx.txHash);
@@ -834,10 +900,15 @@ abstract class ZcashWalletBase
900 _offerTx(byHash, _zcashInfoFromZkoolTx(tx, currentHeight, isShieldAction: isShield));
901 }
902
837 - final knownHashes = {for (final tx in txs) tx.txHash, ...byHash.keys};
903 + final knownHashes = {
904 + for (final tx in txs) ZcashWalletService.normalizeTxId(tx.txHash),
905 + for (final hash in byHash.keys) ZcashWalletService.normalizeTxId(hash),
906 + };
907 for (final mempoolTx in ZcashMempoolService.instance.txsForAccount(accountId)) {
908 final hash = ZcashWalletService.normalizeTxId(mempoolTx.txid);
909 if (knownHashes.contains(hash)) {
910 + ZcashMempoolService.instance.removeTx(hash);
911 + _pendingOutgoingAmounts.remove(hash);
912 continue;
913 }
914 final info = _zcashInfoFromMempoolTx(mempoolTx, accountId);
@@ -998,31 +1069,36 @@ abstract class ZcashWalletBase
1069 }
1070
1071 bool _isTransactionUpdating = false;
1072 + bool _transactionUpdateQueued = false;
1073
1074 Future<void> updateTransactions() async {
1003 - try {
1004 - if (_isTransactionUpdating) {
1005 - return;
1006 - }
1007 -
1008 - _isTransactionUpdating = true;
1009 - final transactions = await fetchTransactions();
1010 -
1011 - final currentIds = transactionHistory.transactions.keys.toSet();
1012 - final newIds = transactions.keys.toSet();
1013 -
1014 - currentIds
1015 - .difference(newIds)
1016 - .forEach((final id) => transactionHistory.transactions.remove(id));
1075 + if (_isTransactionUpdating) {
1076 + _transactionUpdateQueued = true;
1077 + return;
1078 + }
1079
1018 - transactions.forEach((final key, final tx) {
1019 - transactionHistory.transactions[key] = tx;
1020 - });
1021 - await transactionHistory.save();
1022 - _isTransactionUpdating = false;
1080 + _isTransactionUpdating = true;
1081 + try {
1082 + do {
1083 + _transactionUpdateQueued = false;
1084 + final transactions = await fetchTransactions();
1085 +
1086 + final currentIds = transactionHistory.transactions.keys.toSet();
1087 + final newIds = transactions.keys.toSet();
1088 +
1089 + currentIds
1090 + .difference(newIds)
1091 + .forEach((final id) => transactionHistory.transactions.remove(id));
1092 +
1093 + transactions.forEach((final key, final tx) {
1094 + transactionHistory.transactions[key] = tx;
1095 + });
1096 + await transactionHistory.save();
1097 + } while (_transactionUpdateQueued);
1098 } catch (e, stackTrace) {
1099 printV("Update transactions error: $e");
1100 printV("Stack trace: $stackTrace");
1101 + } finally {
1102 _isTransactionUpdating = false;
1103 }
1104 }
@@ -1081,6 +1157,8 @@ abstract class ZcashWalletBase
1157
1158 static final autoShieldMutex = Mutex();
1159 static DateTime? _lastAutoShieldAt;
1160 + static final ironwoodMigrateMutex = Mutex();
1161 + static DateTime? _lastIronwoodMigrateAt;
1162 Future<void> _autoShield() async {
1163 if (_lastAutoShieldAt != null &&
1164 _lastAutoShieldAt!.isAfter(DateTime.now().subtract(const Duration(seconds: 75)))) {
@@ -1105,35 +1183,37 @@ abstract class ZcashWalletBase
1183 accountId: accountId,
1184 func: (coin) async {
1185 final _notes = await zkool_account.listNotes(c: coin);
1108 - final List<zkool_account.TxNote> txNotes = [];
1186 + BigInt sweepable = BigInt.zero;
1187 for (int i = 0; i < _notes.length; i++) {
1188 final note = _notes[i];
1189 + if (note.pool < 0 || note.pool >= NotePool.values.length) {
1190 + continue;
1191 + }
1192 final noteType = NotePool.values[note.pool];
1112 - if ([NotePool.sapling, NotePool.transparent].contains(noteType)) {
1113 - txNotes.add(note);
1193 + if (noteType == NotePool.transparent || noteType == NotePool.sapling) {
1194 + sweepable += note.value;
1195 }
1196 }
1197
1117 - final sweepable = txNotes.isEmpty
1118 - ? BigInt.from(0)
1119 - : txNotes.map((final txn) => txn.value).reduce((final a, final b) => a + b);
1120 -
1198 if (sweepable <= BigInt.from(_autoShieldMinSweep)) {
1199 return null;
1200 }
1201
1202 + final ironwood = await zkool_network.isIronwoodActive(c: coin);
1203 final txPlan = await zkool_pay.prepare(
1204 recipients: [
1205 zkool_paydart.Recipient(
1206 assetBase: zecBase,
1207 address: walletAddresses.orchardAddress!,
1208 amount: sweepable,
1209 + pools: ironwood ? ironwoodPoolMask : null,
1210 ),
1211 ],
1212 options: zkool_pay.PaymentOptions(
1213 srcPools: 3,
1214 recipientPaysFee: true,
1215 smartTransparent: false,
1216 + mode: 0,
1217 ),
1218 c: coin,
1219 );
@@ -1156,34 +1236,134 @@ abstract class ZcashWalletBase
1236 _lastAutoShieldAt = DateTime.now();
1237 printV("shielded: $txId");
1238 await updateTransactions();
1159 - await _refreshBalance(runAutoShield: false);
1239 + await _refreshBalance(runAutoShield: false, runIronwoodMigrate: false);
1240 }
1241
1162 - Future<void> _refreshBalance({required final bool runAutoShield}) async {
1242 + Future<void> _ironwoodMigrate() async {
1243 + if (_lastIronwoodMigrateAt != null &&
1244 + _lastIronwoodMigrateAt!.isAfter(DateTime.now().subtract(const Duration(seconds: 75)))) {
1245 + return;
1246 + }
1247 try {
1164 - final bal = await runWithCoin(
1248 + await ironwoodMigrateMutex.acquire();
1249 + await _$ironwoodMigrate();
1250 + } catch (e, s) {
1251 + printV("ironwood migration failed: $e");
1252 + s.toString().split("\n").forEach(printV);
1253 + } finally {
1254 + ironwoodMigrateMutex.release();
1255 + }
1256 + }
1257 +
1258 + Future<void> _$ironwoodMigrate() async {
1259 + if (syncStatus is! SyncedSyncStatus) {
1260 + return;
1261 + }
1262 + final event = await runWithCoin(
1263 + accountId: accountId,
1264 + func: (coin) async {
1265 + if (!await zkool_network.isIronwoodActive(c: coin)) {
1266 + return null;
1267 + }
1268 + final bal = await zkool_sync.balance(c: coin);
1269 + if (bal.field0.length <= 2 || bal.field0[2] <= BigInt.zero) {
1270 + return null;
1271 + }
1272 + return zkool_migrate.stepMigration(c: coin);
1273 + },
1274 + );
1275 + if (event == null) {
1276 + return;
1277 + }
1278 + switch (event) {
1279 + case zkool_migrate.MigrationEvent_Complete():
1280 + case zkool_migrate.MigrationEvent_NothingToDo():
1281 + return;
1282 + case zkool_migrate.MigrationEvent_SplitComplete(:final fee):
1283 + printV("ironwood split step complete, fee: $fee");
1284 + case zkool_migrate.MigrationEvent_MigrateComplete(:final fee):
1285 + printV("ironwood migrate step complete, fee: $fee");
1286 + case zkool_migrate.MigrationEvent_Error(:final message):
1287 + printV("ironwood migration error: $message");
1288 + return;
1289 + }
1290 +
1291 + _lastIronwoodMigrateAt = DateTime.now();
1292 + await updateTransactions();
1293 + await _refreshBalance(runAutoShield: false, runIronwoodMigrate: false);
1294 + }
1295 +
1296 + Future<void> _updateIronwoodActive() async {
1297 + bool? active;
1298 + try {
1299 + active = await runWithCoin(
1300 accountId: accountId,
1166 - func: (coin) => zkool_sync.balance(c: coin),
1301 + func: (final coin) => zkool_network.isIronwoodActive(c: coin),
1302 );
1303 + } catch (e) {
1304 + printV("isIronwoodActive: $e");
1305 + }
1306
1169 - // 0 - transparent
1170 - // 1 - sapling
1171 - // 2 - orchard
1172 - final confirmedTotal = bal.field0.reduce((final a, final b) => a + b);
1307 + if (active == null && networkFor(walletInfo) == ZcashNetwork.regtest) {
1308 + try {
1309 + final height = await runWithCoin(
1310 + accountId: accountId,
1311 + func: (final coin) => zkool_network.getCurrentHeight(c: coin),
1312 + );
1313 + active = height >= ZcashNetwork.regtestNu63Height;
1314 + printV("regtest ironwood inferred from height $height: $active");
1315 + } catch (e) {
1316 + printV("regtest height check failed: $e");
1317 + }
1318 + }
1319
1174 - // int knownOutPending = 0;
1175 - // ZcashWalletBase.temporarySentTx[accountId]?.forEach((final sTx) {
1176 - // knownOutPending += sTx.value; // it's negative
1177 - // });
1178 - final confirmedSpendable = confirmedTotal - bal.field0[0];
1320 + if (active == null) {
1321 + return;
1322 + }
1323 +
1324 + ironwoodActive = active;
1325 + runInAction(() => walletAddresses.setIronwoodActive(active!));
1326 + printV("ironwoodActive=$active (account $accountId)");
1327 + }
1328 +
1329 + Future<void> _refreshBalance({
1330 + required final bool runAutoShield,
1331 + final bool runIronwoodMigrate = true,
1332 + }) async {
1333 + try {
1334 + await _updateIronwoodActive();
1335 + final bal = await runWithCoin(
1336 + accountId: accountId,
1337 + func: (final coin) async => zkool_sync.balance(c: coin),
1338 + );
1339 +
1340 + // 0 - transparent, 1 - sapling, 2 - orchard, 3 - ironwood
1341 + final transparent = bal.field0[0];
1342 + final sapling = bal.field0.length > 1 ? bal.field0[1] : BigInt.zero;
1343 + final orchard = bal.field0.length > 2 ? bal.field0[2] : BigInt.zero;
1344 + final ironwood = bal.field0.length > 3 ? bal.field0[3] : BigInt.zero;
1345
1346 if (runAutoShield) {
1347 await _autoShield();
1348 }
1349 + if (runIronwoodMigrate) {
1350 + await _ironwoodMigrate();
1351 + }
1352 +
1353 + // After NU6.3, Orchard notes are migrated to Ironwood - show them as unconfirmed.
1354 + final BigInt availableAmount;
1355 + final BigInt unavailableAmount;
1356 + if (ironwoodActive == true && orchard > BigInt.zero) {
1357 + availableAmount = sapling + ironwood;
1358 + unavailableAmount = transparent + orchard;
1359 + } else {
1360 + availableAmount = sapling + orchard + ironwood;
1361 + unavailableAmount = transparent;
1362 + }
1363
1364 balance[CryptoCurrency.zec] = ZcashBalance(
1185 - Money(confirmedSpendable, currency),
1186 - Money(confirmedTotal - confirmedSpendable, currency),
1365 + Money(availableAmount, currency),
1366 + Money(unavailableAmount, currency),
1367 frozen: Money.zero(currency),
1368 );
1369 } catch (e, stackTrace) {
@@ -1211,7 +1391,9 @@ abstract class ZcashWalletBase
1391 late ZcashWalletAddresses walletAddresses = ZcashWalletAddresses(accountId, walletInfo);
1392
1393 static Future<ZcashWallet> create(final WalletCredentials credentials) async {
1214 - await $init();
1394 + final network = networkForCredentials(credentials);
1395 + await $init(network: network);
1396 + credentials.walletInfo?.network = network.value;
1397 final newWalletCredentials = credentials as ZcashNewWalletCredentials;
1398
1399 String mnemonic;
@@ -1222,7 +1404,7 @@ abstract class ZcashWalletBase
1404 mnemonic = bip39.generateMnemonic(strength: strength);
1405 }
1406
1225 - final birthHeight = await ZcashHeight.getBlockHeightByTime(DateTime.now());
1407 + final birthHeight = await birthHeightForNetwork(network);
1408
1409 final accountId = await restoreZcashWalletFromSeed(
1410 name: credentials.name,
@@ -1241,7 +1423,9 @@ abstract class ZcashWalletBase
1423 }
1424
1425 static Future<ZcashWallet> restore(final WalletCredentials credentials) async {
1244 - await $init();
1426 + final network = networkForCredentials(credentials);
1427 + await $init(network: network);
1428 + credentials.walletInfo?.network = network.value;
1429 final fromSeedCredentials = credentials as ZcashFromSeedWalletCredentials;
1430 final String? seed = fromSeedCredentials.seed;
1431 if (seed == null || seed.isEmpty) {
@@ -1265,7 +1449,9 @@ abstract class ZcashWalletBase
1449 }
1450
1451 static Future<ZcashWallet> restoreKeys(final WalletCredentials credentials) async {
1268 - await $init();
1452 + final network = networkForCredentials(credentials);
1453 + await $init(network: network);
1454 + credentials.walletInfo?.network = network.value;
1455 final fromKeysCredentials = credentials as ZcashFromKeysWalletCredentials;
1456 final String? keys = fromKeysCredentials.privateKey;
1457 if (keys == null || keys.isEmpty) {
@@ -1299,7 +1485,8 @@ abstract class ZcashWalletBase
1485 required final String password,
1486 required final WalletInfo walletInfo,
1487 }) async {
1302 - await $init();
1488 + final network = networkFor(walletInfo);
1489 + await $init(network: network);
1490 // if (password.isNotEmpty) {
1491 // setDbPasswd(coin, password);
1492 // }
@@ -1379,9 +1566,32 @@ abstract class ZcashWalletBase
1566
1567 static WalletType get _type => WalletType.zcash;
1568
1382 - static Future<String> getDbDataPath() async {
1569 + static ZcashNetwork networkFor(final WalletInfo? walletInfo) =>
1570 + ZcashNetwork.fromName(walletInfo?.network ?? ZcashNetwork.mainnet.value);
1571 +
1572 + static ZcashNetwork networkForCredentials(final WalletCredentials credentials) {
1573 + if (credentials is ZcashNewWalletCredentials) {
1574 + return ZcashNetwork.fromIndex(credentials.network);
1575 + }
1576 + if (credentials is ZcashFromSeedWalletCredentials) {
1577 + return ZcashNetwork.fromIndex(credentials.network);
1578 + }
1579 + if (credentials is ZcashFromKeysWalletCredentials) {
1580 + return ZcashNetwork.fromIndex(credentials.network);
1581 + }
1582 + return ZcashNetwork.mainnet;
1583 + }
1584 +
1585 + static Future<int> birthHeightForNetwork(final ZcashNetwork network) async {
1586 + if (network != ZcashNetwork.mainnet) {
1587 + return 1;
1588 + }
1589 + return ZcashHeight.getBlockHeightByTime(DateTime.now());
1590 + }
1591 +
1592 + static Future<String> getDbDataPath({final ZcashNetwork network = ZcashNetwork.mainnet}) async {
1593 final pathForWalletType = await pathForWalletTypeDir(type: _type);
1384 - final dbDataPath = "${pathForWalletType}/zec.v2.db";
1594 + final dbDataPath = "${pathForWalletType}/${network.dbFileName}";
1595 if (!Directory(pathForWalletType).existsSync()) {
1596 Directory(pathForWalletType).createSync(recursive: true);
1597 }
@@ -1398,6 +1608,8 @@ abstract class ZcashWalletBase
1608 }
1609
1610 static bool _initialized = false;
1611 + static bool _rustInitialized = false;
1612 + static ZcashNetwork? _activeNetwork;
1613
1614 static void unlockDatabase(final String password) {
1615 _password = password;
@@ -1406,22 +1618,25 @@ abstract class ZcashWalletBase
1618 static var c = zkool_coin.Coin();
1619
1620 static String? _password;
1409 - static Future<void> $init() async {
1410 - if (_initialized) return;
1411 - _initialized = true;
1412 - printV(r".$init()");
1413 - await zkool_frb.RustLib.init();
1621 + static Future<void> $init({final ZcashNetwork network = ZcashNetwork.mainnet}) async {
1622 + if (!_rustInitialized) {
1623 + await zkool_frb.RustLib.init();
1624 + _rustInitialized = true;
1625 + }
1626 + if (_initialized && _activeNetwork == network) {
1627 + return;
1628 + }
1629 + printV(r".$init($network)");
1630 ZcashMempoolService.instance.onAccountsUpdated = (final accountIds) {
1631 for (final accountId in accountIds) {
1632 unawaited(refreshWalletForAccount(accountId));
1633 }
1634 };
1419 - final dbFile = File(await getDbDataPath());
1635 + final dbFile = File(await getDbDataPath(network: network));
1636 final ywalletDbFile = File(await getDbDataPathLegacyYwallet());
1637 await zkool_network.initDatadir(directory: dbFile.parent.path);
1422 - // c = await c.openDatabase(dbFilepath: dbFile.path, password: 'cw_zcash_migration');
1638 c = await c.openDatabase(dbFilepath: dbFile.path, password: null);
1424 - printV("initWallet");
1639 + printV("initWallet: ${dbFile.path}");
1640 if (_password == null) {
1641 throw Exception("Zcash wallet locked! Please contact support");
1642 }
@@ -1432,6 +1647,7 @@ abstract class ZcashWalletBase
1647 //TODO(mrcyjanek): migrate to zkool
1648 }
1649
1650 + _activeNetwork = network;
1651 _initialized = true;
1652 }
1653
cw_zcash/lib/src/zcash_wallet_addresses.dart
+16 -8
@@ -39,8 +39,24 @@ abstract class ZcashWalletAddressesBase extends WalletAddresses with Store {
39 @observable
40 String? unifiedAddress;
41
42 + @observable
43 + bool ironwoodActive = false;
44 +
45 + @action
46 + void setIronwoodActive(final bool active) {
47 + ironwoodActive = active;
48 + }
49 +
50 @override
51 @computed
52 + List<ReceivePageOption> get receivePageOptions {
53 + return [
54 + ...ZcashReceivePageOption.allOptionsFor(ironwood: ironwoodActive),
55 + ...ReceivePageOptions.where((final element) => element != ReceivePageOption.mainnet),
56 + ];
57 + }
58 +
59 + @override
60 String get latestAddress {
61 switch (addressPageType) {
62 case ZcashAddressType.transparent:
@@ -267,14 +283,6 @@ abstract class ZcashWalletAddressesBase extends WalletAddresses with Store {
283 return addressInfos[0] ?? [];
284 }
285
270 - @override
271 - List<ReceivePageOption> get receivePageOptions {
272 - return [
273 - ...ZcashReceivePageOption.all,
274 - ...ReceivePageOptions.where((final element) => element != ReceivePageOption.mainnet),
275 - ];
276 - }
277 -
286 @override
287 PaymentURI getPaymentUri(final String amount) => ZcashURI(amount: amount, address: address);
288 }
cw_zcash/lib/src/zcash_wallet_service.dart
+5 -5
@@ -91,7 +91,11 @@ class ZcashWalletService
91
92 @override
93 Future<ZcashWallet> openWallet(final String name, final String password) async {
94 - await ZcashWalletBase.$init();
94 + final walletInfo = await WalletInfo.get(name, getType());
95 + if (walletInfo == null) {
96 + throw Exception('Wallet not found');
97 + }
98 + await ZcashWalletBase.$init(network: ZcashWalletBase.networkFor(walletInfo));
99 if (await isWalletExit(name)) {
100 final path = (await pathForWallet(name: name, type: getType())) + ".v2";
101 if (!File(path).existsSync()) {
@@ -100,10 +104,6 @@ class ZcashWalletService
104 }
105
106 await loadShieldTxs();
103 - final walletInfo = await WalletInfo.get(name, getType());
104 - if (walletInfo == null) {
105 - throw Exception('Wallet not found');
106 - }
107 try {
108 final wallet = await ZcashWalletBase.open(
109 name: name,
cw_zcash/lib/src/zkool_compat.dart
+8
@@ -1,3 +1,11 @@
1 import 'dart:typed_data';
2
3 final zecBase = Uint8List(32);
4 +
5 +/// Whether NU6.3 (Ironwood) is active at the chain tip. Updated on balance refresh.
6 +bool ironwoodActive = false;
7 +
8 +/// Pool bitmask for Ironwood (pool index 3). Orchard and Ironwood share the same address.
9 +const ironwoodPoolMask = 8;
10 +
11 +int? get shieldedRecipientPools => ironwoodActive ? ironwoodPoolMask : null;
cw_zcash/lib/src/zkooltx.dart
+27 -2
@@ -17,7 +17,7 @@ enum TxType {
17 transparentSelfTransfer, // 12
18 }
19
20 -enum NotePool { transparent, sapling, orchard, unknown }
20 +enum NotePool { transparent, sapling, orchard, ironwood }
21
22 class ZkoolTx {
23 ZkoolTx(final zkool_account.Tx tx, final zkool_account.TxAccount txAccount)
@@ -72,6 +72,22 @@ class ZkoolTx {
72 .fold(BigInt.zero, (final a, final o) => a + o.value);
73 }
74
75 + BigInt get ironwoodReceived {
76 + final fromNotes = _txAccount.notes
77 + .where((final n) => n.pool == NotePool.ironwood.index)
78 + .fold(BigInt.zero, (final a, final n) => a + n.value);
79 + if (fromNotes > BigInt.zero) {
80 + return fromNotes;
81 + }
82 + return _txAccount.outputs
83 + .where((final o) => o.pool == NotePool.ironwood.index)
84 + .fold(BigInt.zero, (final a, final o) => a + o.value);
85 + }
86 +
87 + BigInt get orchardSpent => _txAccount.spends
88 + .where((final s) => s.pool == NotePool.orchard.index)
89 + .fold(BigInt.zero, (final a, final s) => a + s.value);
90 +
91 String get txHash {
92 final reversed = Uint8List.fromList(_txAccount.txid.reversed.toList());
93 final txId = uint8ListToHex(reversed);
@@ -222,6 +238,13 @@ class ZkoolTx {
238 }
239
240 static Uint8List _txidFromJson(final dynamic raw) {
241 + return _bytesFromJson(raw, fieldName: "txid");
242 + }
243 +
244 + static Uint8List _bytesFromJson(final dynamic raw, {required final String fieldName}) {
245 + if (raw == null) {
246 + return Uint8List(0);
247 + }
248 if (raw is Uint8List) {
249 return raw;
250 }
@@ -231,7 +254,7 @@ class ZkoolTx {
254 if (raw is String) {
255 return base64.decode(raw);
256 }
234 - throw FormatException("Invalid txid in ZkoolTx json: $raw");
257 + throw FormatException("Invalid $fieldName in ZkoolTx json: $raw");
258 }
259
260 static List<T> _listFromJson<T>(final dynamic raw, final T Function(Map<String, dynamic>) parse) {
@@ -257,6 +280,7 @@ class ZkoolTx {
280 tpe: txJson["tpe"] == null ? null : _asInt(txJson["tpe"]),
281 zsaValue: _asInt(txJson["zsaValue"]),
282 assetDisplay: txJson["assetDisplay"] as String? ?? "",
283 + isUserMemo: txJson["isUserMemo"] as bool? ?? false,
284 ),
285 zkool_account.TxAccount(
286 id: _asInt(txAccountJson["id"]),
@@ -304,6 +328,7 @@ class ZkoolTx {
328 pool: _asInt(a["pool"]),
329 output: a["output"] == null ? null : _asInt(a["output"]),
330 memo: a["memo"] as String?,
331 + memoBytes: _bytesFromJson(a["memoBytes"], fieldName: "memoBytes"),
332 ),
333 ),
334 ),
cw_zcash/pubspec.yaml
+1 -1
@@ -21,7 +21,7 @@ dependencies:
21 # path: ../../zkool2
22 git:
23 url: https://github.com/cake-tech/zkool2.git
24 - ref: dbd3dde8d8485428326a58e55660b7404666dd7c
24 + ref: 97d18db966c061a536fa9056efb3756f4b9aa89d
25
26 path_provider: any
27 sqflite_common_ffi: any
ios/Podfile
+1 -1
@@ -43,7 +43,7 @@ post_install do |installer|
43 flutter_additional_ios_build_settings(target)
44
45 target.build_configurations.each do |config|
46 - config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0'
46 + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.0'
47 config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
48 '$(inherited)',
49
ios/Podfile.lock
+8 -59
@@ -1,14 +1,4 @@
1 PODS:
2 - - AppAuth (1.7.6):
3 - - AppAuth/Core (= 1.7.6)
4 - - AppAuth/ExternalUserAgent (= 1.7.6)
5 - - AppAuth/Core (1.7.6)
6 - - AppAuth/ExternalUserAgent (1.7.6):
7 - - AppAuth/Core
8 - - AppCheckCore (11.2.0):
9 - - GoogleUtilities/Environment (~> 8.0)
10 - - GoogleUtilities/UserDefaults (~> 8.0)
11 - - PromisesObjC (~> 2.4)
2 - bitbox_flutter (0.0.1):
3 - Flutter
4 - breez_sdk_spark_flutter (0.14.0):
@@ -64,6 +54,8 @@ PODS:
54 - DKImagePickerController/PhotoGallery
55 - Flutter
56 - Flutter (1.0.0)
57 + - flutter_contacts (0.0.1):
58 + - Flutter
59 - flutter_inappwebview_ios (0.0.1):
60 - Flutter
61 - flutter_inappwebview_ios/Core (= 0.0.1)
@@ -83,34 +75,6 @@ PODS:
75 - Flutter
76 - fluttertoast (0.0.2):
77 - Flutter
86 - - google_sign_in_ios (0.0.1):
87 - - AppAuth (>= 1.7.4)
88 - - Flutter
89 - - FlutterMacOS
90 - - GoogleSignIn (~> 8.0)
91 - - GTMSessionFetcher (>= 3.4.0)
92 - - GoogleSignIn (8.0.0):
93 - - AppAuth (< 2.0, >= 1.7.3)
94 - - AppCheckCore (~> 11.0)
95 - - GTMAppAuth (< 5.0, >= 4.1.1)
96 - - GTMSessionFetcher/Core (~> 3.3)
97 - - GoogleUtilities/Environment (8.1.0):
98 - - GoogleUtilities/Privacy
99 - - GoogleUtilities/Logger (8.1.0):
100 - - GoogleUtilities/Environment
101 - - GoogleUtilities/Privacy
102 - - GoogleUtilities/Privacy (8.1.0)
103 - - GoogleUtilities/UserDefaults (8.1.0):
104 - - GoogleUtilities/Logger
105 - - GoogleUtilities/Privacy
106 - - GTMAppAuth (4.1.1):
107 - - AppAuth/Core (~> 1.7)
108 - - GTMSessionFetcher/Core (< 4.0, >= 3.3)
109 - - GTMSessionFetcher (3.5.0):
110 - - GTMSessionFetcher/Full (= 3.5.0)
111 - - GTMSessionFetcher/Core (3.5.0)
112 - - GTMSessionFetcher/Full (3.5.0):
113 - - GTMSessionFetcher/Core
78 - image_picker_ios (0.0.1):
79 - Flutter
80 - in_app_review (2.0.0):
@@ -126,7 +90,6 @@ PODS:
90 - payjoin_flutter (0.23.0)
91 - permission_handler_apple (9.4.8):
92 - Flutter
129 - - PromisesObjC (2.4.0)
93 - quick_actions_ios (0.0.1):
94 - Flutter
95 - reown_yttrium (0.0.1):
@@ -204,6 +167,7 @@ DEPENDENCIES:
167 - fast_scanner (from `.symlinks/plugins/fast_scanner/ios`)
168 - file_picker (from `.symlinks/plugins/file_picker/ios`)
169 - Flutter (from `Flutter`)
170 + - flutter_contacts (from `.symlinks/plugins/flutter_contacts/ios`)
171 - flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`)
172 - flutter_local_authentication (from `.symlinks/plugins/flutter_local_authentication/ios`)
173 - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
@@ -211,7 +175,6 @@ DEPENDENCIES:
175 - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
176 - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
177 - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
214 - - google_sign_in_ios (from `.symlinks/plugins/google_sign_in_ios/darwin`)
178 - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
179 - in_app_review (from `.symlinks/plugins/in_app_review/ios`)
180 - integration_test (from `.symlinks/plugins/integration_test/ios`)
@@ -237,17 +200,10 @@ DEPENDENCIES:
200
201 SPEC REPOS:
202 https://github.com/CocoaPods/Specs.git:
240 - - AppAuth
241 - - AppCheckCore
203 - CryptoSwift
204 - DKImagePickerController
205 - DKPhotoGallery
245 - - GoogleSignIn
246 - - GoogleUtilities
247 - - GTMAppAuth
248 - - GTMSessionFetcher
206 - OrderedSet
250 - - PromisesObjC
207 - SDWebImage
208 - sqlite3
209 - SwiftyGif
@@ -278,6 +234,8 @@ EXTERNAL SOURCES:
234 :path: ".symlinks/plugins/file_picker/ios"
235 Flutter:
236 :path: Flutter
237 + flutter_contacts:
238 + :path: ".symlinks/plugins/flutter_contacts/ios"
239 flutter_inappwebview_ios:
240 :path: ".symlinks/plugins/flutter_inappwebview_ios/ios"
241 flutter_local_authentication:
@@ -292,8 +250,6 @@ EXTERNAL SOURCES:
250 :path: ".symlinks/plugins/flutter_secure_storage/ios"
251 fluttertoast:
252 :path: ".symlinks/plugins/fluttertoast/ios"
295 - google_sign_in_ios:
296 - :path: ".symlinks/plugins/google_sign_in_ios/darwin"
253 image_picker_ios:
254 :path: ".symlinks/plugins/image_picker_ios/ios"
255 in_app_review:
@@ -340,8 +296,6 @@ EXTERNAL SOURCES:
296 :path: ".symlinks/plugins/wakelock_plus/ios"
297
298 SPEC CHECKSUMS:
343 - AppAuth: d4f13a8fe0baf391b2108511793e4b479691fb73
344 - AppCheckCore: cc8fd0a3a230ddd401f326489c99990b013f0c4f
299 bitbox_flutter: 9505732798041c413152669751beeaecc5fe400f
300 breez_sdk_spark_flutter: ed3b6709b8ce9b40309d8728640eade5b19e8a0d
301 connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
@@ -357,6 +311,7 @@ SPEC CHECKSUMS:
311 fast_scanner: 2cb1ad3e69e645e9980fb4961396ce5804caa3e3
312 file_picker: 9b3292d7c8bc68c8a7bf8eb78f730e49c8efc517
313 Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
314 + flutter_contacts: 2ec1d6b62ca2869c3bbb3871bd74d4e3ef157135
315 flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
316 flutter_local_authentication: 989278c681612f1ee0e36019e149137f114b9d7f
317 flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb
@@ -364,11 +319,6 @@ SPEC CHECKSUMS:
319 flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
320 flutter_secure_storage: 2c2ff13db9e0a5647389bff88b0ecac56e3f3418
321 fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1
367 - google_sign_in_ios: b48bb9af78576358a168361173155596c845f0b9
368 - GoogleSignIn: ce8c89bb9b37fb624b92e7514cc67335d1e277e4
369 - GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
370 - GTMAppAuth: f69bd07d68cd3b766125f7e072c45d7340dea0de
371 - GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
322 image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
323 in_app_review: 7dd1ea365263f834b8464673f9df72c80c17c937
324 integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
@@ -377,7 +327,6 @@ SPEC CHECKSUMS:
327 package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
328 payjoin_flutter: d9d4c8aa16bd5dfedb9b21d0edc8199e0187d96e
329 permission_handler_apple: 92d754bbaa7361d436db2d6c3c1c2a0fdcec462e
380 - PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
330 quick_actions_ios: 500fcc11711d9f646739093395c4ae8eec25f779
331 reown_yttrium: cee334ade64725b1d83f7b34c706a6aae2696d58
332 rive_common: dd421daaf9ae69f0125aa761dd96abd278399952
@@ -398,6 +347,6 @@ SPEC CHECKSUMS:
347 wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
348 YttriumWrapper: 31e937fe9fbe0f1314d2ca6be9ce9b379a059966
349
401 -PODFILE CHECKSUM: 5296465b1c6d14d506230356756826012f65d97a
350 +PODFILE CHECKSUM: b08d7f4c8d9c64f5648599a30caea878b0f7616a
351
403 -COCOAPODS: 1.16.2
352 +COCOAPODS: 1.17.0
ios/Runner.xcodeproj/project.pbxproj
+6 -6
@@ -479,7 +479,7 @@
479 GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
480 GCC_WARN_UNUSED_FUNCTION = YES;
481 GCC_WARN_UNUSED_VARIABLE = YES;
482 - IPHONEOS_DEPLOYMENT_TARGET = 13.0;
482 + IPHONEOS_DEPLOYMENT_TARGET = 15.0;
483 MTL_ENABLE_DEBUG_INFO = NO;
484 SDKROOT = iphoneos;
485 SUPPORTED_PLATFORMS = iphoneos;
@@ -509,7 +509,7 @@
509 "$(PROJECT_DIR)",
510 );
511 INFOPLIST_FILE = Runner/Info.plist;
512 - IPHONEOS_DEPLOYMENT_TARGET = 13.0;
512 + IPHONEOS_DEPLOYMENT_TARGET = 15.0;
513 LD_RUNPATH_SEARCH_PATHS = (
514 "$(inherited)",
515 "@executable_path/Frameworks",
@@ -578,7 +578,7 @@
578 GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
579 GCC_WARN_UNUSED_FUNCTION = YES;
580 GCC_WARN_UNUSED_VARIABLE = YES;
581 - IPHONEOS_DEPLOYMENT_TARGET = 13.0;
581 + IPHONEOS_DEPLOYMENT_TARGET = 15.0;
582 MTL_ENABLE_DEBUG_INFO = YES;
583 ONLY_ACTIVE_ARCH = YES;
584 SDKROOT = iphoneos;
@@ -628,7 +628,7 @@
628 GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
629 GCC_WARN_UNUSED_FUNCTION = YES;
630 GCC_WARN_UNUSED_VARIABLE = YES;
631 - IPHONEOS_DEPLOYMENT_TARGET = 13.0;
631 + IPHONEOS_DEPLOYMENT_TARGET = 15.0;
632 MTL_ENABLE_DEBUG_INFO = NO;
633 SDKROOT = iphoneos;
634 SUPPORTED_PLATFORMS = iphoneos;
@@ -660,7 +660,7 @@
660 "$(PROJECT_DIR)",
661 );
662 INFOPLIST_FILE = Runner/Info.plist;
663 - IPHONEOS_DEPLOYMENT_TARGET = 13.0;
663 + IPHONEOS_DEPLOYMENT_TARGET = 15.0;
664 LD_RUNPATH_SEARCH_PATHS = (
665 "$(inherited)",
666 "@executable_path/Frameworks",
@@ -704,7 +704,7 @@
704 "$(PROJECT_DIR)",
705 );
706 INFOPLIST_FILE = Runner/Info.plist;
707 - IPHONEOS_DEPLOYMENT_TARGET = 13.0;
707 + IPHONEOS_DEPLOYMENT_TARGET = 15.0;
708 LD_RUNPATH_SEARCH_PATHS = (
709 "$(inherited)",
710 "@executable_path/Frameworks",
lib/core/address_validator.dart
+11 -4
@@ -3,6 +3,7 @@ import 'package:cake_wallet/generated/i18n.dart';
3 import 'package:cake_wallet/core/validator.dart';
4 import 'package:cake_wallet/solana/solana.dart';
5 import 'package:cake_wallet/zano/zano.dart';
6 +import 'package:cake_wallet/zcash/zcash_network_type.dart';
7 import 'package:cw_core/crypto_currency.dart';
8 import 'package:cw_core/erc20_token.dart';
9
@@ -10,8 +11,11 @@ const BEFORE_REGEX = '(^|\\s)';
11 const AFTER_REGEX = '(\$|\\s)';
12
13 class AddressValidator extends TextValidator {
13 - AddressValidator({required CryptoCurrency type, bool isTestnet = false})
14 - : super(
14 + AddressValidator({
15 + required CryptoCurrency type,
16 + bool isTestnet = false,
17 + String? network,
18 + }) : super(
19 errorMessage: S.current.error_text_address,
20 useAdditionalValidation: [CryptoCurrency.btc, CryptoCurrency.ltc].contains(type)
21 ? (String txt) {
@@ -32,7 +36,7 @@ class AddressValidator extends TextValidator {
36 : type == CryptoCurrency.zano
37 ? zano?.validateAddress
38 : null,
35 - pattern: getPattern(type, isTestnet: isTestnet),
39 + pattern: getPattern(type, isTestnet: isTestnet, network: network),
40 length: getLength(type),
41 );
42
@@ -61,7 +65,7 @@ class AddressValidator extends TextValidator {
65 CryptoCurrency.btcln,
66 ];
67
64 - static String getPattern(CryptoCurrency type, {bool isTestnet = false}) {
68 + static String? getPattern(CryptoCurrency type, {bool isTestnet = false, String? network}) {
69 var pattern = "";
70 if (type is Erc20Token) {
71 pattern = '0x[0-9a-zA-Z]+';
@@ -170,6 +174,9 @@ class AddressValidator extends TextValidator {
174 case CryptoCurrency.hbar:
175 pattern = '[0-9a-zA-Z.]+';
176 case CryptoCurrency.zec:
177 + if (ZcashNetworkType.isDevNetwork(network)) {
178 + return null;
179 + }
180 pattern = '(?:'
181 't1[0-9A-Za-z]{33}'
182 '|t3[0-9A-Za-z]{33}'
lib/new-ui/widgets/receive_page/receive_address_type_selector.dart
+3
@@ -6,6 +6,7 @@ import 'package:cake_wallet/src/widgets/section_divider.dart';
6 import 'package:cake_wallet/view_model/dashboard/receive_option_view_model.dart';
7 import 'package:cw_core/receive_page_option.dart';
8 import 'package:flutter/material.dart';
9 +import 'package:flutter_mobx/flutter_mobx.dart';
10 import 'package:flutter_svg/flutter_svg.dart';
11 import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
12
@@ -34,6 +35,7 @@ class _ReceiveAddressTypeSelectorState extends State<ReceiveAddressTypeSelector>
35
36 @override
37 Widget build(BuildContext context) {
38 + return Observer(builder: (final _) {
39 final commonOptions = widget.receiveOptionViewModel.options
40 .where((element) =>
41 element.isCommon ||
@@ -223,6 +225,7 @@ class _ReceiveAddressTypeSelectorState extends State<ReceiveAddressTypeSelector>
225 ],
226 )),
227 );
228 + });
229 }
230 }
231
lib/router.dart
+6
@@ -162,6 +162,7 @@ import 'package:cw_core/unspent_coin_type.dart';
162 import 'package:cw_core/utils/print_verbose.dart';
163 import 'package:cw_core/wallet_info.dart';
164 import 'package:cw_core/wallet_type.dart';
165 +import 'package:cake_wallet/zcash/zcash_network_type.dart';
166 import 'package:flutter/cupertino.dart';
167 import 'package:flutter/material.dart';
168 import 'package:flutter/services.dart';
@@ -822,6 +823,9 @@ Route<dynamic> createRoute(RouteSettings settings) {
823 final isChildWallet = args['isChildWallet'] as bool? ?? false;
824 final useTestnet = args['useTestnet'] as bool;
825 final toggleTestnet = args['toggleTestnet'] as Function(bool? val);
826 + final zcashNetwork = args['zcashNetwork'] as int? ?? ZcashNetworkType.mainnet;
827 + final setZcashNetwork =
828 + args['setZcashNetwork'] as void Function(int network)? ?? (_) {};
829 final restoredWallet = args['restoredWallet'] as RestoredWallet?;
830
831 final viewModelParam = {'type': type, 'isPow': false};
@@ -832,6 +836,8 @@ Route<dynamic> createRoute(RouteSettings settings) {
836 isChildWallet: isChildWallet,
837 useTestnet: useTestnet,
838 toggleUseTestnet: toggleTestnet,
839 + zcashNetwork: zcashNetwork,
840 + setZcashNetwork: setZcashNetwork,
841 advancedPrivacySettingsViewModel:
842 getIt.get<AdvancedPrivacySettingsViewModel>(param1: type),
843 nodeViewModel: getIt.get<NodeCreateOrEditViewModel>(param1: viewModelParam),
lib/src/screens/new_wallet/advanced_privacy_settings_page.dart
+28
@@ -14,11 +14,13 @@ import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
14 import 'package:cake_wallet/src/widgets/primary_button.dart';
15 import 'package:cake_wallet/src/widgets/scrollable_with_bottom_section.dart';
16 import 'package:cake_wallet/utils/show_pop_up.dart';
17 +import 'package:cake_wallet/utils/feature_flag.dart';
18 import 'package:cake_wallet/view_model/advanced_privacy_settings_view_model.dart';
19 import 'package:cake_wallet/view_model/node_list/node_create_or_edit_view_model.dart';
20 import 'package:cake_wallet/view_model/seed_settings_view_model.dart';
21 import 'package:cake_wallet/view_model/settings/choices_list_item.dart';
22 import 'package:cw_core/wallet_type.dart';
23 +import 'package:cake_wallet/zcash/zcash_network_type.dart';
24 import 'package:flutter/material.dart';
25 import 'package:flutter_mobx/flutter_mobx.dart';
26
@@ -28,6 +30,8 @@ class AdvancedPrivacySettingsPage extends BasePage {
30 required this.isChildWallet,
31 required this.useTestnet,
32 required this.toggleUseTestnet,
33 + required this.zcashNetwork,
34 + required this.setZcashNetwork,
35 required this.advancedPrivacySettingsViewModel,
36 required this.nodeViewModel,
37 required this.seedSettingsViewModel,
@@ -44,6 +48,8 @@ class AdvancedPrivacySettingsPage extends BasePage {
48 final bool isChildWallet;
49 final bool useTestnet;
50 final Function(bool? val) toggleUseTestnet;
51 + final int zcashNetwork;
52 + final void Function(int network) setZcashNetwork;
53
54 @override
55 Widget body(BuildContext context) => _AdvancedPrivacySettingsBody(
@@ -51,6 +57,8 @@ class AdvancedPrivacySettingsPage extends BasePage {
57 isChildWallet,
58 useTestnet,
59 toggleUseTestnet,
60 + zcashNetwork,
61 + setZcashNetwork,
62 advancedPrivacySettingsViewModel,
63 nodeViewModel,
64 seedSettingsViewModel,
@@ -63,6 +71,8 @@ class _AdvancedPrivacySettingsBody extends StatefulWidget {
71 this.isChildWallet,
72 this.useTestnet,
73 this.toggleUseTestnet,
74 + this.zcashNetwork,
75 + this.setZcashNetwork,
76 this.privacySettingsViewModel,
77 this.nodeViewModel,
78 this.seedTypeViewModel,
@@ -76,6 +86,8 @@ class _AdvancedPrivacySettingsBody extends StatefulWidget {
86 final bool isChildWallet;
87 final bool useTestnet;
88 final Function(bool? val) toggleUseTestnet;
89 + final int zcashNetwork;
90 + final void Function(int network) setZcashNetwork;
91
92 @override
93 _AdvancedPrivacySettingsBodyState createState() => _AdvancedPrivacySettingsBodyState();
@@ -87,6 +99,7 @@ class _AdvancedPrivacySettingsBodyState extends State<_AdvancedPrivacySettingsBo
99 final _formKey = GlobalKey<NodeFormState>();
100 final _passphraseFormKey = GlobalKey<FormState>();
101 bool? testnetValue;
102 + int? zcashNetworkValue;
103
104 bool obscurePassphrase = true;
105
@@ -112,6 +125,7 @@ class _AdvancedPrivacySettingsBodyState extends State<_AdvancedPrivacySettingsBo
125 if (testnetValue == null && widget.useTestnet) {
126 testnetValue = widget.useTestnet;
127 }
128 + zcashNetworkValue ??= widget.zcashNetwork;
129
130 return Container(
131 padding: EdgeInsets.only(top: 24),
@@ -281,6 +295,20 @@ class _AdvancedPrivacySettingsBodyState extends State<_AdvancedPrivacySettingsBo
295 ],
296 );
297 }),
298 + if (FeatureFlag.hasDevOptions &&
299 + widget.privacySettingsViewModel.type == WalletType.zcash)
300 + SettingsChoicesCell(
301 + ChoicesListItem<int>(
302 + title: 'Zcash network',
303 + items: ZcashNetworkType.values,
304 + selectedItem: zcashNetworkValue ?? ZcashNetworkType.mainnet,
305 + displayItem: ZcashNetworkType.label,
306 + onItemSelected: (final network) {
307 + setState(() => zcashNetworkValue = network);
308 + widget.setZcashNetwork(network);
309 + },
310 + ),
311 + ),
312 if (widget.privacySettingsViewModel.type == WalletType.bitcoin ||
313 widget.privacySettingsViewModel.type == WalletType.decred)
314 Builder(builder: (_) {
lib/src/screens/new_wallet/new_wallet_page.dart
+2
@@ -316,6 +316,8 @@ class _WalletNameFormState extends State<WalletNameForm> {
316 "type": _walletNewVM.type,
317 "useTestnet": _walletNewVM.useTestnet,
318 "toggleTestnet": _walletNewVM.toggleUseTestnet,
319 + "zcashNetwork": _walletNewVM.zcashNetwork,
320 + "setZcashNetwork": _walletNewVM.setZcashNetwork,
321 "isChildWallet": widget.isChildWallet,
322 });
323 },
lib/src/screens/restore/wallet_restore_page.dart
+3 -1
@@ -121,7 +121,9 @@ class WalletRestorePage extends BasePage {
121 'isFromRestore': true,
122 'type': walletRestoreViewModel.type,
123 'useTestnet': walletRestoreViewModel.useTestnet,
124 - 'toggleTestnet': walletRestoreViewModel.toggleUseTestnet
124 + 'toggleTestnet': walletRestoreViewModel.toggleUseTestnet,
125 + 'zcashNetwork': walletRestoreViewModel.zcashNetwork,
126 + 'setZcashNetwork': walletRestoreViewModel.setZcashNetwork,
127 },
128 );
129 },
lib/view_model/dashboard/dashboard_view_model.dart
+10 -4
@@ -538,10 +538,16 @@ abstract class DashboardViewModelBase with Store {
538 .map((item) => _txIdentityString(item.transaction.txHash, item.transaction.direction))
539 .toSet();
540
541 - transactions.removeWhere(
542 - (item) => newKeys
543 - .contains(_txIdentityString(item.transaction.txHash, item.transaction.direction)),
544 - );
541 + transactions.removeWhere((item) {
542 + if (wallet.type == WalletType.zcash) {
543 + return newTransactions.any(
544 + (n) => n.transaction.txHash == item.transaction.txHash,
545 + );
546 + }
547 + return newKeys.contains(
548 + _txIdentityString(item.transaction.txHash, item.transaction.direction),
549 + );
550 + });
551
552 transactions.addAll(newTransactions);
553 // transactions.clear();
lib/view_model/dashboard/receive_option_view_model.dart
+20 -1
@@ -3,6 +3,7 @@ import 'package:cake_wallet/zcash/zcash.dart';
3 import 'package:cw_core/receive_page_option.dart';
4 import 'package:cw_core/wallet_base.dart';
5 import 'package:cw_core/wallet_type.dart';
6 +import 'package:cw_zcash/cw_zcash.dart';
7 import 'package:mobx/mobx.dart';
8
9 part 'receive_option_view_model.g.dart';
@@ -18,7 +19,14 @@ abstract class ReceiveOptionViewModelBase with Store {
19 ? ReceivePageOption.testnet
20 : _wallet.type == WalletType.zcash
21 ? zcash!.getSelectedAddressType(_wallet)
21 - : ReceivePageOption.mainnet);
22 + : ReceivePageOption.mainnet) {
23 + if (_wallet.type == WalletType.zcash) {
24 + reaction<bool>(
25 + (_) => (_wallet.walletAddresses as ZcashWalletAddresses).ironwoodActive,
26 + (_) => _syncZcashShieldedLabel(),
27 + );
28 + }
29 + }
30
31 final WalletBase _wallet;
32
@@ -27,10 +35,21 @@ abstract class ReceiveOptionViewModelBase with Store {
35 @observable
36 ReceivePageOption selectedReceiveOption;
37
38 + @computed
39 List<ReceivePageOption> get options => _wallet.walletAddresses.receivePageOptions;
40
41 String get walletTypeString => walletTypeToString(_wallet.type);
42
43 @action
44 void selectReceiveOption(ReceivePageOption option) => selectedReceiveOption = option;
45 +
46 + @action
47 + void _syncZcashShieldedLabel() {
48 + for (final option in options) {
49 + if (option.value == selectedReceiveOption.value) {
50 + selectedReceiveOption = option;
51 + return;
52 + }
53 + }
54 + }
55 }
lib/view_model/dashboard/transaction_list_item.dart
+3
@@ -60,6 +60,9 @@ class TransactionListItem extends ActionListItem with Keyable {
60 return 'Transaction has missing data';
61 }
62
63 + if (transaction.additionalInfo['isIronwoodMigration'] == true) {
64 + return 'Migration';
65 + }
66 if (transaction.additionalInfo['isAutoShield'] == true) {
67 if (transaction.isPending) {
68 final status = formattedStatus;
lib/view_model/send/send_template_view_model.dart
+5 -2
@@ -48,8 +48,11 @@ abstract class SendTemplateViewModelBase with Store {
48 amountParsingProxy: _appStore.amountParsingProxy,
49 );
50
51 - AddressValidator get addressValidator =>
52 - AddressValidator(type: _wallet.currency, isTestnet: _wallet.isTestnet);
51 + AddressValidator get addressValidator => AddressValidator(
52 + type: _wallet.currency,
53 + isTestnet: _wallet.isTestnet,
54 + network: _wallet.walletInfo.network,
55 + );
56
57 TemplateValidator get templateValidator => TemplateValidator();
58
lib/view_model/send/send_view_model.dart
+9 -3
@@ -190,7 +190,10 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
190
191 // Check if the address is valid for the current currency (except for Zano, which can use handles as addresses)
192 if (selectedCryptoCurrency != CryptoCurrency.zano) {
193 - final isValidAddress = AddressValidator(type: selectedCryptoCurrency).isValid(query);
193 + final isValidAddress = AddressValidator(
194 + type: selectedCryptoCurrency,
195 + network: wallet.walletInfo.network,
196 + ).isValid(query);
197 if (isValidAddress) return null;
198 }
199
@@ -317,8 +320,11 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
320
321 Validator<String> get allAmountValidator => AllAmountValidator();
322
320 - Validator<String> get addressValidator =>
321 - AddressValidator(type: selectedCryptoCurrency, isTestnet: wallet.isTestnet);
323 + Validator<String> get addressValidator => AddressValidator(
324 + type: selectedCryptoCurrency,
325 + isTestnet: wallet.isTestnet,
326 + network: wallet.walletInfo.network,
327 + );
328
329 Validator<String> get textValidator => TextValidator();
330
lib/view_model/transaction_details_view_model.dart
+3
@@ -428,6 +428,9 @@ abstract class TransactionDetailsViewModelBase with Store {
428 }
429
430 String get formattedTitle {
431 + if (transactionInfo.additionalInfo['isIronwoodMigration'] == true) {
432 + return 'Migration';
433 + }
434 if (transactionInfo.additionalInfo['isAutoShield'] == true) {
435 return S.current.shielding;
436 }
lib/view_model/wallet_creation_vm.dart
+9
@@ -17,6 +17,7 @@ import 'package:cw_core/wallet_base.dart';
17 import 'package:cw_core/wallet_credentials.dart';
18 import 'package:cw_core/wallet_info.dart';
19 import 'package:cw_core/wallet_type.dart';
20 +import 'package:cake_wallet/zcash/zcash_network_type.dart';
21 import 'package:mobx/mobx.dart';
22 import 'package:polyseed/polyseed.dart';
23
@@ -33,9 +34,13 @@ abstract class WalletCreationVMBase with Store {
34 @observable
35 bool _useTestnet = false;
36
37 + int _zcashNetwork = ZcashNetworkType.mainnet;
38 +
39 @computed
40 bool get useTestnet => _useTestnet;
41
42 + int get zcashNetwork => _zcashNetwork;
43 +
44 @observable
45 String name;
46
@@ -252,4 +257,8 @@ abstract class WalletCreationVMBase with Store {
257 void toggleUseTestnet(bool? value) {
258 _useTestnet = value ?? !_useTestnet;
259 }
260 +
261 + void setZcashNetwork(int network) {
262 + _zcashNetwork = network;
263 + }
264 }
lib/view_model/wallet_new_vm.dart
+1
@@ -146,6 +146,7 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store {
146 password: walletPassword,
147 mnemonic: newWalletArguments!.mnemonic,
148 passphrase: passphrase,
149 + network: zcashNetwork,
150 );
151 case WalletType.decred:
152 return decred!.createDecredNewWalletCredentials(name: name);
lib/view_model/wallet_restore_view_model.dart
+1
@@ -224,6 +224,7 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store {
224 password: password,
225 passphrase: passphrase,
226 height: height,
227 + network: zcashNetwork,
228 );
229 case WalletType.none:
230 case WalletType.haven:
lib/zcash/cw_zcash.dart
+13 -18
@@ -7,12 +7,14 @@ class CWZcash extends Zcash {
7 WalletInfo? walletInfo,
8 String? password,
9 String? mnemonic,
10 - required String? passphrase}) {
10 + required String? passphrase,
11 + int network = 0}) {
12 return ZcashNewWalletCredentials(
13 name: name,
14 passphrase: passphrase,
15 password: password,
16 mnemonic: mnemonic,
17 + network: network,
18 );
19 }
20
@@ -36,9 +38,10 @@ class CWZcash extends Zcash {
38 required String mnemonic,
39 required String password,
40 String? passphrase,
39 - required int? height}) {
41 + required int? height,
42 + int network = 0}) {
43 return ZcashFromSeedWalletCredentials(
41 - name: name, seed: mnemonic, passphrase: passphrase, password: password, height: height);
44 + name: name, seed: mnemonic, passphrase: passphrase, password: password, height: height, network: network);
45 }
46
47 @override
@@ -161,9 +164,12 @@ class CWZcash extends Zcash {
164 @override
165 ReceivePageOption getSelectedAddressType(Object wallet) {
166 final zcashWallet = wallet as ZcashWallet;
164 - final t =
165 - (zcashWallet.walletAddresses as ZcashWalletAddresses).walletInfo.addressPageType ?? "";
166 - return ZcashReceivePageOption.fromType(ZcashReceivePageOption.typeFromString(t));
167 + final addresses = zcashWallet.walletAddresses as ZcashWalletAddresses;
168 + final type = ZcashReceivePageOption.typeFromString(addresses.walletInfo.addressPageType ?? "");
169 + if (type == ZcashAddressType.shieldedOrchard) {
170 + return ZcashReceivePageOption.shieldedOrchard(ironwood: addresses.ironwoodActive);
171 + }
172 + return ZcashReceivePageOption.fromType(type);
173 }
174
175 bool hasSelectedTransparentAddress(Object wallet) {
@@ -172,18 +178,7 @@ class CWZcash extends Zcash {
178
179 @override
180 dynamic getZcashAddressType(ReceivePageOption option) {
175 - switch (option) {
176 - case ZcashReceivePageOption.unified:
177 - return ZcashAddressType.unifiedType;
178 - case ZcashReceivePageOption.transparent:
179 - return ZcashAddressType.transparent;
180 - case ZcashReceivePageOption.shieldedSapling:
181 - return ZcashAddressType.shieldedSapling;
182 - case ZcashReceivePageOption.shieldedOrchard:
183 - return ZcashAddressType.shieldedOrchard;
184 - default:
185 - throw Exception("Unknown ReceivePageOption!");
186 - }
181 + return (option as ZcashReceivePageOption).toType();
182 }
183
184 @override
lib/zcash/zcash_network_type.dart new
+17
@@ -0,0 +1,17 @@
1 +/// Zcash network indices passed from lib/ into cw_zcash.
2 +abstract final class ZcashNetworkType {
3 + static const int mainnet = 0;
4 + static const int testnet = 1;
5 + static const int regtest = 2;
6 +
7 + static const List<int> values = [mainnet, testnet, regtest];
8 +
9 + static String label(final int network) => switch (network) {
10 + testnet => 'Testnet',
11 + regtest => 'Regtest',
12 + _ => 'Mainnet',
13 + };
14 +
15 + static bool isDevNetwork(final String? network) =>
16 + network?.toLowerCase() == 'testnet' || network?.toLowerCase() == 'regtest';
17 +}
tool/configure.dart
+4 -2
@@ -1721,13 +1721,15 @@ abstract class Zcash {
1721 WalletInfo? walletInfo,
1722 String? password,
1723 String? mnemonic,
1724 - required String? passphrase});
1724 + required String? passphrase,
1725 + int network = 0});
1726 WalletCredentials createZcashRestoreWalletFromSeedCredentials(
1727 {required String name,
1728 required String mnemonic,
1729 required String password,
1730 String? passphrase,
1730 - required int? height});
1731 + required int? height,
1732 + int network = 0});
1733 WalletCredentials createZcashRestoreWalletFromPrivateKey(
1734 {required String name, required String privateKey, required String password, required int height});
1735 String getAddress(WalletBase wallet);