mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-12-24 20:56:45 +00:00
add option to optimize transactions for privacy and display privacy analysis
This commit is contained in:
parent
b22e891b7d
commit
7371ca2994
9 changed files with 281 additions and 32 deletions
2
drongo
2
drongo
|
@ -1 +1 @@
|
||||||
Subproject commit 81c202198e8b057271414d15259df556a90bc6f1
|
Subproject commit 7ac4bce14f04163c57b94e34945b5e4a1bf79eb6
|
|
@ -2,13 +2,13 @@ package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
|
import javafx.beans.property.ObjectProperty;
|
||||||
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
import javafx.beans.property.SimpleStringProperty;
|
import javafx.beans.property.SimpleStringProperty;
|
||||||
import javafx.beans.property.StringProperty;
|
import javafx.beans.property.StringProperty;
|
||||||
import javafx.event.EventHandler;
|
import javafx.scene.Node;
|
||||||
import javafx.geometry.Bounds;
|
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.control.Tooltip;
|
import javafx.scene.control.Tooltip;
|
||||||
import javafx.scene.input.MouseEvent;
|
|
||||||
import javafx.util.Duration;
|
import javafx.util.Duration;
|
||||||
import org.controlsfx.glyphfont.Glyph;
|
import org.controlsfx.glyphfont.Glyph;
|
||||||
|
|
||||||
|
@ -19,6 +19,7 @@ public class HelpLabel extends Label {
|
||||||
super("", getHelpGlyph());
|
super("", getHelpGlyph());
|
||||||
tooltip = new Tooltip();
|
tooltip = new Tooltip();
|
||||||
tooltip.textProperty().bind(helpTextProperty());
|
tooltip.textProperty().bind(helpTextProperty());
|
||||||
|
tooltip.graphicProperty().bind(helpGraphicProperty());
|
||||||
tooltip.setShowDuration(Duration.seconds(15));
|
tooltip.setShowDuration(Duration.seconds(15));
|
||||||
getStyleClass().add("help-label");
|
getStyleClass().add("help-label");
|
||||||
|
|
||||||
|
@ -49,4 +50,18 @@ public class HelpLabel extends Label {
|
||||||
public final String getHelpText() {
|
public final String getHelpText() {
|
||||||
return helpText == null ? "" : helpText.getValue();
|
return helpText == null ? "" : helpText.getValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ObjectProperty<Node> helpGraphicProperty() {
|
||||||
|
if(helpGraphicProperty == null) {
|
||||||
|
helpGraphicProperty = new SimpleObjectProperty<Node>(this, "helpGraphic", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return helpGraphicProperty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ObjectProperty<Node> helpGraphicProperty;
|
||||||
|
|
||||||
|
public final void setHelpGraphic(Node graphic) {
|
||||||
|
helpGraphicProperty().setValue(graphic);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -162,28 +162,28 @@ public class TransactionDiagram extends GridPane {
|
||||||
topYaxis.setStartX(width * 0.5);
|
topYaxis.setStartX(width * 0.5);
|
||||||
topYaxis.setStartY(getDiagramHeight() * 0.5 - 20.0);
|
topYaxis.setStartY(getDiagramHeight() * 0.5 - 20.0);
|
||||||
topYaxis.setEndX(width * 0.5);
|
topYaxis.setEndX(width * 0.5);
|
||||||
topYaxis.setEndY(0);
|
topYaxis.setEndY(10);
|
||||||
topYaxis.getStyleClass().add("inputs-type");
|
topYaxis.getStyleClass().add("inputs-type");
|
||||||
|
|
||||||
Line topBracket = new Line();
|
Line topBracket = new Line();
|
||||||
topBracket.setStartX(width * 0.5);
|
topBracket.setStartX(width * 0.5);
|
||||||
topBracket.setStartY(0);
|
topBracket.setStartY(10);
|
||||||
topBracket.setEndX(width);
|
topBracket.setEndX(width);
|
||||||
topBracket.setEndY(0);
|
topBracket.setEndY(10);
|
||||||
topBracket.getStyleClass().add("inputs-type");
|
topBracket.getStyleClass().add("inputs-type");
|
||||||
|
|
||||||
Line bottomYaxis = new Line();
|
Line bottomYaxis = new Line();
|
||||||
bottomYaxis.setStartX(width * 0.5);
|
bottomYaxis.setStartX(width * 0.5);
|
||||||
bottomYaxis.setStartY(getDiagramHeight());
|
bottomYaxis.setStartY(getDiagramHeight() - 10);
|
||||||
bottomYaxis.setEndX(width * 0.5);
|
bottomYaxis.setEndX(width * 0.5);
|
||||||
bottomYaxis.setEndY(getDiagramHeight() * 0.5 + 20.0);
|
bottomYaxis.setEndY(getDiagramHeight() * 0.5 + 20.0);
|
||||||
bottomYaxis.getStyleClass().add("inputs-type");
|
bottomYaxis.getStyleClass().add("inputs-type");
|
||||||
|
|
||||||
Line bottomBracket = new Line();
|
Line bottomBracket = new Line();
|
||||||
bottomBracket.setStartX(width * 0.5);
|
bottomBracket.setStartX(width * 0.5);
|
||||||
bottomBracket.setStartY(getDiagramHeight());
|
bottomBracket.setStartY(getDiagramHeight() - 10);
|
||||||
bottomBracket.setEndX(width);
|
bottomBracket.setEndX(width);
|
||||||
bottomBracket.setEndY(getDiagramHeight());
|
bottomBracket.setEndY(getDiagramHeight() - 10);
|
||||||
bottomBracket.getStyleClass().add("inputs-type");
|
bottomBracket.getStyleClass().add("inputs-type");
|
||||||
|
|
||||||
group.getChildren().addAll(widthLine, topYaxis, topBracket, bottomYaxis, bottomBracket);
|
group.getChildren().addAll(widthLine, topYaxis, topBracket, bottomYaxis, bottomBracket);
|
||||||
|
@ -344,7 +344,7 @@ public class TransactionDiagram extends GridPane {
|
||||||
group.getChildren().add(yaxisLine);
|
group.getChildren().add(yaxisLine);
|
||||||
|
|
||||||
double width = 140.0;
|
double width = 140.0;
|
||||||
int numOutputs = displayedPayments.size() + (walletTx.getChangeNode() == null ? 1 : 2);
|
int numOutputs = displayedPayments.size() + walletTx.getChangeMap().size() + 1;
|
||||||
for(int i = 1; i <= numOutputs; i++) {
|
for(int i = 1; i <= numOutputs; i++) {
|
||||||
CubicCurve curve = new CubicCurve();
|
CubicCurve curve = new CubicCurve();
|
||||||
curve.getStyleClass().add("output-line");
|
curve.getStyleClass().add("output-line");
|
||||||
|
@ -391,15 +391,16 @@ public class TransactionDiagram extends GridPane {
|
||||||
outputsBox.getChildren().add(createSpacer());
|
outputsBox.getChildren().add(createSpacer());
|
||||||
}
|
}
|
||||||
|
|
||||||
if(walletTx.getChangeNode() != null) {
|
for(Map.Entry<WalletNode, Long> changeEntry : walletTx.getChangeMap().entrySet()) {
|
||||||
|
WalletNode changeNode = changeEntry.getKey();
|
||||||
WalletNode defaultChangeNode = walletTx.getWallet().getFreshNode(KeyPurpose.CHANGE);
|
WalletNode defaultChangeNode = walletTx.getWallet().getFreshNode(KeyPurpose.CHANGE);
|
||||||
boolean overGapLimit = (walletTx.getChangeNode().getIndex() - defaultChangeNode.getIndex()) > walletTx.getWallet().getGapLimit();
|
boolean overGapLimit = (changeNode.getIndex() - defaultChangeNode.getIndex()) > walletTx.getWallet().getGapLimit();
|
||||||
|
|
||||||
HBox actionBox = new HBox();
|
HBox actionBox = new HBox();
|
||||||
String changeDesc = walletTx.getChangeAddress().toString().substring(0, 8) + "...";
|
String changeDesc = walletTx.getChangeAddress(changeNode).toString().substring(0, 8) + "...";
|
||||||
Label changeLabel = new Label(changeDesc, overGapLimit ? getChangeWarningGlyph() : getChangeGlyph());
|
Label changeLabel = new Label(changeDesc, overGapLimit ? getChangeWarningGlyph() : getChangeGlyph());
|
||||||
changeLabel.getStyleClass().addAll("output-label", "change-label");
|
changeLabel.getStyleClass().addAll("output-label", "change-label");
|
||||||
Tooltip changeTooltip = new Tooltip("Change of " + getSatsValue(walletTx.getChangeAmount()) + " sats to " + walletTx.getChangeNode().getDerivationPath().replace("m", "..") + "\n" + walletTx.getChangeAddress().toString() + (overGapLimit ? "\nAddress is beyond the gap limit!" : ""));
|
Tooltip changeTooltip = new Tooltip("Change of " + getSatsValue(changeEntry.getValue()) + " sats to " + changeNode.getDerivationPath().replace("m", "..") + "\n" + walletTx.getChangeAddress(changeNode).toString() + (overGapLimit ? "\nAddress is beyond the gap limit!" : ""));
|
||||||
changeTooltip.getStyleClass().add("change-label");
|
changeTooltip.getStyleClass().add("change-label");
|
||||||
changeTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
|
changeTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
|
||||||
changeLabel.setTooltip(changeTooltip);
|
changeLabel.setTooltip(changeTooltip);
|
||||||
|
@ -469,7 +470,9 @@ public class TransactionDiagram extends GridPane {
|
||||||
}
|
}
|
||||||
|
|
||||||
public Glyph getOutputGlyph(Payment payment) {
|
public Glyph getOutputGlyph(Payment payment) {
|
||||||
if(walletTx.isConsolidationSend(payment)) {
|
if(payment.getType().equals(Payment.Type.FAKE_MIX)) {
|
||||||
|
return getFakeMixGlyph();
|
||||||
|
} else if(walletTx.isConsolidationSend(payment)) {
|
||||||
return getConsolidationGlyph();
|
return getConsolidationGlyph();
|
||||||
} else if(walletTx.isPremixSend(payment)) {
|
} else if(walletTx.isPremixSend(payment)) {
|
||||||
return getPremixGlyph();
|
return getPremixGlyph();
|
||||||
|
@ -526,6 +529,13 @@ public class TransactionDiagram extends GridPane {
|
||||||
return whirlpoolFeeGlyph;
|
return whirlpoolFeeGlyph;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Glyph getFakeMixGlyph() {
|
||||||
|
Glyph fakeMixGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.THEATER_MASKS);
|
||||||
|
fakeMixGlyph.getStyleClass().add("fakemix-icon");
|
||||||
|
fakeMixGlyph.setFontSize(12);
|
||||||
|
return fakeMixGlyph;
|
||||||
|
}
|
||||||
|
|
||||||
public static Glyph getTxoGlyph() {
|
public static Glyph getTxoGlyph() {
|
||||||
return getChangeGlyph();
|
return getChangeGlyph();
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,7 @@ public class FontAwesome5 extends GlyphFont {
|
||||||
EXTERNAL_LINK_ALT('\uf35d'),
|
EXTERNAL_LINK_ALT('\uf35d'),
|
||||||
ELLIPSIS_H('\uf141'),
|
ELLIPSIS_H('\uf141'),
|
||||||
EYE('\uf06e'),
|
EYE('\uf06e'),
|
||||||
|
FEATHER_ALT('\uf56b'),
|
||||||
FILE_CSV('\uf6dd'),
|
FILE_CSV('\uf6dd'),
|
||||||
HAND_HOLDING('\uf4bd'),
|
HAND_HOLDING('\uf4bd'),
|
||||||
HAND_HOLDING_MEDICAL('\ue05c'),
|
HAND_HOLDING_MEDICAL('\ue05c'),
|
||||||
|
@ -42,6 +43,7 @@ public class FontAwesome5 extends GlyphFont {
|
||||||
LAPTOP('\uf109'),
|
LAPTOP('\uf109'),
|
||||||
LOCK('\uf023'),
|
LOCK('\uf023'),
|
||||||
LOCK_OPEN('\uf3c1'),
|
LOCK_OPEN('\uf3c1'),
|
||||||
|
MINUS_CIRCLE('\uf056'),
|
||||||
PEN_FANCY('\uf5ac'),
|
PEN_FANCY('\uf5ac'),
|
||||||
PLUS('\uf067'),
|
PLUS('\uf067'),
|
||||||
PLAY_CIRCLE('\uf144'),
|
PLAY_CIRCLE('\uf144'),
|
||||||
|
@ -58,13 +60,15 @@ public class FontAwesome5 extends GlyphFont {
|
||||||
SQUARE('\uf0c8'),
|
SQUARE('\uf0c8'),
|
||||||
SNOWFLAKE('\uf2dc'),
|
SNOWFLAKE('\uf2dc'),
|
||||||
SUN('\uf185'),
|
SUN('\uf185'),
|
||||||
|
THEATER_MASKS('\uf630'),
|
||||||
TIMES_CIRCLE('\uf057'),
|
TIMES_CIRCLE('\uf057'),
|
||||||
TOGGLE_OFF('\uf204'),
|
TOGGLE_OFF('\uf204'),
|
||||||
TOGGLE_ON('\uf205'),
|
TOGGLE_ON('\uf205'),
|
||||||
TOOLS('\uf7d9'),
|
TOOLS('\uf7d9'),
|
||||||
UNDO('\uf0e2'),
|
UNDO('\uf0e2'),
|
||||||
USER_FRIENDS('\uf500'),
|
USER_FRIENDS('\uf500'),
|
||||||
WALLET('\uf555');
|
WALLET('\uf555'),
|
||||||
|
WEIGHT('\uf496');
|
||||||
|
|
||||||
private final char ch;
|
private final char ch;
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ import com.sparrowwallet.sparrow.Mode;
|
||||||
import com.sparrowwallet.sparrow.Theme;
|
import com.sparrowwallet.sparrow.Theme;
|
||||||
import com.sparrowwallet.sparrow.net.*;
|
import com.sparrowwallet.sparrow.net.*;
|
||||||
import com.sparrowwallet.sparrow.wallet.FeeRatesSelection;
|
import com.sparrowwallet.sparrow.wallet.FeeRatesSelection;
|
||||||
|
import com.sparrowwallet.sparrow.wallet.OptimizationStrategy;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
@ -26,6 +27,7 @@ public class Config {
|
||||||
private BitcoinUnit bitcoinUnit;
|
private BitcoinUnit bitcoinUnit;
|
||||||
private FeeRatesSource feeRatesSource;
|
private FeeRatesSource feeRatesSource;
|
||||||
private FeeRatesSelection feeRatesSelection;
|
private FeeRatesSelection feeRatesSelection;
|
||||||
|
private OptimizationStrategy sendOptimizationStrategy;
|
||||||
private Currency fiatCurrency;
|
private Currency fiatCurrency;
|
||||||
private ExchangeSource exchangeSource;
|
private ExchangeSource exchangeSource;
|
||||||
private boolean loadRecentWallets = true;
|
private boolean loadRecentWallets = true;
|
||||||
|
@ -139,6 +141,15 @@ public class Config {
|
||||||
flush();
|
flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public OptimizationStrategy getSendOptimizationStrategy() {
|
||||||
|
return sendOptimizationStrategy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSendOptimizationStrategy(OptimizationStrategy sendOptimizationStrategy) {
|
||||||
|
this.sendOptimizationStrategy = sendOptimizationStrategy;
|
||||||
|
flush();
|
||||||
|
}
|
||||||
|
|
||||||
public Currency getFiatCurrency() {
|
public Currency getFiatCurrency() {
|
||||||
return fiatCurrency;
|
return fiatCurrency;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
package com.sparrowwallet.sparrow.wallet;
|
||||||
|
|
||||||
|
public enum OptimizationStrategy {
|
||||||
|
EFFICIENCY("Efficiency"), PRIVACY("Privacy");
|
||||||
|
|
||||||
|
private final String name;
|
||||||
|
|
||||||
|
private OptimizationStrategy(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ package com.sparrowwallet.sparrow.wallet;
|
||||||
import com.google.common.eventbus.Subscribe;
|
import com.google.common.eventbus.Subscribe;
|
||||||
import com.samourai.whirlpool.client.whirlpool.beans.Pool;
|
import com.samourai.whirlpool.client.whirlpool.beans.Pool;
|
||||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||||
|
import com.sparrowwallet.drongo.KeyPurpose;
|
||||||
import com.sparrowwallet.drongo.SecureString;
|
import com.sparrowwallet.drongo.SecureString;
|
||||||
import com.sparrowwallet.drongo.address.InvalidAddressException;
|
import com.sparrowwallet.drongo.address.InvalidAddressException;
|
||||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||||
|
@ -36,6 +37,7 @@ import javafx.fxml.Initializable;
|
||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
import javafx.scene.control.*;
|
import javafx.scene.control.*;
|
||||||
import javafx.scene.layout.StackPane;
|
import javafx.scene.layout.StackPane;
|
||||||
|
import javafx.scene.layout.VBox;
|
||||||
import javafx.util.Duration;
|
import javafx.util.Duration;
|
||||||
import javafx.util.StringConverter;
|
import javafx.util.StringConverter;
|
||||||
import org.controlsfx.glyphfont.Glyph;
|
import org.controlsfx.glyphfont.Glyph;
|
||||||
|
@ -116,6 +118,18 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
@FXML
|
@FXML
|
||||||
private TransactionDiagram transactionDiagram;
|
private TransactionDiagram transactionDiagram;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private ToggleGroup optimizationToggleGroup;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private ToggleButton efficiencyToggle;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private ToggleButton privacyToggle;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private HelpLabel privacyAnalysis;
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private Button clearButton;
|
private Button clearButton;
|
||||||
|
|
||||||
|
@ -145,7 +159,7 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
|
|
||||||
private final BooleanProperty includeSpentMempoolOutputsProperty = new SimpleBooleanProperty(false);
|
private final BooleanProperty includeSpentMempoolOutputsProperty = new SimpleBooleanProperty(false);
|
||||||
|
|
||||||
private final List<WalletNode> excludedChangeNodes = new ArrayList<>();
|
private final Set<WalletNode> excludedChangeNodes = new HashSet<>();
|
||||||
|
|
||||||
private final ChangeListener<String> feeListener = new ChangeListener<>() {
|
private final ChangeListener<String> feeListener = new ChangeListener<>() {
|
||||||
@Override
|
@Override
|
||||||
|
@ -208,6 +222,8 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
|
|
||||||
private WalletTransactionService walletTransactionService;
|
private WalletTransactionService walletTransactionService;
|
||||||
|
|
||||||
|
private boolean overrideOptimizationStrategy;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void initialize(URL location, ResourceBundle resources) {
|
public void initialize(URL location, ResourceBundle resources) {
|
||||||
EventManager.get().register(this);
|
EventManager.get().register(this);
|
||||||
|
@ -357,7 +373,7 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
|
|
||||||
walletTransactionProperty.addListener((observable, oldValue, walletTransaction) -> {
|
walletTransactionProperty.addListener((observable, oldValue, walletTransaction) -> {
|
||||||
if(walletTransaction != null) {
|
if(walletTransaction != null) {
|
||||||
setPayments(walletTransaction.getPayments());
|
setPayments(walletTransaction.getPayments().stream().filter(payment -> payment.getType() != Payment.Type.FAKE_MIX).collect(Collectors.toList()));
|
||||||
|
|
||||||
double feeRate = walletTransaction.getFeeRate();
|
double feeRate = walletTransaction.getFeeRate();
|
||||||
if(userFeeSet.get()) {
|
if(userFeeSet.get()) {
|
||||||
|
@ -372,6 +388,7 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
}
|
}
|
||||||
|
|
||||||
transactionDiagram.update(walletTransaction);
|
transactionDiagram.update(walletTransaction);
|
||||||
|
updatePrivacyAnalysis(walletTransaction);
|
||||||
createButton.setDisable(walletTransaction == null || isInsufficientFeeRate());
|
createButton.setDisable(walletTransaction == null || isInsufficientFeeRate());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -386,6 +403,21 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
|
|
||||||
addFeeRangeTrackHighlight(0);
|
addFeeRangeTrackHighlight(0);
|
||||||
|
|
||||||
|
efficiencyToggle.setOnAction(event -> {
|
||||||
|
if(getWalletForm().getWallet().isWhirlpoolMixWallet() && !overrideOptimizationStrategy) {
|
||||||
|
AppServices.showWarningDialog("Privacy may be lost!", "It is recommended to optimize for privacy when sending coinjoined outputs.");
|
||||||
|
overrideOptimizationStrategy = true;
|
||||||
|
}
|
||||||
|
Config.get().setSendOptimizationStrategy(OptimizationStrategy.EFFICIENCY);
|
||||||
|
updateTransaction();
|
||||||
|
});
|
||||||
|
privacyToggle.setOnAction(event -> {
|
||||||
|
Config.get().setSendOptimizationStrategy(OptimizationStrategy.PRIVACY);
|
||||||
|
updateTransaction();
|
||||||
|
});
|
||||||
|
setPreferredOptimizationStrategy();
|
||||||
|
updatePrivacyAnalysis(null);
|
||||||
|
|
||||||
createButton.managedProperty().bind(createButton.visibleProperty());
|
createButton.managedProperty().bind(createButton.visibleProperty());
|
||||||
premixButton.managedProperty().bind(premixButton.visibleProperty());
|
premixButton.managedProperty().bind(premixButton.visibleProperty());
|
||||||
createButton.visibleProperty().bind(premixButton.visibleProperty().not());
|
createButton.visibleProperty().bind(premixButton.visibleProperty().not());
|
||||||
|
@ -508,6 +540,7 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
|
|
||||||
try {
|
try {
|
||||||
List<Payment> payments = transactionPayments != null ? transactionPayments : getPayments();
|
List<Payment> payments = transactionPayments != null ? transactionPayments : getPayments();
|
||||||
|
updateOptimizationButtons(payments);
|
||||||
if(!userFeeSet.get() || (getFeeValueSats() != null && getFeeValueSats() > 0)) {
|
if(!userFeeSet.get() || (getFeeValueSats() != null && getFeeValueSats() > 0)) {
|
||||||
Wallet wallet = getWalletForm().getWallet();
|
Wallet wallet = getWalletForm().getWallet();
|
||||||
Long userFee = userFeeSet.get() ? getFeeValueSats() : null;
|
Long userFee = userFeeSet.get() ? getFeeValueSats() : null;
|
||||||
|
@ -517,7 +550,7 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
boolean includeMempoolOutputs = Config.get().isIncludeMempoolOutputs();
|
boolean includeMempoolOutputs = Config.get().isIncludeMempoolOutputs();
|
||||||
boolean includeSpentMempoolOutputs = includeSpentMempoolOutputsProperty.get();
|
boolean includeSpentMempoolOutputs = includeSpentMempoolOutputsProperty.get();
|
||||||
|
|
||||||
walletTransactionService = new WalletTransactionService(wallet, getUtxoSelectors(), getUtxoFilters(), payments, excludedChangeNodes, feeRate, getMinimumFeeRate(), userFee, currentBlockHeight, groupByAddress, includeMempoolOutputs, includeSpentMempoolOutputs);
|
walletTransactionService = new WalletTransactionService(wallet, getUtxoSelectors(payments), getUtxoFilters(), payments, excludedChangeNodes, feeRate, getMinimumFeeRate(), userFee, currentBlockHeight, groupByAddress, includeMempoolOutputs, includeSpentMempoolOutputs);
|
||||||
walletTransactionService.setOnSucceeded(event -> {
|
walletTransactionService.setOnSucceeded(event -> {
|
||||||
if(!walletTransactionService.isIgnoreResult()) {
|
if(!walletTransactionService.isIgnoreResult()) {
|
||||||
walletTransactionProperty.setValue(walletTransactionService.getValue());
|
walletTransactionProperty.setValue(walletTransactionService.getValue());
|
||||||
|
@ -551,7 +584,7 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<UtxoSelector> getUtxoSelectors() throws InvalidAddressException {
|
private List<UtxoSelector> getUtxoSelectors(List<Payment> payments) throws InvalidAddressException {
|
||||||
if(utxoSelectorProperty.get() != null) {
|
if(utxoSelectorProperty.get() != null) {
|
||||||
return List.of(utxoSelectorProperty.get());
|
return List.of(utxoSelectorProperty.get());
|
||||||
}
|
}
|
||||||
|
@ -560,7 +593,16 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
long noInputsFee = wallet.getNoInputsFee(getPayments(), getUserFeeRate());
|
long noInputsFee = wallet.getNoInputsFee(getPayments(), getUserFeeRate());
|
||||||
long costOfChange = wallet.getCostOfChange(getUserFeeRate(), getMinimumFeeRate());
|
long costOfChange = wallet.getCostOfChange(getUserFeeRate(), getMinimumFeeRate());
|
||||||
|
|
||||||
return List.of(new BnBUtxoSelector(noInputsFee, costOfChange), new KnapsackUtxoSelector(noInputsFee));
|
List<UtxoSelector> selectors = new ArrayList<>();
|
||||||
|
OptimizationStrategy optimizationStrategy = (OptimizationStrategy)optimizationToggleGroup.getSelectedToggle().getUserData();
|
||||||
|
if(optimizationStrategy == OptimizationStrategy.PRIVACY
|
||||||
|
&& payments.size() == 1
|
||||||
|
&& (payments.get(0).getAddress().getScriptType() == getWalletForm().getWallet().getAddress(getWalletForm().wallet.getFreshNode(KeyPurpose.RECEIVE)).getScriptType())) {
|
||||||
|
selectors.add(new StonewallUtxoSelector(noInputsFee));
|
||||||
|
}
|
||||||
|
|
||||||
|
selectors.addAll(List.of(new BnBUtxoSelector(noInputsFee, costOfChange), new KnapsackUtxoSelector(noInputsFee)));
|
||||||
|
return selectors;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class WalletTransactionService extends Service<WalletTransaction> {
|
private static class WalletTransactionService extends Service<WalletTransaction> {
|
||||||
|
@ -568,7 +610,7 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
private final List<UtxoSelector> utxoSelectors;
|
private final List<UtxoSelector> utxoSelectors;
|
||||||
private final List<UtxoFilter> utxoFilters;
|
private final List<UtxoFilter> utxoFilters;
|
||||||
private final List<Payment> payments;
|
private final List<Payment> payments;
|
||||||
private final List<WalletNode> excludedChangeNodes;
|
private final Set<WalletNode> excludedChangeNodes;
|
||||||
private final double feeRate;
|
private final double feeRate;
|
||||||
private final double longTermFeeRate;
|
private final double longTermFeeRate;
|
||||||
private final Long fee;
|
private final Long fee;
|
||||||
|
@ -578,7 +620,7 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
private final boolean includeSpentMempoolOutputs;
|
private final boolean includeSpentMempoolOutputs;
|
||||||
private boolean ignoreResult;
|
private boolean ignoreResult;
|
||||||
|
|
||||||
public WalletTransactionService(Wallet wallet, List<UtxoSelector> utxoSelectors, List<UtxoFilter> utxoFilters, List<Payment> payments, List<WalletNode> excludedChangeNodes, double feeRate, double longTermFeeRate, Long fee, Integer currentBlockHeight, boolean groupByAddress, boolean includeMempoolOutputs, boolean includeSpentMempoolOutputs) {
|
public WalletTransactionService(Wallet wallet, List<UtxoSelector> utxoSelectors, List<UtxoFilter> utxoFilters, List<Payment> payments, Set<WalletNode> excludedChangeNodes, double feeRate, double longTermFeeRate, Long fee, Integer currentBlockHeight, boolean groupByAddress, boolean includeMempoolOutputs, boolean includeSpentMempoolOutputs) {
|
||||||
this.wallet = wallet;
|
this.wallet = wallet;
|
||||||
this.utxoSelectors = utxoSelectors;
|
this.utxoSelectors = utxoSelectors;
|
||||||
this.utxoFilters = utxoFilters;
|
this.utxoFilters = utxoFilters;
|
||||||
|
@ -898,6 +940,45 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isFakeMixPossible(List<Payment> payments) {
|
||||||
|
return (utxoSelectorProperty.get() == null
|
||||||
|
&& payments.size() == 1
|
||||||
|
&& (payments.get(0).getAddress().getScriptType() == getWalletForm().getWallet().getAddress(getWalletForm().wallet.getFreshNode(KeyPurpose.RECEIVE)).getScriptType()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateOptimizationButtons(List<Payment> payments) {
|
||||||
|
if(isFakeMixPossible(payments)) {
|
||||||
|
setPreferredOptimizationStrategy();
|
||||||
|
privacyToggle.setDisable(false);
|
||||||
|
} else {
|
||||||
|
optimizationToggleGroup.selectToggle(efficiencyToggle);
|
||||||
|
privacyToggle.setDisable(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private OptimizationStrategy getPreferredOptimizationStrategy() {
|
||||||
|
OptimizationStrategy optimizationStrategy = Config.get().getSendOptimizationStrategy();
|
||||||
|
if(getWalletForm().getWallet().isWhirlpoolMixWallet() && !overrideOptimizationStrategy) {
|
||||||
|
optimizationStrategy = OptimizationStrategy.PRIVACY;
|
||||||
|
}
|
||||||
|
|
||||||
|
return optimizationStrategy;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setPreferredOptimizationStrategy() {
|
||||||
|
optimizationToggleGroup.selectToggle(getPreferredOptimizationStrategy() == OptimizationStrategy.PRIVACY ? privacyToggle : efficiencyToggle);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updatePrivacyAnalysis(WalletTransaction walletTransaction) {
|
||||||
|
if(walletTransaction == null) {
|
||||||
|
privacyAnalysis.setHelpText("Determines whether to optimize the transaction for low fees or greater privacy");
|
||||||
|
privacyAnalysis.setHelpGraphic(null);
|
||||||
|
} else {
|
||||||
|
privacyAnalysis.setHelpText("");
|
||||||
|
privacyAnalysis.setHelpGraphic(new PrivacyAnalysisTooltip(walletTransaction));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void clear(ActionEvent event) {
|
public void clear(ActionEvent event) {
|
||||||
boolean firstTab = true;
|
boolean firstTab = true;
|
||||||
for(Iterator<Tab> iterator = paymentTabs.getTabs().iterator(); iterator.hasNext(); ) {
|
for(Iterator<Tab> iterator = paymentTabs.getTabs().iterator(); iterator.hasNext(); ) {
|
||||||
|
@ -990,9 +1071,9 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
List<String> nodeHashes = inputHashes.computeIfAbsent(node, k -> new ArrayList<>());
|
List<String> nodeHashes = inputHashes.computeIfAbsent(node, k -> new ArrayList<>());
|
||||||
nodeHashes.add(ElectrumServer.getScriptHash(walletForm.getWallet(), node));
|
nodeHashes.add(ElectrumServer.getScriptHash(walletForm.getWallet(), node));
|
||||||
}
|
}
|
||||||
Map<WalletNode, List<String>> changeHash = Collections.emptyMap();
|
Map<WalletNode, List<String>> changeHash = new LinkedHashMap<>();
|
||||||
if(walletTransactionProperty.get().getChangeNode() != null) {
|
for(WalletNode changeNode : walletTransactionProperty.get().getChangeMap().keySet()) {
|
||||||
changeHash = Map.of(walletTransactionProperty.get().getChangeNode(), List.of(ElectrumServer.getScriptHash(walletForm.getWallet(), walletTransactionProperty.get().getChangeNode())));
|
changeHash.put(changeNode, List.of(ElectrumServer.getScriptHash(walletForm.getWallet(), changeNode)));
|
||||||
}
|
}
|
||||||
log.debug("Creating tx " + walletTransactionProperty.get().getTransaction().getTxId() + ", expecting notifications for \ninputs \n" + inputHashes + " and \nchange \n" + changeHash);
|
log.debug("Creating tx " + walletTransactionProperty.get().getTransaction().getTxId() + ", expecting notifications for \ninputs \n" + inputHashes + " and \nchange \n" + changeHash);
|
||||||
}
|
}
|
||||||
|
@ -1006,9 +1087,7 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
private void addWalletTransactionNodes() {
|
private void addWalletTransactionNodes() {
|
||||||
WalletTransaction walletTransaction = walletTransactionProperty.get();
|
WalletTransaction walletTransaction = walletTransactionProperty.get();
|
||||||
Set<WalletNode> nodes = new LinkedHashSet<>(walletTransaction.getSelectedUtxos().values());
|
Set<WalletNode> nodes = new LinkedHashSet<>(walletTransaction.getSelectedUtxos().values());
|
||||||
if(walletTransaction.getChangeNode() != null) {
|
nodes.addAll(walletTransaction.getChangeMap().keySet());
|
||||||
nodes.add(walletTransaction.getChangeNode());
|
|
||||||
}
|
|
||||||
List<WalletNode> consolidationNodes = walletTransaction.getConsolidationSendNodes();
|
List<WalletNode> consolidationNodes = walletTransaction.getConsolidationSendNodes();
|
||||||
nodes.addAll(consolidationNodes);
|
nodes.addAll(consolidationNodes);
|
||||||
|
|
||||||
|
@ -1262,7 +1341,7 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
@Subscribe
|
@Subscribe
|
||||||
public void replaceChangeAddress(ReplaceChangeAddressEvent event) {
|
public void replaceChangeAddress(ReplaceChangeAddressEvent event) {
|
||||||
if(event.getWalletTransaction() == walletTransactionProperty.get()) {
|
if(event.getWalletTransaction() == walletTransactionProperty.get()) {
|
||||||
excludedChangeNodes.add(event.getWalletTransaction().getChangeNode());
|
excludedChangeNodes.addAll(event.getWalletTransaction().getChangeMap().keySet());
|
||||||
updateTransaction();
|
updateTransaction();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1295,4 +1374,85 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
updateTransaction();
|
updateTransaction();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class PrivacyAnalysisTooltip extends VBox {
|
||||||
|
private List<Label> analysisLabels = new ArrayList<>();
|
||||||
|
|
||||||
|
public PrivacyAnalysisTooltip(WalletTransaction walletTransaction) {
|
||||||
|
List<Payment> payments = walletTransaction.getPayments();
|
||||||
|
List<Payment> userPayments = payments.stream().filter(payment -> payment.getType() != Payment.Type.FAKE_MIX).collect(Collectors.toList());
|
||||||
|
OptimizationStrategy optimizationStrategy = getPreferredOptimizationStrategy();
|
||||||
|
boolean fakeMixPresent = payments.stream().anyMatch(payment -> payment.getType() == Payment.Type.FAKE_MIX);
|
||||||
|
boolean roundPaymentAmounts = userPayments.stream().anyMatch(payment -> payment.getAmount() % 100 == 0);
|
||||||
|
boolean mixedAddressTypes = userPayments.stream().anyMatch(payment -> payment.getAddress().getScriptType() != getWalletForm().getWallet().getAddress(getWalletForm().wallet.getFreshNode(KeyPurpose.RECEIVE)).getScriptType());
|
||||||
|
boolean addressReuse = userPayments.stream().anyMatch(payment -> getWalletForm().getWallet().getWalletAddresses().get(payment.getAddress()) != null && !getWalletForm().getWallet().getWalletAddresses().get(payment.getAddress()).getTransactionOutputs().isEmpty());
|
||||||
|
|
||||||
|
if(optimizationStrategy == OptimizationStrategy.PRIVACY) {
|
||||||
|
if(fakeMixPresent) {
|
||||||
|
addLabel("Appears as a two person coinjoin", getPlusGlyph());
|
||||||
|
} else {
|
||||||
|
if(mixedAddressTypes) {
|
||||||
|
addLabel("Cannot fake coinjoin due to mixed address types", getWarningGlyph());
|
||||||
|
} else if(utxoSelectorProperty().get() != null) {
|
||||||
|
addLabel("Cannot fake coinjoin due to coin control", getWarningGlyph());
|
||||||
|
} else if(userPayments.size() > 1) {
|
||||||
|
addLabel("Cannot fake coinjoin due to multiple payments", getWarningGlyph());
|
||||||
|
} else {
|
||||||
|
addLabel("Cannot fake coinjoin due to insufficient funds", getWarningGlyph());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(mixedAddressTypes) {
|
||||||
|
addLabel("Address types different to the wallet indicate external payments", getMinusGlyph());
|
||||||
|
}
|
||||||
|
|
||||||
|
if(roundPaymentAmounts && !fakeMixPresent) {
|
||||||
|
addLabel("Rounded payment amounts indicate external payments", getMinusGlyph());
|
||||||
|
}
|
||||||
|
|
||||||
|
if(addressReuse) {
|
||||||
|
addLabel("Address reuse detected", getMinusGlyph());
|
||||||
|
}
|
||||||
|
|
||||||
|
if(analysisLabels.isEmpty() || (analysisLabels.size() == 1 && analysisLabels.get(0).getText().startsWith("Cannot fake coinjoin"))) {
|
||||||
|
addLabel("Appears as a possible self transfer", getPlusGlyph());
|
||||||
|
}
|
||||||
|
|
||||||
|
analysisLabels.sort(Comparator.comparingInt(o -> (Integer)o.getGraphic().getUserData()));
|
||||||
|
getChildren().addAll(analysisLabels);
|
||||||
|
setSpacing(5);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addLabel(String text, Node graphic) {
|
||||||
|
Label label = new Label(text);
|
||||||
|
label.setStyle("-fx-font-size: 11px");
|
||||||
|
label.setGraphic(graphic);
|
||||||
|
analysisLabels.add(label);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Glyph getPlusGlyph() {
|
||||||
|
Glyph plusGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.PLUS_CIRCLE);
|
||||||
|
plusGlyph.setUserData(0);
|
||||||
|
plusGlyph.setStyle("-fx-text-fill: rgb(80, 161, 79)");
|
||||||
|
plusGlyph.setFontSize(12);
|
||||||
|
return plusGlyph;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Glyph getWarningGlyph() {
|
||||||
|
Glyph warningGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_TRIANGLE);
|
||||||
|
warningGlyph.setUserData(1);
|
||||||
|
warningGlyph.setStyle("-fx-text-fill: rgb(238, 210, 2)");
|
||||||
|
warningGlyph.setFontSize(12);
|
||||||
|
return warningGlyph;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Glyph getMinusGlyph() {
|
||||||
|
Glyph minusGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.MINUS_CIRCLE);
|
||||||
|
minusGlyph.setUserData(2);
|
||||||
|
minusGlyph.setStyle("-fx-text-fill: #e06c75");
|
||||||
|
minusGlyph.setFontSize(12);
|
||||||
|
return minusGlyph;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
-fx-pref-height: 40px;
|
-fx-pref-height: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.send-form .form .fieldset:horizontal .label-container {
|
.send-form .form .fieldset:horizontal .label-container, .buttonRowLabel {
|
||||||
-fx-pref-width: 90px;
|
-fx-pref-width: 90px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,8 @@
|
||||||
<?import com.sparrowwallet.sparrow.control.MempoolSizeFeeRatesChart?>
|
<?import com.sparrowwallet.sparrow.control.MempoolSizeFeeRatesChart?>
|
||||||
<?import org.controlsfx.control.SegmentedButton?>
|
<?import org.controlsfx.control.SegmentedButton?>
|
||||||
<?import com.sparrowwallet.sparrow.wallet.FeeRatesSelection?>
|
<?import com.sparrowwallet.sparrow.wallet.FeeRatesSelection?>
|
||||||
|
<?import com.sparrowwallet.sparrow.wallet.OptimizationStrategy?>
|
||||||
|
<?import com.sparrowwallet.sparrow.control.HelpLabel?>
|
||||||
|
|
||||||
<BorderPane stylesheets="@send.css, @wallet.css, @../script.css, @../general.css" styleClass="wallet-pane" xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.sparrowwallet.sparrow.wallet.SendController">
|
<BorderPane stylesheets="@send.css, @wallet.css, @../script.css, @../general.css" styleClass="wallet-pane" xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.sparrowwallet.sparrow.wallet.SendController">
|
||||||
<center>
|
<center>
|
||||||
|
@ -149,6 +151,33 @@
|
||||||
<padding>
|
<padding>
|
||||||
<Insets left="25.0" right="25.0" bottom="25.0" />
|
<Insets left="25.0" right="25.0" bottom="25.0" />
|
||||||
</padding>
|
</padding>
|
||||||
|
<HBox AnchorPane.leftAnchor="5" alignment="CENTER_LEFT">
|
||||||
|
<Label text="Optimize:" styleClass="buttonRowLabel" />
|
||||||
|
<SegmentedButton>
|
||||||
|
<toggleGroup>
|
||||||
|
<ToggleGroup fx:id="optimizationToggleGroup" />
|
||||||
|
</toggleGroup>
|
||||||
|
<buttons>
|
||||||
|
<ToggleButton fx:id="efficiencyToggle" text="Efficiency" toggleGroup="$optimizationToggleGroup">
|
||||||
|
<tooltip>
|
||||||
|
<Tooltip text="Smallest transaction size for lowest fees"/>
|
||||||
|
</tooltip>
|
||||||
|
<userData>
|
||||||
|
<OptimizationStrategy fx:constant="EFFICIENCY"/>
|
||||||
|
</userData>
|
||||||
|
</ToggleButton>
|
||||||
|
<ToggleButton fx:id="privacyToggle" text="Privacy" toggleGroup="$optimizationToggleGroup">
|
||||||
|
<tooltip>
|
||||||
|
<Tooltip text="Higher entropy transactions that reduce probabilities in blockchain analysis"/>
|
||||||
|
</tooltip>
|
||||||
|
<userData>
|
||||||
|
<OptimizationStrategy fx:constant="PRIVACY"/>
|
||||||
|
</userData>
|
||||||
|
</ToggleButton>
|
||||||
|
</buttons>
|
||||||
|
</SegmentedButton>
|
||||||
|
<HelpLabel fx:id="privacyAnalysis" />
|
||||||
|
</HBox>
|
||||||
<HBox AnchorPane.rightAnchor="10">
|
<HBox AnchorPane.rightAnchor="10">
|
||||||
<Button fx:id="clearButton" text="Clear" cancelButton="true" onAction="#clear" />
|
<Button fx:id="clearButton" text="Clear" cancelButton="true" onAction="#clear" />
|
||||||
<Region HBox.hgrow="ALWAYS" style="-fx-min-width: 20px" />
|
<Region HBox.hgrow="ALWAYS" style="-fx-min-width: 20px" />
|
||||||
|
|
Loading…
Reference in a new issue