diff --git a/src/main/java/com/sparrowwallet/sparrow/control/AddressCell.java b/src/main/java/com/sparrowwallet/sparrow/control/AddressCell.java index 4e019b2e..7603d5e8 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/AddressCell.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/AddressCell.java @@ -1,12 +1,14 @@ package com.sparrowwallet.sparrow.control; import com.sparrowwallet.drongo.address.Address; +import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; import com.sparrowwallet.sparrow.wallet.Entry; import com.sparrowwallet.sparrow.wallet.UtxoEntry; import javafx.geometry.Pos; import javafx.scene.control.ContentDisplay; import javafx.scene.control.Tooltip; import javafx.scene.control.TreeTableCell; +import org.controlsfx.glyphfont.Glyph; public class AddressCell extends TreeTableCell { public AddressCell() { @@ -32,10 +34,37 @@ public class AddressCell extends TreeTableCell { setText(address.toString()); setContextMenu(new EntryCell.AddressContextMenu(address, utxoEntry.getOutputDescriptor())); Tooltip tooltip = new Tooltip(); - tooltip.setText(utxoEntry.getNode().getDerivationPath()); + tooltip.setText(getTooltipText(utxoEntry)); setTooltip(tooltip); + + if(utxoEntry.isDuplicateAddress()) { + setGraphic(getDuplicateGlyph()); + } else { + setGraphic(null); + } + + utxoEntry.duplicateAddressProperty().addListener((observable, oldValue, newValue) -> { + if(newValue) { + setGraphic(getDuplicateGlyph()); + Tooltip tt = new Tooltip(); + tt.setText(getTooltipText(utxoEntry)); + setTooltip(tt); + } else { + setGraphic(null); + } + }); } - setGraphic(null); } } + + private String getTooltipText(UtxoEntry utxoEntry) { + return utxoEntry.getNode().getDerivationPath() + (utxoEntry.isDuplicateAddress() ? " (Duplicate address)" : ""); + } + + private static Glyph getDuplicateGlyph() { + Glyph duplicateGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_CIRCLE); + duplicateGlyph.getStyleClass().add("duplicate-warning"); + duplicateGlyph.setFontSize(12); + return duplicateGlyph; + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/UtxoEntry.java b/src/main/java/com/sparrowwallet/sparrow/wallet/UtxoEntry.java index f239ea41..4b050088 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/UtxoEntry.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/UtxoEntry.java @@ -4,6 +4,8 @@ import com.sparrowwallet.drongo.address.Address; import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex; import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.drongo.wallet.WalletNode; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.BooleanPropertyBase; import javafx.collections.FXCollections; import javafx.collections.ObservableList; @@ -41,4 +43,37 @@ public class UtxoEntry extends HashIndexEntry { public String getOutputDescriptor() { return getWallet().getOutputDescriptor(node); } + + /** + * Defines whether this utxo shares it's address with another utxo in the wallet + */ + private BooleanProperty duplicateAddress; + + public final void setDuplicateAddress(boolean value) { + if(duplicateAddress != null || value) { + duplicateAddressProperty().set(value); + } + } + + public final boolean isDuplicateAddress() { + return duplicateAddress != null && duplicateAddress.get(); + } + + public final BooleanProperty duplicateAddressProperty() { + if(duplicateAddress == null) { + duplicateAddress = new BooleanPropertyBase(false) { + + @Override + public Object getBean() { + return UtxoEntry.this; + } + + @Override + public String getName() { + return "duplicate"; + } + }; + } + return duplicateAddress; + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletUtxosEntry.java b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletUtxosEntry.java index 5ebeabbd..a6a349f1 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletUtxosEntry.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletUtxosEntry.java @@ -14,6 +14,7 @@ public class WalletUtxosEntry extends Entry { public WalletUtxosEntry(Wallet wallet) { super(wallet.getName(), getWalletUtxos(wallet).entrySet().stream().map(entry -> new UtxoEntry(wallet, entry.getKey(), HashIndexEntry.Type.OUTPUT, entry.getValue())).collect(Collectors.toList())); this.wallet = wallet; + calculateDuplicates(); } public Wallet getWallet() { @@ -25,6 +26,23 @@ public class WalletUtxosEntry extends Entry { return 0L; } + protected void calculateDuplicates() { + Map addressMap = new HashMap<>(); + + for(Entry entry : getChildren()) { + UtxoEntry utxoEntry = (UtxoEntry)entry; + String address = utxoEntry.getAddress().toString(); + + UtxoEntry duplicate = addressMap.get(address); + if(duplicate != null) { + duplicate.setDuplicateAddress(true); + utxoEntry.setDuplicateAddress(true); + } else { + addressMap.put(address, utxoEntry); + } + } + } + public void updateUtxos() { List current = getWalletUtxos(wallet).entrySet().stream().map(entry -> new UtxoEntry(wallet, entry.getKey(), HashIndexEntry.Type.OUTPUT, entry.getValue())).collect(Collectors.toList()); List previous = new ArrayList<>(getChildren()); @@ -36,6 +54,8 @@ public class WalletUtxosEntry extends Entry { List entriesRemoved = new ArrayList<>(previous); entriesRemoved.removeAll(current); getChildren().removeAll(entriesRemoved); + + calculateDuplicates(); } private static Map getWalletUtxos(Wallet wallet) { diff --git a/src/main/resources/com/sparrowwallet/sparrow/wallet/wallet.css b/src/main/resources/com/sparrowwallet/sparrow/wallet/wallet.css index 5c66eec1..3142f373 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/wallet/wallet.css +++ b/src/main/resources/com/sparrowwallet/sparrow/wallet/wallet.css @@ -101,3 +101,7 @@ .tree-table-row-cell:selected .entry-cell:hover .button .label .text { -fx-fill: white; } + +.duplicate-warning { + -fx-text-fill: rgb(202, 18, 67); +}