fix: display proper address in zcash transactions (#3457)
cyan committed
Aug 7, 2026 at 02:28 UTC
c93843f3347a0d8f816000eb84190f4fa494f363
2 files changed
+98
-107
cw_zcash/lib/src/zcash_wallet.dart
+97
-106
@@ -521,10 +521,10 @@ abstract class ZcashWalletBase
521
522
static const _dispPhrase = "Received to disposable address";
523
524
- bool _hasExternalOutputs(final ZkoolTx tx) =>
525
- tx.outputsWithAddress.any((final o) => !_addressBelongsToWallet(o.address));
524
+ bool _hasExternalOutputs(final ZkoolTx tx, final Set<String> ownedAddresses) =>
525
+ tx.outputsWithAddress.any((final o) => !_isOwnedAddress(o.address, ownedAddresses));
526
527
- bool _isIronwoodMigrationTx(final ZkoolTx tx) {
527
+ bool _isIronwoodMigrationTx(final ZkoolTx tx, final Set<String> ownedAddresses) {
528
// zkool classifies migration as selfTransfer with fee-sized net value.
529
if (tx.type != TxType.selfTransfer) {
530
return false;
@@ -540,7 +540,7 @@ abstract class ZcashWalletBase
540
return true;
541
}
542
// Orchard → Orchard split step (SD notes created, all outputs are ours).
543
- if (tx.orchardReceived > BigInt.zero && !_hasExternalOutputs(tx)) {
543
+ if (tx.orchardReceived > BigInt.zero && !_hasExternalOutputs(tx, ownedAddresses)) {
544
return true;
545
}
546
// Orchard → Ironwood before the IW note is attached to tx details.
@@ -577,10 +577,7 @@ abstract class ZcashWalletBase
577
.whereType<String>()
578
.where((final m) => m.isNotEmpty)
579
.firstOrNull;
580
- final recipientAddresses = _recipientAddresses(
581
- accountNotes.map((final n) => n.address).whereType<String>(),
582
- direction,
583
- );
580
+ final recipientAddresses = const <String>[];
581
582
final info = ZcashTransactionInfo(
583
id: txHash,
@@ -608,17 +605,20 @@ abstract class ZcashWalletBase
605
final bool isShieldAction = false,
606
final TransactionDirection? directionOverride,
607
final BigInt? amountOverride,
608
+ required final Set<String> ownedAddresses,
609
}) {
610
final confirmations = tx.height > 0 && currentHeight >= tx.height
611
? currentHeight - tx.height + 1
612
: 0;
613
final memo = extraMemo != null ? "${tx.memo ?? ''}\n$extraMemo".trim() : tx.memo;
616
- final isMigration = directionOverride == null && _isIronwoodMigrationTx(tx);
614
+ final isMigration = directionOverride == null && _isIronwoodMigrationTx(tx, ownedAddresses);
615
final direction = directionOverride ??
616
(isMigration ? TransactionDirection.outgoing : tx.direction);
619
- final recipientAddresses = _recipientAddresses(_paymentOutputAddresses(tx), direction);
617
final amount = amountOverride ??
618
(isMigration ? _migrationDisplayAmount(tx) : tx.value);
619
+ final recipientAddresses = direction == TransactionDirection.outgoing
620
+ ? _outgoingRecipientAddresses(tx, ownedAddresses: ownedAddresses)
621
+ : const <String>[];
622
final info = ZcashTransactionInfo(
623
id: tx.txHash,
624
amount: Money(amount, currency),
@@ -641,14 +641,18 @@ abstract class ZcashWalletBase
641
return info;
642
}
643
644
- bool _isShieldActionTx(final ZkoolTx tx, {required final Set<String> rotationSweepHashes}) {
644
+ bool _isShieldActionTx(
645
+ final ZkoolTx tx, {
646
+ required final Set<String> rotationSweepHashes,
647
+ required final Set<String> ownedAddresses,
648
+ }) {
649
if (ZcashWalletService.isAutoshieldTx(tx.txHash)) {
650
return true;
651
}
652
if (rotationSweepHashes.contains(tx.txHash)) {
653
return true;
654
}
651
- if (_isPayToSelfAutoshield(tx)) {
655
+ if (_isPayToSelfAutoshield(tx, ownedAddresses)) {
656
return true;
657
}
658
if (tx.direction == TransactionDirection.outgoing &&
@@ -658,7 +662,7 @@ abstract class ZcashWalletBase
662
return false;
663
}
664
661
- bool _isPayToSelfAutoshield(final ZkoolTx tx) {
665
+ bool _isPayToSelfAutoshield(final ZkoolTx tx, final Set<String> ownedAddresses) {
666
if (tx.type != TxType.shield && tx.type != TxType.transparentSelfTransfer) {
667
return false;
668
}
@@ -669,18 +673,23 @@ abstract class ZcashWalletBase
673
return false;
674
}
675
for (final dest in tx.outputAddresses) {
672
- if (_addressBelongsToWallet(dest)) {
676
+ if (_isOwnedAddress(dest, ownedAddresses)) {
677
return true;
678
}
679
}
680
return tx.orchardReceived > BigInt.zero;
681
}
682
679
- bool _shouldSplitAutoshieldTx(final ZkoolTx tx, {required final bool isShield}) {
683
+ bool _shouldSplitAutoshieldTx(
684
+ final ZkoolTx tx, {
685
+ required final bool isShield,
686
+ required final Set<String> ownedAddresses,
687
+ }) {
688
if (!isShield) {
689
return false;
690
}
683
- if (ZcashWalletService.isAutoshieldTx(tx.txHash) || _isPayToSelfAutoshield(tx)) {
691
+ if (ZcashWalletService.isAutoshieldTx(tx.txHash) ||
692
+ _isPayToSelfAutoshield(tx, ownedAddresses)) {
693
return tx.transparentOrSaplingSpent > BigInt.zero && tx.orchardReceived > BigInt.zero;
694
}
695
return false;
@@ -724,104 +733,57 @@ abstract class ZcashWalletBase
733
}
734
}
735
727
- bool _addressBelongsToWallet(final String addr) {
728
- final addrs = walletAddresses;
729
- if (addrs.containsAddress(addr) ||
730
- addrs.hiddenAddresses.contains(addr) ||
731
- addrs.usedAddresses.contains(addr)) {
732
- return true;
736
+ Future<Set<String>> _ownedAddressSet(final zkool_coin.Coin coin) async {
737
+ final owned = <String>{};
738
+ for (final address in await zkool_account.listOwnedAddresses(c: coin)) {
739
+ if (address.isEmpty) {
740
+ continue;
741
+ }
742
+ owned.add(address);
743
+ if (address.startsWith('u')) {
744
+ owned.addAll(_uaReceivers(address));
745
+ }
746
}
747
+ final addrs = walletAddresses;
748
for (final infos in addrs.addressInfos.values) {
749
for (final info in infos) {
736
- if (info.address == addr) {
737
- return true;
738
- }
750
+ owned.add(info.address);
751
}
752
}
741
- for (final own in [
742
- addrs.orchardAddress,
743
- addrs.unifiedAddress,
744
- addrs.saplingAddress,
745
- addrs.transparentAddress,
746
- addrs.address,
747
- ]) {
748
- if (own == null || own.isEmpty || own.startsWith('unknown ')) {
749
- continue;
750
- }
751
- if (addr == own || addr.startsWith(own) || own.startsWith(addr)) {
752
- return true;
753
- }
754
- }
755
- return false;
753
+ owned.addAll(addrs.hiddenAddresses);
754
+ owned.addAll(addrs.usedAddresses);
755
+ return owned;
756
}
757
758
- List<String> _paymentOutputAddresses(final ZkoolTx tx) {
759
- final all = tx.outputsWithAddress.toList();
760
- final external = all.where((final o) => !_addressBelongsToWallet(o.address)).toList();
761
- final outputs = external.isNotEmpty ? external : all;
762
- final transparent = outputs.where((final o) => o.pool == NotePool.transparent.index).toList();
758
+ bool _isOwnedAddress(final String addr, final Set<String> ownedAddresses) =>
759
+ ownedAddresses.contains(addr);
760
+
761
+ List<String> _outgoingRecipientAddresses(
762
+ final ZkoolTx tx, {
763
+ required final Set<String> ownedAddresses,
764
+ }) {
765
+ var outputs = tx.outputsWithAddress
766
+ .where((final o) => !_isOwnedAddress(o.address, ownedAddresses))
767
+ .toList();
768
+ if (outputs.isEmpty) {
769
+ return [];
770
+ }
771
+
772
+ final transparent =
773
+ outputs.where((final o) => o.pool == NotePool.transparent.index).toList();
774
if (transparent.length >= 2) {
764
- return transparent.map((final o) => o.address).toList();
775
+ outputs = transparent;
776
}
766
- return outputs.map((final o) => o.address).toList();
777
+
778
+ return _dedupeAddresses(outputs.map((final o) => o.address));
779
}
780
769
- List<String> _recipientAddresses(
770
- final Iterable<String> raw,
771
- final TransactionDirection direction,
772
- ) {
781
+ List<String> _dedupeAddresses(final Iterable<String> raw) {
782
final seen = <String>{};
774
- final addresses = [
783
+ return [
784
for (final address in raw)
785
if (address.trim().isNotEmpty && seen.add(address.trim())) address.trim(),
786
];
778
- if (addresses.isEmpty) {
779
- return [];
780
- }
781
-
782
- final external = direction == TransactionDirection.incoming
783
- ? addresses
784
- : [
785
- for (final address in addresses)
786
- if (!_addressBelongsToWallet(address)) address,
787
- ];
788
- final list = external.isNotEmpty ? external : addresses;
789
- if (list.isEmpty) {
790
- return [];
791
- }
792
-
793
- final embedded = {
794
- for (final address in list)
795
- if (address.startsWith('u')) ..._uaReceivers(address),
796
- };
797
- var result = embedded.isEmpty
798
- ? list
799
- : [
800
- for (final address in list)
801
- if (address.startsWith('u') || !embedded.contains(address)) address,
802
- ];
803
- if (result.isEmpty) {
804
- result = list;
805
- }
806
-
807
- final standalone = [
808
- for (final address in result)
809
- if (!address.startsWith('u') && !embedded.contains(address)) address,
810
- ];
811
- if (standalone.length >= 2) {
812
- result = standalone;
813
- }
814
-
815
- if (direction == TransactionDirection.incoming) {
816
- return [
817
- result.firstWhere(
818
- (final a) => a.startsWith('u'),
819
- orElse: () =>
820
- result.firstWhere((final a) => a.startsWith('z'), orElse: () => result.first),
821
- ),
822
- ];
823
- }
824
- return result;
787
}
788
789
Set<String> _uaReceivers(final String ua) {
@@ -839,9 +801,10 @@ abstract class ZcashWalletBase
801
@override
802
Future<Map<String, ZcashTransactionInfo>> fetchTransactions() async {
803
await ZcashWalletService.loadShieldTxs();
842
- final (txs, currentHeight) = await runWithCoin(
804
+ final (txs, currentHeight, ownedAddresses) = await runWithCoin(
805
accountId: accountId,
806
func: (coin) async {
807
+ final owned = await _ownedAddressSet(coin);
808
final txsI = await zkool_account.listTxHistory(c: coin);
809
final txsA = await Future.wait(
810
txsI.map((final tx) => zkool_account.getTxDetails(idTx: tx.id, c: coin)),
@@ -857,7 +820,7 @@ abstract class ZcashWalletBase
820
} catch (e) {
821
printV("failed to get height: $e");
822
}
860
- return (txs, currentHeight);
823
+ return (txs, currentHeight, owned);
824
},
825
);
826
final Map<String, ZcashTransactionInfo> byHash = {};
@@ -871,11 +834,25 @@ abstract class ZcashWalletBase
834
if (tx.direction == TransactionDirection.incoming) {
835
_offerTx(
836
byHash,
874
- _zcashInfoFromZkoolTx(tx, currentHeight, extraMemo: _dispPhrase, isRotationReceive: true),
837
+ _zcashInfoFromZkoolTx(
838
+ tx,
839
+ currentHeight,
840
+ extraMemo: _dispPhrase,
841
+ isRotationReceive: true,
842
+ ownedAddresses: ownedAddresses,
843
+ ),
844
);
845
continue;
846
}
878
- _offerTx(byHash, _zcashInfoFromZkoolTx(tx, currentHeight, isShieldAction: true));
847
+ _offerTx(
848
+ byHash,
849
+ _zcashInfoFromZkoolTx(
850
+ tx,
851
+ currentHeight,
852
+ isShieldAction: true,
853
+ ownedAddresses: ownedAddresses,
854
+ ),
855
+ );
856
}
857
858
final Map<String, ZcashTransactionInfo> splitEntries = {};
@@ -884,8 +861,12 @@ abstract class ZcashWalletBase
861
if (tx.height > 0) {
862
ZcashMempoolService.instance.removeTx(tx.txHash);
863
}
887
- final isShield = _isShieldActionTx(tx, rotationSweepHashes: rotationSweepHashes);
888
- if (_shouldSplitAutoshieldTx(tx, isShield: isShield)) {
864
+ final isShield = _isShieldActionTx(
865
+ tx,
866
+ rotationSweepHashes: rotationSweepHashes,
867
+ ownedAddresses: ownedAddresses,
868
+ );
869
+ if (_shouldSplitAutoshieldTx(tx, isShield: isShield, ownedAddresses: ownedAddresses)) {
870
byHash.remove(tx.txHash);
871
splitEntries[_txResultKey(tx.txHash, suffix: '_shield')] = _zcashInfoFromZkoolTx(
872
tx,
@@ -893,16 +874,26 @@ abstract class ZcashWalletBase
874
isShieldAction: true,
875
directionOverride: TransactionDirection.outgoing,
876
amountOverride: tx.transparentOrSaplingSpent,
877
+ ownedAddresses: ownedAddresses,
878
);
879
splitEntries[_txResultKey(tx.txHash, suffix: '_recv')] = _zcashInfoFromZkoolTx(
880
tx,
881
currentHeight,
882
directionOverride: TransactionDirection.incoming,
883
amountOverride: tx.orchardReceived,
884
+ ownedAddresses: ownedAddresses,
885
);
886
continue;
887
}
905
- _offerTx(byHash, _zcashInfoFromZkoolTx(tx, currentHeight, isShieldAction: isShield));
888
+ _offerTx(
889
+ byHash,
890
+ _zcashInfoFromZkoolTx(
891
+ tx,
892
+ currentHeight,
893
+ isShieldAction: isShield,
894
+ ownedAddresses: ownedAddresses,
895
+ ),
896
+ );
897
}
898
899
final knownHashes = {
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: 6329d4aacb4568ee80ff84baba5ea93b07ae1013
24
+ ref: 16fd3d11575f850939f498c3ea75d391915456d1
25
26
path_provider: any
27
sqflite_common_ffi: any