mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-12-24 12:46: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 javafx.application.Platform;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.Bounds;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.util.Duration;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
|
||||
|
@ -19,6 +19,7 @@ public class HelpLabel extends Label {
|
|||
super("", getHelpGlyph());
|
||||
tooltip = new Tooltip();
|
||||
tooltip.textProperty().bind(helpTextProperty());
|
||||
tooltip.graphicProperty().bind(helpGraphicProperty());
|
||||
tooltip.setShowDuration(Duration.seconds(15));
|
||||
getStyleClass().add("help-label");
|
||||
|
||||
|
@ -49,4 +50,18 @@ public class HelpLabel extends Label {
|
|||
public final String getHelpText() {
|
||||
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.setStartY(getDiagramHeight() * 0.5 - 20.0);
|
||||
topYaxis.setEndX(width * 0.5);
|
||||
topYaxis.setEndY(0);
|
||||
topYaxis.setEndY(10);
|
||||
topYaxis.getStyleClass().add("inputs-type");
|
||||
|
||||
Line topBracket = new Line();
|
||||
topBracket.setStartX(width * 0.5);
|
||||
topBracket.setStartY(0);
|
||||
topBracket.setStartY(10);
|
||||
topBracket.setEndX(width);
|
||||
topBracket.setEndY(0);
|
||||
topBracket.setEndY(10);
|
||||
topBracket.getStyleClass().add("inputs-type");
|
||||
|
||||
Line bottomYaxis = new Line();
|
||||
bottomYaxis.setStartX(width * 0.5);
|
||||
bottomYaxis.setStartY(getDiagramHeight());
|
||||
bottomYaxis.setStartY(getDiagramHeight() - 10);
|
||||
bottomYaxis.setEndX(width * 0.5);
|
||||
bottomYaxis.setEndY(getDiagramHeight() * 0.5 + 20.0);
|
||||
bottomYaxis.getStyleClass().add("inputs-type");
|
||||
|
||||
Line bottomBracket = new Line();
|
||||
bottomBracket.setStartX(width * 0.5);
|
||||
bottomBracket.setStartY(getDiagramHeight());
|
||||
bottomBracket.setStartY(getDiagramHeight() - 10);
|
||||
bottomBracket.setEndX(width);
|
||||
bottomBracket.setEndY(getDiagramHeight());
|
||||
bottomBracket.setEndY(getDiagramHeight() - 10);
|
||||
bottomBracket.getStyleClass().add("inputs-type");
|
||||
|
||||
group.getChildren().addAll(widthLine, topYaxis, topBracket, bottomYaxis, bottomBracket);
|
||||
|
@ -344,7 +344,7 @@ public class TransactionDiagram extends GridPane {
|
|||
group.getChildren().add(yaxisLine);
|
||||
|
||||
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++) {
|
||||
CubicCurve curve = new CubicCurve();
|
||||
curve.getStyleClass().add("output-line");
|
||||
|
@ -391,15 +391,16 @@ public class TransactionDiagram extends GridPane {
|
|||
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);
|
||||
boolean overGapLimit = (walletTx.getChangeNode().getIndex() - defaultChangeNode.getIndex()) > walletTx.getWallet().getGapLimit();
|
||||
boolean overGapLimit = (changeNode.getIndex() - defaultChangeNode.getIndex()) > walletTx.getWallet().getGapLimit();
|
||||
|
||||
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());
|
||||
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.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
|
||||
changeLabel.setTooltip(changeTooltip);
|
||||
|
@ -469,7 +470,9 @@ public class TransactionDiagram extends GridPane {
|
|||
}
|
||||
|
||||
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();
|
||||
} else if(walletTx.isPremixSend(payment)) {
|
||||
return getPremixGlyph();
|
||||
|
@ -526,6 +529,13 @@ public class TransactionDiagram extends GridPane {
|
|||
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() {
|
||||
return getChangeGlyph();
|
||||
}
|
||||
|
|
|
@ -33,6 +33,7 @@ public class FontAwesome5 extends GlyphFont {
|
|||
EXTERNAL_LINK_ALT('\uf35d'),
|
||||
ELLIPSIS_H('\uf141'),
|
||||
EYE('\uf06e'),
|
||||
FEATHER_ALT('\uf56b'),
|
||||
FILE_CSV('\uf6dd'),
|
||||
HAND_HOLDING('\uf4bd'),
|
||||
HAND_HOLDING_MEDICAL('\ue05c'),
|
||||
|
@ -42,6 +43,7 @@ public class FontAwesome5 extends GlyphFont {
|
|||
LAPTOP('\uf109'),
|
||||
LOCK('\uf023'),
|
||||
LOCK_OPEN('\uf3c1'),
|
||||
MINUS_CIRCLE('\uf056'),
|
||||
PEN_FANCY('\uf5ac'),
|
||||
PLUS('\uf067'),
|
||||
PLAY_CIRCLE('\uf144'),
|
||||
|
@ -58,13 +60,15 @@ public class FontAwesome5 extends GlyphFont {
|
|||
SQUARE('\uf0c8'),
|
||||
SNOWFLAKE('\uf2dc'),
|
||||
SUN('\uf185'),
|
||||
THEATER_MASKS('\uf630'),
|
||||
TIMES_CIRCLE('\uf057'),
|
||||
TOGGLE_OFF('\uf204'),
|
||||
TOGGLE_ON('\uf205'),
|
||||
TOOLS('\uf7d9'),
|
||||
UNDO('\uf0e2'),
|
||||
USER_FRIENDS('\uf500'),
|
||||
WALLET('\uf555');
|
||||
WALLET('\uf555'),
|
||||
WEIGHT('\uf496');
|
||||
|
||||
private final char ch;
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import com.sparrowwallet.sparrow.Mode;
|
|||
import com.sparrowwallet.sparrow.Theme;
|
||||
import com.sparrowwallet.sparrow.net.*;
|
||||
import com.sparrowwallet.sparrow.wallet.FeeRatesSelection;
|
||||
import com.sparrowwallet.sparrow.wallet.OptimizationStrategy;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
|
@ -26,6 +27,7 @@ public class Config {
|
|||
private BitcoinUnit bitcoinUnit;
|
||||
private FeeRatesSource feeRatesSource;
|
||||
private FeeRatesSelection feeRatesSelection;
|
||||
private OptimizationStrategy sendOptimizationStrategy;
|
||||
private Currency fiatCurrency;
|
||||
private ExchangeSource exchangeSource;
|
||||
private boolean loadRecentWallets = true;
|
||||
|
@ -139,6 +141,15 @@ public class Config {
|
|||
flush();
|
||||
}
|
||||
|
||||
public OptimizationStrategy getSendOptimizationStrategy() {
|
||||
return sendOptimizationStrategy;
|
||||
}
|
||||
|
||||
public void setSendOptimizationStrategy(OptimizationStrategy sendOptimizationStrategy) {
|
||||
this.sendOptimizationStrategy = sendOptimizationStrategy;
|
||||
flush();
|
||||
}
|
||||
|
||||
public Currency getFiatCurrency() {
|
||||
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.samourai.whirlpool.client.whirlpool.beans.Pool;
|
||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.SecureString;
|
||||
import com.sparrowwallet.drongo.address.InvalidAddressException;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
|
@ -36,6 +37,7 @@ import javafx.fxml.Initializable;
|
|||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.util.Duration;
|
||||
import javafx.util.StringConverter;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
|
@ -116,6 +118,18 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
@FXML
|
||||
private TransactionDiagram transactionDiagram;
|
||||
|
||||
@FXML
|
||||
private ToggleGroup optimizationToggleGroup;
|
||||
|
||||
@FXML
|
||||
private ToggleButton efficiencyToggle;
|
||||
|
||||
@FXML
|
||||
private ToggleButton privacyToggle;
|
||||
|
||||
@FXML
|
||||
private HelpLabel privacyAnalysis;
|
||||
|
||||
@FXML
|
||||
private Button clearButton;
|
||||
|
||||
|
@ -145,7 +159,7 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
|
||||
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<>() {
|
||||
@Override
|
||||
|
@ -208,6 +222,8 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
|
||||
private WalletTransactionService walletTransactionService;
|
||||
|
||||
private boolean overrideOptimizationStrategy;
|
||||
|
||||
@Override
|
||||
public void initialize(URL location, ResourceBundle resources) {
|
||||
EventManager.get().register(this);
|
||||
|
@ -357,7 +373,7 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
|
||||
walletTransactionProperty.addListener((observable, oldValue, walletTransaction) -> {
|
||||
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();
|
||||
if(userFeeSet.get()) {
|
||||
|
@ -372,6 +388,7 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
}
|
||||
|
||||
transactionDiagram.update(walletTransaction);
|
||||
updatePrivacyAnalysis(walletTransaction);
|
||||
createButton.setDisable(walletTransaction == null || isInsufficientFeeRate());
|
||||
});
|
||||
|
||||
|
@ -386,6 +403,21 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
|
||||
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());
|
||||
premixButton.managedProperty().bind(premixButton.visibleProperty());
|
||||
createButton.visibleProperty().bind(premixButton.visibleProperty().not());
|
||||
|
@ -508,6 +540,7 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
|
||||
try {
|
||||
List<Payment> payments = transactionPayments != null ? transactionPayments : getPayments();
|
||||
updateOptimizationButtons(payments);
|
||||
if(!userFeeSet.get() || (getFeeValueSats() != null && getFeeValueSats() > 0)) {
|
||||
Wallet wallet = getWalletForm().getWallet();
|
||||
Long userFee = userFeeSet.get() ? getFeeValueSats() : null;
|
||||
|
@ -517,7 +550,7 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
boolean includeMempoolOutputs = Config.get().isIncludeMempoolOutputs();
|
||||
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 -> {
|
||||
if(!walletTransactionService.isIgnoreResult()) {
|
||||
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) {
|
||||
return List.of(utxoSelectorProperty.get());
|
||||
}
|
||||
|
@ -560,7 +593,16 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
long noInputsFee = wallet.getNoInputsFee(getPayments(), getUserFeeRate());
|
||||
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> {
|
||||
|
@ -568,7 +610,7 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
private final List<UtxoSelector> utxoSelectors;
|
||||
private final List<UtxoFilter> utxoFilters;
|
||||
private final List<Payment> payments;
|
||||
private final List<WalletNode> excludedChangeNodes;
|
||||
private final Set<WalletNode> excludedChangeNodes;
|
||||
private final double feeRate;
|
||||
private final double longTermFeeRate;
|
||||
private final Long fee;
|
||||
|
@ -578,7 +620,7 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
private final boolean includeSpentMempoolOutputs;
|
||||
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.utxoSelectors = utxoSelectors;
|
||||
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) {
|
||||
boolean firstTab = true;
|
||||
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<>());
|
||||
nodeHashes.add(ElectrumServer.getScriptHash(walletForm.getWallet(), node));
|
||||
}
|
||||
Map<WalletNode, List<String>> changeHash = Collections.emptyMap();
|
||||
if(walletTransactionProperty.get().getChangeNode() != null) {
|
||||
changeHash = Map.of(walletTransactionProperty.get().getChangeNode(), List.of(ElectrumServer.getScriptHash(walletForm.getWallet(), walletTransactionProperty.get().getChangeNode())));
|
||||
Map<WalletNode, List<String>> changeHash = new LinkedHashMap<>();
|
||||
for(WalletNode changeNode : walletTransactionProperty.get().getChangeMap().keySet()) {
|
||||
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);
|
||||
}
|
||||
|
@ -1006,9 +1087,7 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
private void addWalletTransactionNodes() {
|
||||
WalletTransaction walletTransaction = walletTransactionProperty.get();
|
||||
Set<WalletNode> nodes = new LinkedHashSet<>(walletTransaction.getSelectedUtxos().values());
|
||||
if(walletTransaction.getChangeNode() != null) {
|
||||
nodes.add(walletTransaction.getChangeNode());
|
||||
}
|
||||
nodes.addAll(walletTransaction.getChangeMap().keySet());
|
||||
List<WalletNode> consolidationNodes = walletTransaction.getConsolidationSendNodes();
|
||||
nodes.addAll(consolidationNodes);
|
||||
|
||||
|
@ -1262,7 +1341,7 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
@Subscribe
|
||||
public void replaceChangeAddress(ReplaceChangeAddressEvent event) {
|
||||
if(event.getWalletTransaction() == walletTransactionProperty.get()) {
|
||||
excludedChangeNodes.add(event.getWalletTransaction().getChangeNode());
|
||||
excludedChangeNodes.addAll(event.getWalletTransaction().getChangeMap().keySet());
|
||||
updateTransaction();
|
||||
}
|
||||
}
|
||||
|
@ -1295,4 +1374,85 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
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;
|
||||
}
|
||||
|
||||
.send-form .form .fieldset:horizontal .label-container {
|
||||
.send-form .form .fieldset:horizontal .label-container, .buttonRowLabel {
|
||||
-fx-pref-width: 90px;
|
||||
}
|
||||
|
||||
|
|
|
@ -23,6 +23,8 @@
|
|||
<?import com.sparrowwallet.sparrow.control.MempoolSizeFeeRatesChart?>
|
||||
<?import org.controlsfx.control.SegmentedButton?>
|
||||
<?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">
|
||||
<center>
|
||||
|
@ -149,6 +151,33 @@
|
|||
<padding>
|
||||
<Insets left="25.0" right="25.0" bottom="25.0" />
|
||||
</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">
|
||||
<Button fx:id="clearButton" text="Clear" cancelButton="true" onAction="#clear" />
|
||||
<Region HBox.hgrow="ALWAYS" style="-fx-min-width: 20px" />
|
||||
|
|
Loading…
Reference in a new issue