fix: hide dust balance from "confirming" (#3456)
cyan committed
Aug 4, 2026 at 01:09 UTC
dc2134172590fcb04ac43db3db0df4d500b1784b
2 files changed
+70
-36
cw_zcash/lib/src/zcash_taddress_rotation.dart
+2
-2
@@ -34,7 +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;
37
+ static const int minSpendableNote = 5000;
38
static const int _lookahead = 5;
39
// Matches transparentLimit in ZcashWalletBase._oneshotSync.
40
static const int _transparentSyncLimit = 100;
@@ -415,7 +415,7 @@ class ZcashTaddressRotation {
415
if (note.pool != NotePool.transparent.index || note.locked) {
416
continue;
417
}
418
- if (note.value < BigInt.from(_minSpendableNote)) {
418
+ if (note.value < BigInt.from(minSpendableNote)) {
419
continue;
420
}
421
if (note.height > height) {
cw_zcash/lib/src/zcash_wallet.dart
+68
-34
@@ -83,6 +83,12 @@ abstract class ZcashWalletBase
83
84
static const int _autoShieldMinSweep = 30000;
85
86
+ // zkool's migrate::MIN_SD.
87
+ static const int _ironwoodMigrateMinNote = 500000;
88
+
89
+ static int _minSweepThreshold({required final bool ironwood}) =>
90
+ ironwood ? _ironwoodMigrateMinNote : _autoShieldMinSweep;
91
+
92
Money _feeFromTxPlan(
93
final zkool_pay.PcztPackage txPlan,
94
final TransactionPriority priority,
@@ -469,8 +475,7 @@ abstract class ZcashWalletBase
475
}
476
477
// pools parameter: bitmask for which pools to use for sending
472
- // 1=Transparent, 2=Sapling, 4=Orchard, 7=All pools
473
- // Using 7 (all pools) allows spending from any pool type
478
+ // 1=Transparent, 2=Sapling, 4=Orchard, 8=Ironwood
479
try {
480
return await runWithCoin(
481
accountId: accountId,
@@ -479,7 +484,7 @@ abstract class ZcashWalletBase
484
final txPlan = await zkool_pay.prepare(
485
recipients: recipients,
486
options: zkool_pay.PaymentOptions(
482
- srcPools: ironwood ? 15 : 7,
487
+ srcPools: ironwood ? 8 : 4,
488
recipientPaysFee: receipientPaysFee,
489
smartTransparent: false,
490
mode: 0,
@@ -1175,6 +1180,40 @@ abstract class ZcashWalletBase
1180
}
1181
}
1182
1183
+ /// Total of transparent + sapling notes at or above the per-note spendable floor.
1184
+ static Future<BigInt> _sweepableTotal(final zkool_coin.Coin coin) async {
1185
+ final notes = await zkool_account.listNotes(c: coin);
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];
1193
+ if ((noteType == NotePool.transparent || noteType == NotePool.sapling) &&
1194
+ note.value >= BigInt.from(ZcashTaddressRotation.minSpendableNote)) {
1195
+ sweepable += note.value;
1196
+ }
1197
+ }
1198
+ return sweepable;
1199
+ }
1200
+
1201
+ /// Orchard notes that migration will actually split or move to Ironwood.
1202
+ static Future<BigInt> _migratableOrchardTotal(final zkool_coin.Coin coin) async {
1203
+ final notes = await zkool_account.listNotes(c: coin);
1204
+ BigInt migratable = BigInt.zero;
1205
+ for (int i = 0; i < notes.length; i++) {
1206
+ final note = notes[i];
1207
+ if (note.pool != NotePool.orchard.index || note.locked) {
1208
+ continue;
1209
+ }
1210
+ if (note.value >= BigInt.from(_ironwoodMigrateMinNote)) {
1211
+ migratable += note.value;
1212
+ }
1213
+ }
1214
+ return migratable;
1215
+ }
1216
+
1217
Future<void> _$autoShield() async {
1218
if (syncStatus is! SyncedSyncStatus) {
1219
return;
@@ -1182,24 +1221,12 @@ abstract class ZcashWalletBase
1221
final txId = await runWithCoin(
1222
accountId: accountId,
1223
func: (coin) async {
1185
- final _notes = await zkool_account.listNotes(c: coin);
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];
1193
- if (noteType == NotePool.transparent || noteType == NotePool.sapling) {
1194
- sweepable += note.value;
1195
- }
1196
- }
1224
+ final sweepable = await _sweepableTotal(coin);
1225
+ final ironwood = await zkool_network.isIronwoodActive(c: coin);
1226
1198
- if (sweepable <= BigInt.from(_autoShieldMinSweep)) {
1227
+ if (sweepable <= BigInt.from(_minSweepThreshold(ironwood: ironwood))) {
1228
return null;
1229
}
1201
-
1202
- final ironwood = await zkool_network.isIronwoodActive(c: coin);
1230
final txPlan = await zkool_pay.prepare(
1231
recipients: [
1232
zkool_paydart.Recipient(
@@ -1332,17 +1359,6 @@ abstract class ZcashWalletBase
1359
}) async {
1360
try {
1361
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
-
1362
if (runAutoShield) {
1363
await _autoShield();
1364
}
@@ -1350,18 +1366,36 @@ abstract class ZcashWalletBase
1366
await _ironwoodMigrate();
1367
}
1368
1369
+ final (bal, sweepable, migratableOrchard) = await runWithCoin(
1370
+ accountId: accountId,
1371
+ func: (final coin) async => (
1372
+ await zkool_sync.balance(c: coin),
1373
+ await _sweepableTotal(coin),
1374
+ await _migratableOrchardTotal(coin),
1375
+ ),
1376
+ );
1377
+
1378
+ // 0 - transparent, 1 - sapling, 2 - orchard, 3 - ironwood
1379
+ final orchard = bal.field0.length > 2 ? bal.field0[2] : BigInt.zero;
1380
+ final ironwood = bal.field0.length > 3 ? bal.field0[3] : BigInt.zero;
1381
+
1382
// After NU6.3, Orchard notes are migrated to Ironwood - show them as unconfirmed.
1383
+ // Unavailable uses the same per-note totals and thresholds as auto-shield/migration guards.
1384
final BigInt availableAmount;
1385
final BigInt unavailableAmount;
1386
if (ironwoodActive == true && orchard > BigInt.zero) {
1357
- availableAmount = sapling + ironwood;
1358
- unavailableAmount = transparent + orchard;
1387
+ final sweepableUnavailable = sweepable <= BigInt.from(_ironwoodMigrateMinNote)
1388
+ ? BigInt.zero
1389
+ : sweepable;
1390
+ availableAmount = ironwood;
1391
+ unavailableAmount = migratableOrchard + sweepableUnavailable;
1392
} else {
1360
- availableAmount = sapling + orchard + ironwood;
1361
- unavailableAmount = transparent;
1393
+ final minSweep = _minSweepThreshold(ironwood: ironwoodActive == true);
1394
+ availableAmount = orchard + ironwood;
1395
+ unavailableAmount = sweepable <= BigInt.from(minSweep) ? BigInt.zero : sweepable;
1396
}
1397
1364
- balance[CryptoCurrency.zec] = ZcashBalance(
1398
+ balance[currency] = ZcashBalance(
1399
Money(availableAmount, currency),
1400
Money(unavailableAmount, currency),
1401
frozen: Money.zero(currency),