From dc2134172590fcb04ac43db3db0df4d500b1784b Mon Sep 17 00:00:00 2001 From: cyan Date: Tue, 4 Aug 2026 01:09:34 +0200 Subject: [PATCH] fix: hide dust balance from "confirming" (#3456) --- cw_zcash/lib/src/zcash_taddress_rotation.dart | 4 +- cw_zcash/lib/src/zcash_wallet.dart | 102 ++++++++++++------ 2 files changed, 70 insertions(+), 36 deletions(-) diff --git a/cw_zcash/lib/src/zcash_taddress_rotation.dart b/cw_zcash/lib/src/zcash_taddress_rotation.dart index cbbb0b0e5..e0b344e8c 100644 --- a/cw_zcash/lib/src/zcash_taddress_rotation.dart +++ b/cw_zcash/lib/src/zcash_taddress_rotation.dart @@ -34,7 +34,7 @@ class ZcashTaddressRotation { static bool _isStarted = false; static zkool_coin.Coin get c => ZcashWalletBase.c; static const int _sweepThreshold = 30000; - static const int _minSpendableNote = 5000; + static const int minSpendableNote = 5000; static const int _lookahead = 5; // Matches transparentLimit in ZcashWalletBase._oneshotSync. static const int _transparentSyncLimit = 100; @@ -415,7 +415,7 @@ class ZcashTaddressRotation { if (note.pool != NotePool.transparent.index || note.locked) { continue; } - if (note.value < BigInt.from(_minSpendableNote)) { + if (note.value < BigInt.from(minSpendableNote)) { continue; } if (note.height > height) { diff --git a/cw_zcash/lib/src/zcash_wallet.dart b/cw_zcash/lib/src/zcash_wallet.dart index cc6df83e4..6a09724fa 100644 --- a/cw_zcash/lib/src/zcash_wallet.dart +++ b/cw_zcash/lib/src/zcash_wallet.dart @@ -83,6 +83,12 @@ abstract class ZcashWalletBase static const int _autoShieldMinSweep = 30000; + // zkool's migrate::MIN_SD. + static const int _ironwoodMigrateMinNote = 500000; + + static int _minSweepThreshold({required final bool ironwood}) => + ironwood ? _ironwoodMigrateMinNote : _autoShieldMinSweep; + Money _feeFromTxPlan( final zkool_pay.PcztPackage txPlan, final TransactionPriority priority, @@ -469,8 +475,7 @@ abstract class ZcashWalletBase } // pools parameter: bitmask for which pools to use for sending - // 1=Transparent, 2=Sapling, 4=Orchard, 7=All pools - // Using 7 (all pools) allows spending from any pool type + // 1=Transparent, 2=Sapling, 4=Orchard, 8=Ironwood try { return await runWithCoin( accountId: accountId, @@ -479,7 +484,7 @@ abstract class ZcashWalletBase final txPlan = await zkool_pay.prepare( recipients: recipients, options: zkool_pay.PaymentOptions( - srcPools: ironwood ? 15 : 7, + srcPools: ironwood ? 8 : 4, recipientPaysFee: receipientPaysFee, smartTransparent: false, mode: 0, @@ -1175,6 +1180,40 @@ abstract class ZcashWalletBase } } + /// Total of transparent + sapling notes at or above the per-note spendable floor. + static Future _sweepableTotal(final zkool_coin.Coin coin) async { + final notes = await zkool_account.listNotes(c: coin); + BigInt sweepable = BigInt.zero; + for (int i = 0; i < notes.length; i++) { + final note = notes[i]; + if (note.pool < 0 || note.pool >= NotePool.values.length) { + continue; + } + final noteType = NotePool.values[note.pool]; + if ((noteType == NotePool.transparent || noteType == NotePool.sapling) && + note.value >= BigInt.from(ZcashTaddressRotation.minSpendableNote)) { + sweepable += note.value; + } + } + return sweepable; + } + + /// Orchard notes that migration will actually split or move to Ironwood. + static Future _migratableOrchardTotal(final zkool_coin.Coin coin) async { + final notes = await zkool_account.listNotes(c: coin); + BigInt migratable = BigInt.zero; + for (int i = 0; i < notes.length; i++) { + final note = notes[i]; + if (note.pool != NotePool.orchard.index || note.locked) { + continue; + } + if (note.value >= BigInt.from(_ironwoodMigrateMinNote)) { + migratable += note.value; + } + } + return migratable; + } + Future _$autoShield() async { if (syncStatus is! SyncedSyncStatus) { return; @@ -1182,24 +1221,12 @@ abstract class ZcashWalletBase final txId = await runWithCoin( accountId: accountId, func: (coin) async { - final _notes = await zkool_account.listNotes(c: coin); - BigInt sweepable = BigInt.zero; - for (int i = 0; i < _notes.length; i++) { - final note = _notes[i]; - if (note.pool < 0 || note.pool >= NotePool.values.length) { - continue; - } - final noteType = NotePool.values[note.pool]; - if (noteType == NotePool.transparent || noteType == NotePool.sapling) { - sweepable += note.value; - } - } + final sweepable = await _sweepableTotal(coin); + final ironwood = await zkool_network.isIronwoodActive(c: coin); - if (sweepable <= BigInt.from(_autoShieldMinSweep)) { + if (sweepable <= BigInt.from(_minSweepThreshold(ironwood: ironwood))) { return null; } - - final ironwood = await zkool_network.isIronwoodActive(c: coin); final txPlan = await zkool_pay.prepare( recipients: [ zkool_paydart.Recipient( @@ -1332,17 +1359,6 @@ abstract class ZcashWalletBase }) async { try { await _updateIronwoodActive(); - final bal = await runWithCoin( - accountId: accountId, - func: (final coin) async => zkool_sync.balance(c: coin), - ); - - // 0 - transparent, 1 - sapling, 2 - orchard, 3 - ironwood - final transparent = bal.field0[0]; - final sapling = bal.field0.length > 1 ? bal.field0[1] : BigInt.zero; - final orchard = bal.field0.length > 2 ? bal.field0[2] : BigInt.zero; - final ironwood = bal.field0.length > 3 ? bal.field0[3] : BigInt.zero; - if (runAutoShield) { await _autoShield(); } @@ -1350,18 +1366,36 @@ abstract class ZcashWalletBase await _ironwoodMigrate(); } + final (bal, sweepable, migratableOrchard) = await runWithCoin( + accountId: accountId, + func: (final coin) async => ( + await zkool_sync.balance(c: coin), + await _sweepableTotal(coin), + await _migratableOrchardTotal(coin), + ), + ); + + // 0 - transparent, 1 - sapling, 2 - orchard, 3 - ironwood + final orchard = bal.field0.length > 2 ? bal.field0[2] : BigInt.zero; + final ironwood = bal.field0.length > 3 ? bal.field0[3] : BigInt.zero; + // After NU6.3, Orchard notes are migrated to Ironwood - show them as unconfirmed. + // Unavailable uses the same per-note totals and thresholds as auto-shield/migration guards. final BigInt availableAmount; final BigInt unavailableAmount; if (ironwoodActive == true && orchard > BigInt.zero) { - availableAmount = sapling + ironwood; - unavailableAmount = transparent + orchard; + final sweepableUnavailable = sweepable <= BigInt.from(_ironwoodMigrateMinNote) + ? BigInt.zero + : sweepable; + availableAmount = ironwood; + unavailableAmount = migratableOrchard + sweepableUnavailable; } else { - availableAmount = sapling + orchard + ironwood; - unavailableAmount = transparent; + final minSweep = _minSweepThreshold(ironwood: ironwoodActive == true); + availableAmount = orchard + ironwood; + unavailableAmount = sweepable <= BigInt.from(minSweep) ? BigInt.zero : sweepable; } - balance[CryptoCurrency.zec] = ZcashBalance( + balance[currency] = ZcashBalance( Money(availableAmount, currency), Money(unavailableAmount, currency), frozen: Money.zero(currency),