From 8168c98c6fa95ebb2b57e0f1ab848f482cbfaaca Mon Sep 17 00:00:00 2001 From: Roger <214061537+Arbeitszeit@users.noreply.github.com> Date: Sat, 4 Jul 2026 16:23:32 -0300 Subject: [PATCH 1/2] fix Zcash transparent address metadata --- cw_core/lib/wallet_info.dart | 4 ++ cw_zcash/lib/src/zcash_taddress_rotation.dart | 28 +++++++- cw_zcash/lib/src/zcash_wallet.dart | 14 ++-- cw_zcash/lib/src/zcash_wallet_addresses.dart | 67 ++++++++++++++----- ...let_address_edit_or_create_view_model.dart | 12 ++++ .../wallet_address_list_view_model.dart | 16 ++++- 6 files changed, 116 insertions(+), 25 deletions(-) diff --git a/cw_core/lib/wallet_info.dart b/cw_core/lib/wallet_info.dart index 5877ad2dab..13a74053c2 100644 --- a/cw_core/lib/wallet_info.dart +++ b/cw_core/lib/wallet_info.dart @@ -61,6 +61,8 @@ class WalletInfoAddressInfo { required this.accountIndex, required this.address, required this.label, + this.txCount, + this.balance, }); int id; @@ -69,6 +71,8 @@ class WalletInfoAddressInfo { int accountIndex; String address; String label; + int? txCount; + int? balance; static String get tableName => 'walletInfoAddressInfo'; diff --git a/cw_zcash/lib/src/zcash_taddress_rotation.dart b/cw_zcash/lib/src/zcash_taddress_rotation.dart index 1c2ef1f205..bbdd6c0e82 100644 --- a/cw_zcash/lib/src/zcash_taddress_rotation.dart +++ b/cw_zcash/lib/src/zcash_taddress_rotation.dart @@ -54,6 +54,27 @@ class ZcashTaddressRotation { static Map> rotationAccounts = {}; static Map> rotationAccountsUsable = {}; static Map> shieldedAccountsTx = {}; + static final Map addressBalances = {}; + static final Map addressTxCounts = {}; + + static void refreshAddressMetadata(final Account account) { + try { + final address = WarpApi.getTAddr(coin, account.id); + addressBalances[address] = + WarpApi.getPoolBalances(coin, account.id, 0, true).transparent; + addressTxCounts[address] = WarpApi.getTxsSync(coin, account.id).length; + } catch (e) { + printV("Failed to refresh metadata for Zcash account ${account.id}: $e"); + } + } + + static int? balanceForAddress(final String address) => addressBalances[address]; + + static int? txCountForAddress(final String address) => addressTxCounts[address]; + + static List accountsForAccount(final int accountId) => + rotationAccounts[accountId]?.toList() ?? []; + static Future init() async { printV("Deserializing previous state"); if (_isStarted) { @@ -416,6 +437,7 @@ class ZcashTaddressRotation { 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)}"); + refreshAddressMetadata(acc[i]); } return acc.map((final a) => WarpApi.getTAddr(coin, a.id)).toList(); } @@ -423,14 +445,14 @@ class ZcashTaddressRotation { static List? allUsedAddressesForAccount(final int accountId) { final seed = WarpApi.getBackup(coin, accountId).seed; if (seed == null) return []; - final acc = rotationAccounts[seed]?.toList(); + final acc = rotationAccounts[accountId]?.toList(); if (acc == null) { printV("Nothing found"); return null; } acc.removeWhere((final a1) { - for (int i = 0; i < (rotationAccountsUsable[seed]?.length ?? 0); i++) { - if (rotationAccountsUsable[seed]?[i].id == a1.id) { + for (int i = 0; i < (rotationAccountsUsable[accountId]?.length ?? 0); i++) { + if (rotationAccountsUsable[accountId]?[i].id == a1.id) { return true; } } diff --git a/cw_zcash/lib/src/zcash_wallet.dart b/cw_zcash/lib/src/zcash_wallet.dart index d255ae7fbe..d4f331a916 100644 --- a/cw_zcash/lib/src/zcash_wallet.dart +++ b/cw_zcash/lib/src/zcash_wallet.dart @@ -784,21 +784,27 @@ abstract class ZcashWalletBase final confirmedPoolBalances = WarpApi.getPoolBalances(coin, accountId, 3, true); final confirmedBalances = confirmedPoolBalances.unpack(); - final confirmedTotal = - confirmedBalances.orchard + confirmedBalances.sapling + confirmedBalances.transparent; + + int rotatedTransparentBalance = 0; + for (final account in ZcashTaddressRotation.accountsForAccount(accountId)) { + final accountBalances = WarpApi.getPoolBalances(coin, account.id, 0, true); + rotatedTransparentBalance += accountBalances.transparent; + ZcashTaddressRotation.refreshAddressMetadata(account); + } int knownOutPending = 0; ZcashWalletBase.temporarySentTx[accountId]?.forEach((final sTx) { knownOutPending += sTx.value; // it's negative }); - final confirmedSpendable = confirmedTotal - balances.transparent + knownOutPending; + final confirmedSpendable = + confirmedBalances.orchard + confirmedBalances.sapling + knownOutPending; unawaited(_autoShield()); balance[CryptoCurrency.zec] = ZcashBalance( Money.fromInt(confirmedSpendable, currency), Money.fromInt(spendable - confirmedSpendable, currency), - frozen: Money.zero(currency), + frozen: Money.fromInt(balances.transparent + rotatedTransparentBalance, currency), ); } catch (e, stackTrace) { printV("Balance update error: $e"); diff --git a/cw_zcash/lib/src/zcash_wallet_addresses.dart b/cw_zcash/lib/src/zcash_wallet_addresses.dart index a9fd207ae9..48502bc4c1 100644 --- a/cw_zcash/lib/src/zcash_wallet_addresses.dart +++ b/cw_zcash/lib/src/zcash_wallet_addresses.dart @@ -131,7 +131,11 @@ abstract class ZcashWalletAddressesBase extends WalletAddresses with Store { @override bool containsAddress(final String address) { - return this.address == address || addressesMap.values.contains(address); + return this.address == address || + addressesMap.values.contains(address) || + addressInfos.values.any((final infos) { + return infos.any((final info) => info.address == address); + }); } static int get coin => ZcashWalletBase.coin; @@ -149,25 +153,42 @@ abstract class ZcashWalletAddressesBase extends WalletAddresses with Store { address = latestAddress; } + Map _labelsByAddress() { + final labels = {}; + for (final infos in addressInfos.values) { + for (final info in infos) { + labels[info.address] = info.label; + } + } + return labels; + } + + Map> _buildTransparentAddressInfos( + final Map labels, + ) { + int mapKey = 0; + final infos = ZcashTaddressRotation.accountsForAccount(accountId).map((final account) { + final address = WarpApi.getTAddr(coin, account.id); + ZcashTaddressRotation.refreshAddressMetadata(account); + return WalletInfoAddressInfo( + walletInfoId: walletInfo.internalId, + mapKey: ++mapKey, + accountIndex: account.id, + address: address, + label: labels[address] ?? "", + txCount: ZcashTaddressRotation.txCountForAddress(address), + balance: ZcashTaddressRotation.balanceForAddress(address), + ); + }).toList(); + return {0: infos}; + } + @override Future 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() ?? - [], - }; + addressInfos = _buildTransparentAddressInfos(_labelsByAddress()); hiddenAddresses.addAll( ZcashTaddressRotation.allUsedAddressesForAccount(accountId)?.toSet() ?? {}, ); @@ -227,8 +248,24 @@ abstract class ZcashWalletAddressesBase extends WalletAddresses with Store { if (addressPageType != ZcashAddressType.transparentRotated) { return []; } + final knownAddresses = addressInfos.values + .expand((final infos) => infos) + .map((final info) => info.address) + .toSet(); + final currentAddresses = ZcashTaddressRotation.accountsForAccount(accountId) + .map((final account) => WarpApi.getTAddr(coin, account.id)) + .toSet(); + if (knownAddresses.length != currentAddresses.length || + !knownAddresses.containsAll(currentAddresses)) { + addressInfos = _buildTransparentAddressInfos(_labelsByAddress()); + } + final List allInfos = []; for (final entry in addressInfos.entries) { + for (final info in entry.value) { + info.txCount = ZcashTaddressRotation.txCountForAddress(info.address); + info.balance = ZcashTaddressRotation.balanceForAddress(info.address); + } allInfos.addAll(entry.value); } return allInfos; diff --git a/lib/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart b/lib/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart index 52c5954d89..6a7b617acb 100644 --- a/lib/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart +++ b/lib/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart @@ -121,6 +121,18 @@ abstract class WalletAddressEditOrCreateViewModelBase with Store { return; } + if (wallet.type == WalletType.zcash) { + for (final infos in wallet.walletAddresses.addressInfos.values) { + for (final info in infos) { + if (info.address == _item!.address) { + info.label = label; + } + } + } + await wallet.walletAddresses.saveAddressesInBox(); + return; + } + final index = _item?.id; if (index != null) { if (wallet.type == WalletType.monero) { diff --git a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart index b69558bffb..c49d18ff4f 100644 --- a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart +++ b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart @@ -368,8 +368,17 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo if (wallet.type == WalletType.zcash) { final addrInfos = zcash!.getAddressInfos(wallet); addrInfos.forEach((info) { - addressList.add( - new WalletAddressListItem(isPrimary: false, address: info.address, name: info.label)); + addressList.add(WalletAddressListItem( + id: info.mapKey, + isPrimary: false, + address: info.address, + name: info.label, + txCount: info.txCount, + balance: info.balance == null + ? null + : _appStore.amountParsingProxy + .getDisplayCryptoString(info.balance!, walletTypeToCryptoCurrency(type)), + )); }); } @@ -566,7 +575,8 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo String get monoImage => getChainMonoImage(type); @computed - bool get isBalanceAvailable => isElectrumWallet; + bool get isBalanceAvailable => + isElectrumWallet || (wallet.type == WalletType.zcash && isZCashTransparent); @computed bool get isReceivedAvailable => [WalletType.monero, WalletType.wownero].contains(wallet.type); From d9720d22ea66618be47aa44b98bf44fc97706c09 Mon Sep 17 00:00:00 2001 From: Roger <214061537+Arbeitszeit@users.noreply.github.com> Date: Sat, 4 Jul 2026 16:25:28 -0300 Subject: [PATCH 2/2] add manual Zcash transparent address generation --- cw_zcash/lib/src/zcash_taddress_rotation.dart | 128 ++++++++++++++++-- cw_zcash/lib/src/zcash_wallet_addresses.dart | 9 ++ lib/new-ui/pages/receive_page.dart | 24 +++- .../wallet_address_list_view_model.dart | 14 +- lib/zcash/cw_zcash.dart | 7 + tool/configure.dart | 1 + 6 files changed, 165 insertions(+), 18 deletions(-) diff --git a/cw_zcash/lib/src/zcash_taddress_rotation.dart b/cw_zcash/lib/src/zcash_taddress_rotation.dart index bbdd6c0e82..cb590efc59 100644 --- a/cw_zcash/lib/src/zcash_taddress_rotation.dart +++ b/cw_zcash/lib/src/zcash_taddress_rotation.dart @@ -56,6 +56,9 @@ class ZcashTaddressRotation { static Map> shieldedAccountsTx = {}; static final Map addressBalances = {}; static final Map addressTxCounts = {}; + static const int minimumUsableAddressPool = 5; + static const int recoveryGapLimit = 20; + static const int maximumManualUnusedGap = recoveryGapLimit - 1; static void refreshAddressMetadata(final Account account) { try { @@ -75,6 +78,38 @@ class ZcashTaddressRotation { static List accountsForAccount(final int accountId) => rotationAccounts[accountId]?.toList() ?? []; + static Future generateAddressForAccount(final int accountId) async { + final mainSeed = WarpApi.getBackup(coin, accountId).seed; + if (mainSeed == null) { + throw StateError("Transparent address generation requires a wallet seed"); + } + + rotationAccounts[accountId] ??= WarpApi.getAccountList(coin).where((final account) { + return isSeedForWallet(mainSeed, WarpApi.getBackup(coin, account.id).seed); + }).toList(); + + if (_trailingUnusedCount(accountId) >= maximumManualUnusedGap) { + throw StateError( + "Use one of the existing transparent addresses before generating another", + ); + } + + final id = await _createRotationAccount(accountId, mainSeed); + final account = WarpApi.getAccountList(coin).firstWhere((final item) => item.id == id); + final accounts = rotationAccounts[accountId]!; + if (!accounts.any((final item) => item.id == account.id)) { + accounts.add(account); + } + rotationAccountsUsable[accountId] ??= []; + if (!rotationAccountsUsable[accountId]!.any((final item) => item.id == account.id)) { + rotationAccountsUsable[accountId]!.add(account); + } + + refreshAddressMetadata(account); + await serializeToFile(); + return WarpApi.getTAddr(coin, account.id); + } + static Future init() async { printV("Deserializing previous state"); if (_isStarted) { @@ -193,6 +228,68 @@ class ZcashTaddressRotation { static Uint8List atob(final String value) => Uint8List.fromList(List.from(base64.decode(value))); + static List _sortedAccountsFor(final int accountId) { + final accounts = rotationAccounts[accountId]?.toList() ?? []; + accounts.sort((final a, final b) { + final aIndex = WarpApi.getBackup(coin, a.id).index; + final bIndex = WarpApi.getBackup(coin, b.id).index; + return aIndex.compareTo(bIndex); + }); + return accounts; + } + + static bool _isUnused(final Account account) { + refreshAddressMetadata(account); + final address = WarpApi.getTAddr(coin, account.id); + return (addressTxCounts[address] ?? 0) == 0 && (addressBalances[address] ?? 0) == 0; + } + + static int _trailingUnusedCount(final int accountId) { + final accounts = _sortedAccountsFor(accountId); + int count = 0; + for (final account in accounts.reversed) { + if (!_isUnused(account)) break; + count++; + } + return count; + } + + static int _nextAccountIndex(final int accountId) { + int nextIndex = 0; + for (final account in rotationAccounts[accountId] ?? []) { + final index = WarpApi.getBackup(coin, account.id).index; + if (index >= nextIndex) nextIndex = index + 1; + } + return nextIndex; + } + + static Future _createRotationAccount(final int accountId, final String mainSeed) { + final seed = seedForOffset(mainSeed); + final name = CRC32.compute(accountId.toString()).toString(); + final index = _nextAccountIndex(accountId); + return ZcashWalletService.runInDbMutex( + () => WarpApi.newAccount(coin, name, seed, index), + ); + } + + static Future _isRecoveryAccount(final int accountId) async { + final accounts = WarpApi.getAccountList(coin); + String? accountName; + for (final account in accounts) { + if (account.id == accountId) { + accountName = account.name; + break; + } + } + if (accountName == null) return false; + + final walletInfos = await WalletInfo.selectList('type = ?', [WalletType.zcash.index]); + for (final walletInfo in walletInfos) { + if (walletInfo.name == accountName) return walletInfo.isRecovery; + } + return false; + } + static Future createAndSweepTAddresses() async { int chainHeight = 0; try { @@ -217,11 +314,13 @@ class ZcashTaddressRotation { final acc = accounts[i]; final backup = WarpApi.getBackup(coin, acc.id); await WarpApi.transparentSync(coin, acc.id, syncHeight); + refreshAddressMetadata(acc); if (backup.seed == null) continue; seeds[acc.id] = backup.seed!; } for (int i = 0; i < accounts.length; i++) { - final seed = seeds[accounts[i].id]!; + final seed = seeds[accounts[i].id]; + if (seed == null) continue; if ([12, 13, 24, 25].contains(seed.split(" ").length)) { if (seed.split(" ").last.contains(":tgen:")) continue; rotationAccounts[accounts[i].id] = []; @@ -276,13 +375,26 @@ class ZcashTaddressRotation { 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"); + final accountId = raKeys[i]; + final mainSeed = seeds[accountId]; + if (mainSeed == null || !await _isRecoveryAccount(accountId)) { + continue; + } + + if (_trailingUnusedCount(accountId) < recoveryGapLimit) { + await _createRotationAccount(accountId, mainSeed); + didAddNewAccount = true; + } + } + if (didAddNewAccount) { + return createAndSweepTAddresses(); + } + + for (int i = 0; i < raKeys.length; i++) { + if (rotationAccountsUsable[raKeys[i]]!.length < minimumUsableAddressPool && + _trailingUnusedCount(raKeys[i]) < maximumManualUnusedGap) { + final id = await _createRotationAccount(raKeys[i], seeds[raKeys[i]]!); + printV("new rotation account id: $id"); printV("${rotationAccounts[raKeys[i]]}"); printV(raKeys[i]); rotationAccounts[raKeys[i]]!.forEach((final a) { diff --git a/cw_zcash/lib/src/zcash_wallet_addresses.dart b/cw_zcash/lib/src/zcash_wallet_addresses.dart index 48502bc4c1..30e1aa16c1 100644 --- a/cw_zcash/lib/src/zcash_wallet_addresses.dart +++ b/cw_zcash/lib/src/zcash_wallet_addresses.dart @@ -271,6 +271,15 @@ abstract class ZcashWalletAddressesBase extends WalletAddresses with Store { return allInfos; } + Future generateNewTransparentAddress() async { + final labels = _labelsByAddress(); + final newAddress = await ZcashTaddressRotation.generateAddressForAccount(accountId); + addressInfos = _buildTransparentAddressInfos(labels); + await saveAddressesInBox(); + address = newAddress; + return newAddress; + } + @override List get receivePageOptions { return [ diff --git a/lib/new-ui/pages/receive_page.dart b/lib/new-ui/pages/receive_page.dart index 84d1e935ae..2d5e022b1a 100644 --- a/lib/new-ui/pages/receive_page.dart +++ b/lib/new-ui/pages/receive_page.dart @@ -213,15 +213,25 @@ class _NewReceivePageState extends State { icon: _largeQrMode ? Icon(Icons.share) : widget.addressListViewModel.isRotatingAddress ? CupertinoActivityIndicator() : Icon(Icons.refresh), - onPressed: () { + onPressed: () async { if(_largeQrMode) { - ShareUtil.share( - text: widget.addressListViewModel.uri.toString(), - context: context, + ShareUtil.share( + text: widget.addressListViewModel.uri.toString(), + context: context, + ); + } else if(widget.addressListViewModel.hasAddressRotation) { + try { + await widget.addressListViewModel.rotateAddress(); + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + e.toString().replaceFirst("Bad state: ", ""), + ), + ), ); - } else { - if(widget.addressListViewModel.hasAddressRotation) { - widget.addressListViewModel.rotateAddress(); + } } } }):SizedBox.shrink(), diff --git a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart index c49d18ff4f..bb39c46304 100644 --- a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart +++ b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart @@ -483,7 +483,7 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo ].contains(wallet.type) && !isLightning && isZCashTransparent; @computed - bool get hasAddressRotation => hasAddressList && wallet.type != WalletType.zcash; + bool get hasAddressRotation => hasAddressList; @computed bool get isElectrumWallet => [ @@ -634,8 +634,16 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo } try { isRotatingAddress = true; - await createNewAddress(wallet, ""); - wallet.walletAddresses.address = addressList.whereType().last.address; + if (wallet.type == WalletType.zcash) { + if (!isZCashTransparent) { + throw StateError("A disposable transparent address type must be selected"); + } + wallet.walletAddresses.address = await zcash!.generateNewTransparentAddress(wallet); + } else { + await createNewAddress(wallet, ""); + wallet.walletAddresses.address = + addressList.whereType().last.address; + } } finally { isRotatingAddress = false; } diff --git a/lib/zcash/cw_zcash.dart b/lib/zcash/cw_zcash.dart index 9e4af1b29f..3d8f3b51e4 100644 --- a/lib/zcash/cw_zcash.dart +++ b/lib/zcash/cw_zcash.dart @@ -146,6 +146,13 @@ class CWZcash extends Zcash { .getAddressInfos(); } + @override + Future generateNewTransparentAddress(Object wallet) { + final zcashWallet = wallet as ZcashWallet; + return (zcashWallet.walletAddresses as ZcashWalletAddresses) + .generateNewTransparentAddress(); + } + @override TransactionPriority getDefaultTransactionPriority() { return MoneroTransactionPriority.automatic; diff --git a/tool/configure.dart b/tool/configure.dart index 54febefbbc..c6a0cf3eff 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -1751,6 +1751,7 @@ abstract class Zcash { String formatterZcashAmountToString({required int amount}); List getAddressInfos(Object wallet); + Future generateNewTransparentAddress(Object wallet); TransactionPriority getDefaultTransactionPriority(); TransactionPriority getZcashTransactionPriorityAutomatic();