diff --git a/src/main/java/com/sparrowwallet/sparrow/control/AddressCell.java b/src/main/java/com/sparrowwallet/sparrow/control/AddressCell.java index 33af4bf2..f65130f9 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/AddressCell.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/AddressCell.java @@ -37,11 +37,13 @@ public class AddressCell extends TreeTableCell { setContextMenu(new EntryCell.AddressContextMenu(address, utxoEntry.getOutputDescriptor(), new NodeEntry(utxoEntry.getWallet(), utxoEntry.getNode()))); Tooltip tooltip = new Tooltip(); tooltip.setShowDelay(Duration.millis(250)); - tooltip.setText(getTooltipText(utxoEntry, addressStatus.isDuplicate())); + tooltip.setText(getTooltipText(utxoEntry, addressStatus.isDuplicate(), addressStatus.isDustAttack())); setTooltip(tooltip); if(addressStatus.isDuplicate()) { setGraphic(getDuplicateGlyph()); + } else if(addressStatus.isDustAttack()) { + setGraphic(getDustAttackGlyph()); } else { setGraphic(null); } @@ -49,9 +51,9 @@ public class AddressCell extends TreeTableCell { } } - private String getTooltipText(UtxoEntry utxoEntry, boolean duplicate) { + private String getTooltipText(UtxoEntry utxoEntry, boolean duplicate, boolean dustAttack) { return (utxoEntry.getNode().getWallet().isNested() ? utxoEntry.getNode().getWallet().getDisplayName() + " " : "" ) + - utxoEntry.getNode().toString() + (duplicate ? " (Duplicate address)" : ""); + utxoEntry.getNode().toString() + (duplicate ? " (Duplicate address)" : (dustAttack ? " (Possible dust attack)" : "")); } public static Glyph getDuplicateGlyph() { @@ -60,4 +62,11 @@ public class AddressCell extends TreeTableCell { duplicateGlyph.setFontSize(12); return duplicateGlyph; } + + public static Glyph getDustAttackGlyph() { + Glyph dustAttackGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_TRIANGLE); + dustAttackGlyph.getStyleClass().add("dust-attack-warning"); + dustAttackGlyph.setFontSize(12); + return dustAttackGlyph; + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Config.java b/src/main/java/com/sparrowwallet/sparrow/io/Config.java index 728c3a79..dad23f04 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/Config.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/Config.java @@ -18,6 +18,7 @@ import java.util.stream.Collectors; import static com.sparrowwallet.sparrow.AppServices.ENUMERATE_HW_PERIOD_SECS; import static com.sparrowwallet.sparrow.net.PagedBatchRequestBuilder.DEFAULT_PAGE_SIZE; import static com.sparrowwallet.sparrow.net.TcpTransport.DEFAULT_MAX_TIMEOUT; +import static com.sparrowwallet.sparrow.wallet.WalletUtxosEntry.DUST_ATTACK_THRESHOLD_SATS; public class Config { private static final Logger log = LoggerFactory.getLogger(Config.class); @@ -45,6 +46,7 @@ public class Config { private boolean preventSleep = false; private List recentWalletFiles; private Integer keyDerivationPeriod; + private long dustAttackThreshold = DUST_ATTACK_THRESHOLD_SATS; private File hwi; private int enumerateHwPeriod = ENUMERATE_HW_PERIOD_SECS; private Boolean hdCapture; @@ -302,6 +304,10 @@ public class Config { flush(); } + public long getDustAttackThreshold() { + return dustAttackThreshold; + } + public File getHwi() { return hwi; } diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/UtxoEntry.java b/src/main/java/com/sparrowwallet/sparrow/wallet/UtxoEntry.java index 01e615da..90d4f88a 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/UtxoEntry.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/UtxoEntry.java @@ -69,7 +69,13 @@ public class UtxoEntry extends HashIndexEntry { private ObjectProperty addressStatusProperty; public final void setDuplicateAddress(boolean value) { - addressStatusProperty().set(new AddressStatus(value)); + AddressStatus addressStatus = addressStatusProperty().get(); + addressStatusProperty().set(new AddressStatus(value, addressStatus.dustAttack)); + } + + public final void setDustAttack(boolean value) { + AddressStatus addressStatus = addressStatusProperty().get(); + addressStatusProperty().set(new AddressStatus(addressStatus.duplicate, value)); } public final boolean isDuplicateAddress() { @@ -78,7 +84,7 @@ public class UtxoEntry extends HashIndexEntry { public final ObjectProperty addressStatusProperty() { if(addressStatusProperty == null) { - addressStatusProperty = new SimpleObjectProperty<>(UtxoEntry.this, "addressStatus", new AddressStatus(false)); + addressStatusProperty = new SimpleObjectProperty<>(UtxoEntry.this, "addressStatus", new AddressStatus(false, false)); } return addressStatusProperty; @@ -86,9 +92,11 @@ public class UtxoEntry extends HashIndexEntry { public class AddressStatus { private final boolean duplicate; + private final boolean dustAttack; - public AddressStatus(boolean duplicate) { + public AddressStatus(boolean duplicate, boolean dustAttack) { this.duplicate = duplicate; + this.dustAttack = dustAttack; } public UtxoEntry getUtxoEntry() { @@ -102,6 +110,10 @@ public class UtxoEntry extends HashIndexEntry { public boolean isDuplicate() { return duplicate; } + + public boolean isDustAttack() { + return dustAttack; + } } /** diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletUtxosEntry.java b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletUtxosEntry.java index f4ad539a..f6bb238a 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletUtxosEntry.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletUtxosEntry.java @@ -2,16 +2,21 @@ package com.sparrowwallet.sparrow.wallet; import com.samourai.whirlpool.client.wallet.beans.MixProgress; import com.sparrowwallet.drongo.wallet.Wallet; +import com.sparrowwallet.drongo.wallet.WalletNode; import com.sparrowwallet.sparrow.AppServices; +import com.sparrowwallet.sparrow.io.Config; import com.sparrowwallet.sparrow.whirlpool.Whirlpool; import java.util.*; import java.util.stream.Collectors; public class WalletUtxosEntry extends Entry { + public static final int DUST_ATTACK_THRESHOLD_SATS = 1000; + public WalletUtxosEntry(Wallet wallet) { super(wallet, wallet.getName(), wallet.getWalletUtxos().entrySet().stream().map(entry -> new UtxoEntry(entry.getValue().getWallet(), entry.getKey(), HashIndexEntry.Type.OUTPUT, entry.getValue())).collect(Collectors.toList())); calculateDuplicates(); + calculateDust(); updateMixProgress(); } @@ -48,6 +53,18 @@ public class WalletUtxosEntry extends Entry { } } + protected void calculateDust() { + long dustAttackThreshold = Config.get().getDustAttackThreshold(); + Set duplicateNodes = getWallet().getWalletTxos().values().stream() + .collect(Collectors.groupingBy(e -> e, Collectors.counting())) + .entrySet().stream().filter(e -> e.getValue() > 1).map(Map.Entry::getKey).collect(Collectors.toSet()); + + for(Entry entry : getChildren()) { + UtxoEntry utxoEntry = (UtxoEntry) entry; + utxoEntry.setDustAttack(utxoEntry.getValue() <= dustAttackThreshold && duplicateNodes.contains(utxoEntry.getNode())); + } + } + public void updateMixProgress() { Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(getWallet()); if(whirlpool != null) { @@ -74,6 +91,7 @@ public class WalletUtxosEntry extends Entry { getChildren().removeAll(entriesRemoved); calculateDuplicates(); + calculateDust(); updateMixProgress(); } diff --git a/src/main/resources/com/sparrowwallet/sparrow/wallet/wallet.css b/src/main/resources/com/sparrowwallet/sparrow/wallet/wallet.css index db702313..3e910e2f 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/wallet/wallet.css +++ b/src/main/resources/com/sparrowwallet/sparrow/wallet/wallet.css @@ -123,6 +123,10 @@ -fx-text-fill: rgb(202, 18, 67); } +.dust-attack-warning { + -fx-text-fill: rgb(238, 210, 2); +} + .unused-check { -fx-text-fill: #50a14f; }