add option to optimize transactions for privacy and display privacy analysis

This commit is contained in:
Craig Raw 2021-08-27 16:00:17 +02:00
parent b22e891b7d
commit 7371ca2994
9 changed files with 281 additions and 32 deletions

2
drongo

@ -1 +1 @@
Subproject commit 81c202198e8b057271414d15259df556a90bc6f1
Subproject commit 7ac4bce14f04163c57b94e34945b5e4a1bf79eb6

View file

@ -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);
}
}

View file

@ -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();
}

View file

@ -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;

View file

@ -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;
}

View file

@ -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;
}
}

View file

@ -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;
}
}
}

View file

@ -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;
}

View file

@ -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" />