add mix to functionality

This commit is contained in:
Craig Raw 2021-09-02 11:39:56 +02:00
parent adb77771aa
commit 2fc551e35b
14 changed files with 454 additions and 36 deletions

2
drongo

@ -1 +1 @@
Subproject commit 67836b2b557839317316a3e1c8d18b98a51d0e29 Subproject commit b4f4cc8726de3e7b5f875816affe1e0f78f2fa25

View file

@ -899,7 +899,7 @@ public class AppController implements Initializable {
if(wallet.isWhirlpoolMasterWallet()) { if(wallet.isWhirlpoolMasterWallet()) {
String walletId = storage.getWalletId(wallet); String walletId = storage.getWalletId(wallet);
Whirlpool whirlpool = AppServices.get().getWhirlpool(walletId); Whirlpool whirlpool = AppServices.get().getWhirlpool(walletId);
whirlpool.setScode(wallet.getOrCreateMixConfig().getScode()); whirlpool.setScode(wallet.getMasterMixConfig().getScode());
whirlpool.setHDWallet(storage.getWalletId(wallet), copy); whirlpool.setHDWallet(storage.getWalletId(wallet), copy);
} }
@ -1186,6 +1186,14 @@ public class AppController implements Initializable {
tab.setGraphic(tabLabel); tab.setGraphic(tabLabel);
tab.setContextMenu(getTabContextMenu(tab)); tab.setContextMenu(getTabContextMenu(tab));
tab.setClosable(true); tab.setClosable(true);
tab.setOnCloseRequest(event -> {
if(AppServices.get().getWhirlpoolForMixToWallet(((WalletTabData)tab.getUserData()).getWalletForm().getWalletId()) != null) {
Optional<ButtonType> optType = AppServices.showWarningDialog("Close mix to wallet?", "This wallet has been configured as the final destination for mixes, and needs to be open for this to occur.\n\nAre you sure you want to close?", ButtonType.NO, ButtonType.YES);
if(optType.isPresent() && optType.get() == ButtonType.NO) {
event.consume();
}
}
});
TabPane subTabs = new TabPane(); TabPane subTabs = new TabPane();
subTabs.setSide(Side.RIGHT); subTabs.setSide(Side.RIGHT);

View file

@ -11,6 +11,7 @@ import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.psbt.PSBT; import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.uri.BitcoinURI; import com.sparrowwallet.drongo.uri.BitcoinURI;
import com.sparrowwallet.drongo.wallet.KeystoreSource; import com.sparrowwallet.drongo.wallet.KeystoreSource;
import com.sparrowwallet.drongo.wallet.MixConfig;
import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.control.TextUtils; import com.sparrowwallet.sparrow.control.TextUtils;
import com.sparrowwallet.sparrow.control.TrayManager; import com.sparrowwallet.sparrow.control.TrayManager;
@ -479,15 +480,27 @@ public class AppServices {
private void startAllWhirlpool() { private void startAllWhirlpool() {
for(Map.Entry<String, Whirlpool> entry : whirlpoolMap.entrySet().stream().filter(entry -> entry.getValue().hasWallet() && !entry.getValue().isStarted()).collect(Collectors.toList())) { for(Map.Entry<String, Whirlpool> entry : whirlpoolMap.entrySet().stream().filter(entry -> entry.getValue().hasWallet() && !entry.getValue().isStarted()).collect(Collectors.toList())) {
Wallet wallet = getWallet(entry.getKey()); Wallet wallet = getWallet(entry.getKey());
if(wallet.getMixConfig() != null && wallet.getMixConfig().getMixOnStartup() != Boolean.FALSE) { Whirlpool whirlpool = entry.getValue();
Whirlpool.StartupService startupService = new Whirlpool.StartupService(entry.getValue()); startWhirlpool(wallet, whirlpool);
}
}
private void startWhirlpool(Wallet wallet, Whirlpool whirlpool) {
if(wallet.getMasterMixConfig().getMixOnStartup() != Boolean.FALSE) {
try {
String mixToWalletId = getWhirlpoolMixToWalletId(wallet.getMasterMixConfig());
whirlpool.setMixToWallet(mixToWalletId, wallet.getMasterMixConfig().getMinMixes());
} catch(NoSuchElementException e) {
showWarningDialog("Mix to wallet not open", wallet.getName() + " is configured to mix to " + wallet.getMasterMixConfig().getMixToWalletName() + ", but this wallet is not open. Mix to wallets are required to be open to avoid address reuse.");
}
Whirlpool.StartupService startupService = new Whirlpool.StartupService(whirlpool);
startupService.setOnFailed(workerStateEvent -> { startupService.setOnFailed(workerStateEvent -> {
log.error("Failed to start whirlpool", workerStateEvent.getSource().getException()); log.error("Failed to start whirlpool", workerStateEvent.getSource().getException());
}); });
startupService.start(); startupService.start();
} }
} }
}
private void shutdownAllWhirlpool() { private void shutdownAllWhirlpool() {
for(Whirlpool whirlpool : whirlpoolMap.values().stream().filter(Whirlpool::isStarted).collect(Collectors.toList())) { for(Whirlpool whirlpool : whirlpoolMap.values().stream().filter(Whirlpool::isStarted).collect(Collectors.toList())) {
@ -499,6 +512,21 @@ public class AppServices {
} }
} }
public String getWhirlpoolMixToWalletId(MixConfig mixConfig) {
if(mixConfig == null || mixConfig.getMixToWalletFile() == null || mixConfig.getMixToWalletName() == null) {
return null;
}
return getOpenWallets().entrySet().stream()
.filter(entry -> entry.getValue().getWalletFile().equals(mixConfig.getMixToWalletFile()) && entry.getKey().getName().equals(mixConfig.getMixToWalletName()))
.map(entry -> entry.getValue().getWalletId(entry.getKey()))
.findFirst().orElseThrow();
}
public Whirlpool getWhirlpoolForMixToWallet(String walletId) {
return whirlpoolMap.values().stream().filter(whirlpool -> walletId.equals(whirlpool.getMixToWalletId())).findFirst().orElse(null);
}
public void minimizeStage(Stage stage) { public void minimizeStage(Stage stage) {
if(trayManager == null) { if(trayManager == null) {
trayManager = new TrayManager(); trayManager = new TrayManager();
@ -977,12 +1005,20 @@ public class AppServices {
public void walletOpened(WalletOpenedEvent event) { public void walletOpened(WalletOpenedEvent event) {
String walletId = event.getStorage().getWalletId(event.getWallet()); String walletId = event.getStorage().getWalletId(event.getWallet());
Whirlpool whirlpool = whirlpoolMap.get(walletId); Whirlpool whirlpool = whirlpoolMap.get(walletId);
if(whirlpool != null && !whirlpool.isStarted() && isConnected() && event.getWallet().getMixConfig() != null && event.getWallet().getMixConfig().getMixOnStartup() != Boolean.FALSE) { if(whirlpool != null && !whirlpool.isStarted() && isConnected()) {
Whirlpool.StartupService startupService = new Whirlpool.StartupService(whirlpool); startWhirlpool(event.getWallet(), whirlpool);
startupService.setOnFailed(workerStateEvent -> { }
log.error("Failed to start whirlpool", workerStateEvent.getSource().getException());
Whirlpool mixFromWhirlpool = whirlpoolMap.entrySet().stream().filter(entry -> event.getStorage().getWalletFile().equals(getWallet(entry.getKey()).getMasterMixConfig().getMixToWalletFile())).map(Map.Entry::getValue).findFirst().orElse(null);
if(mixFromWhirlpool != null) {
mixFromWhirlpool.setMixToWallet(walletId, getWallet(mixFromWhirlpool.getWalletId()).getMasterMixConfig().getMinMixes());
if(mixFromWhirlpool.isStarted()) {
Whirlpool.RestartService restartService = new Whirlpool.RestartService(mixFromWhirlpool);
restartService.setOnFailed(workerStateEvent -> {
log.error("Failed to restart whirlpool", workerStateEvent.getSource().getException());
}); });
startupService.start(); restartService.start();
}
} }
} }
@ -1007,6 +1043,18 @@ public class AppServices {
WhirlpoolEventService.getInstance().unregister(whirlpool); WhirlpoolEventService.getInstance().unregister(whirlpool);
} }
} }
Whirlpool mixToWhirlpool = getWhirlpoolForMixToWallet(walletId);
if(mixToWhirlpool != null && event.getClosedWalletTabData().stream().noneMatch(walletTabData1 -> walletTabData1.getWalletForm().getWalletId().equals(mixToWhirlpool.getWalletId()))) {
mixToWhirlpool.setMixToWallet(null, null);
if(mixToWhirlpool.isStarted()) {
Whirlpool.RestartService restartService = new Whirlpool.RestartService(mixToWhirlpool);
restartService.setOnFailed(workerStateEvent -> {
log.error("Failed to restart whirlpool", workerStateEvent.getSource().getException());
});
restartService.start();
}
}
} }
} }

View file

@ -4,6 +4,6 @@ import com.sparrowwallet.drongo.wallet.Wallet;
public class WalletMixConfigChangedEvent extends WalletChangedEvent { public class WalletMixConfigChangedEvent extends WalletChangedEvent {
public WalletMixConfigChangedEvent(Wallet wallet) { public WalletMixConfigChangedEvent(Wallet wallet) {
super(wallet); super(wallet.isMasterWallet() ? wallet : wallet.getMasterWallet());
} }
} }

View file

@ -0,0 +1,85 @@
package com.sparrowwallet.sparrow.wallet;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.wallet.MixConfig;
import com.sparrowwallet.drongo.wallet.StandardAccount;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.WalletMixConfigChangedEvent;
import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
import javafx.collections.FXCollections;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Spinner;
import javafx.scene.control.SpinnerValueFactory;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.ResourceBundle;
import java.util.stream.Collectors;
public class MixToController implements Initializable {
private static final Wallet NONE_WALLET = new Wallet("None");
@FXML
private ComboBox<Wallet> mixToWallets;
@FXML
private Spinner<Integer> minMixes;
@Override
public void initialize(URL location, ResourceBundle resources) {
}
public void initializeView(Wallet wallet) {
MixConfig mixConfig = wallet.getMasterMixConfig();
List<Wallet> allWallets = new ArrayList<>();
allWallets.add(NONE_WALLET);
List<Wallet> destinationWallets = AppServices.get().getOpenWallets().keySet().stream().filter(openWallet -> openWallet.isValid()
&& openWallet != wallet && openWallet != wallet.getMasterWallet()
&& openWallet.getPolicyType().equals(PolicyType.SINGLE)
&& !StandardAccount.WHIRLPOOL_ACCOUNTS.contains(openWallet.getStandardAccountType())).collect(Collectors.toList());
allWallets.addAll(destinationWallets);
mixToWallets.setItems(FXCollections.observableList(allWallets));
String mixToWalletId = null;
try {
mixToWalletId = AppServices.get().getWhirlpoolMixToWalletId(mixConfig);
} catch(NoSuchElementException e) {
//ignore, mix to wallet is not open
}
if(mixToWalletId != null) {
mixToWallets.setValue(AppServices.get().getWallet(mixToWalletId));
} else {
mixToWallets.setValue(NONE_WALLET);
}
mixToWallets.valueProperty().addListener((observable, oldValue, newValue) -> {
if(newValue == NONE_WALLET) {
mixConfig.setMixToWalletName(null);
mixConfig.setMixToWalletFile(null);
} else {
mixConfig.setMixToWalletName(newValue.getName());
mixConfig.setMixToWalletFile(AppServices.get().getOpenWallets().get(newValue).getWalletFile());
}
EventManager.get().post(new WalletMixConfigChangedEvent(wallet));
});
int initialMinMixes = mixConfig.getMinMixes() == null ? Whirlpool.DEFAULT_MIXTO_MIN_MIXES : mixConfig.getMinMixes();
minMixes.setValueFactory(new SpinnerValueFactory.IntegerSpinnerValueFactory(1, 10000, initialMinMixes));
minMixes.valueProperty().addListener((observable, oldValue, newValue) -> {
mixConfig.setMinMixes(newValue);
EventManager.get().post(new WalletMixConfigChangedEvent(wallet));
});
}
}

View file

@ -0,0 +1,68 @@
package com.sparrowwallet.sparrow.wallet;
import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.WalletMixConfigChangedEvent;
import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.*;
import org.controlsfx.tools.Borders;
import java.io.IOException;
import java.util.NoSuchElementException;
public class MixToDialog extends Dialog<Boolean> {
private final Wallet wallet;
private final Button applyButton;
public MixToDialog(Wallet wallet) {
final DialogPane dialogPane = getDialogPane();
AppServices.setStageIcon(dialogPane.getScene().getWindow());
this.wallet = wallet;
try {
FXMLLoader mixToLoader = new FXMLLoader(AppServices.class.getResource("wallet/mixto.fxml"));
dialogPane.setContent(Borders.wrap(mixToLoader.load()).emptyBorder().buildAll());
MixToController mixToController = mixToLoader.getController();
mixToController.initializeView(wallet);
Whirlpool whirlpool = AppServices.get().getWhirlpool(wallet);
final ButtonType closeButtonType = new javafx.scene.control.ButtonType("Close", ButtonBar.ButtonData.CANCEL_CLOSE);
final ButtonType applyButtonType = new javafx.scene.control.ButtonType(whirlpool.isStarted() ? "Restart Whirlpool" : "Apply", ButtonBar.ButtonData.APPLY);
dialogPane.getButtonTypes().addAll(closeButtonType, applyButtonType);
applyButton = (Button)dialogPane.lookupButton(applyButtonType);
applyButton.setDisable(true);
applyButton.setDefaultButton(true);
try {
AppServices.get().getWhirlpoolMixToWalletId(wallet.getMasterMixConfig());
} catch(NoSuchElementException e) {
applyButton.setDisable(false);
}
dialogPane.setPrefWidth(400);
dialogPane.setPrefHeight(300);
AppServices.moveToActiveWindowScreen(this);
setResultConverter(dialogButton -> dialogButton == applyButtonType);
setOnCloseRequest(event -> {
EventManager.get().unregister(this);
});
EventManager.get().register(this);
}
catch(IOException e) {
throw new RuntimeException(e);
}
}
@Subscribe
public void walletMixConfigChanged(WalletMixConfigChangedEvent event) {
if(event.getWallet() == (wallet.isMasterWallet() ? wallet : wallet.getMasterWallet())) {
applyButton.setDisable(false);
}
}
}

View file

@ -16,6 +16,7 @@ import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.*; import com.sparrowwallet.sparrow.control.*;
import com.sparrowwallet.sparrow.event.*; import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.Config; import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.io.Storage; import com.sparrowwallet.sparrow.io.Storage;
import com.sparrowwallet.sparrow.whirlpool.Whirlpool; import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
@ -32,6 +33,7 @@ import javafx.scene.control.Tooltip;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.stage.FileChooser; import javafx.stage.FileChooser;
import javafx.stage.Stage; import javafx.stage.Stage;
import org.controlsfx.glyphfont.Glyph;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -58,6 +60,9 @@ public class UtxosController extends WalletFormController implements Initializab
@FXML @FXML
private Button stopMix; private Button stopMix;
@FXML
private Button mixTo;
@FXML @FXML
private Button sendSelected; private Button sendSelected;
@ -73,6 +78,11 @@ public class UtxosController extends WalletFormController implements Initializab
stopMix.setDisable(!newValue); stopMix.setDisable(!newValue);
}; };
private final ChangeListener<Boolean> mixingStartingListener = (observable, oldValue, newValue) -> {
startMix.setDisable(newValue);
mixTo.setDisable(newValue);
};
@Override @Override
public void initialize(URL location, ResourceBundle resources) { public void initialize(URL location, ResourceBundle resources) {
EventManager.get().register(this); EventManager.get().register(this);
@ -93,10 +103,15 @@ public class UtxosController extends WalletFormController implements Initializab
stopMix.setDisable(!newValue); stopMix.setDisable(!newValue);
startMix.setDisable(newValue); startMix.setDisable(newValue);
}); });
mixTo.managedProperty().bind(mixTo.visibleProperty());
mixTo.setVisible(getWalletForm().getWallet().getStandardAccountType() == StandardAccount.WHIRLPOOL_POSTMIX);
if(mixButtonsBox.isVisible()) { if(mixButtonsBox.isVisible()) {
Whirlpool whirlpool = AppServices.get().getWhirlpool(getWalletForm().getWallet()); Whirlpool whirlpool = AppServices.get().getWhirlpool(getWalletForm().getWallet());
if(whirlpool != null) { if(whirlpool != null) {
stopMix.visibleProperty().bind(whirlpool.mixingProperty()); stopMix.visibleProperty().bind(whirlpool.mixingProperty());
whirlpool.startingProperty().addListener(new WeakChangeListener<>(mixingStartingListener));
updateMixToButton();
} }
} }
@ -146,6 +161,25 @@ public class UtxosController extends WalletFormController implements Initializab
} }
} }
private void updateMixToButton() {
MixConfig mixConfig = getWalletForm().getWallet().getMasterMixConfig();
if(mixConfig != null && mixConfig.getMixToWalletName() != null) {
mixTo.setText("Mix to " + mixConfig.getMixToWalletName());
try {
AppServices.get().getWhirlpoolMixToWalletId(mixConfig);
mixTo.setGraphic(getExternalGlyph());
mixTo.setTooltip(new Tooltip("Mixing to " + mixConfig.getMixToWalletName() + " after at least " + (mixConfig.getMinMixes() == null ? Whirlpool.DEFAULT_MIXTO_MIN_MIXES : mixConfig.getMinMixes()) + " mixes"));
} catch(NoSuchElementException e) {
mixTo.setGraphic(getErrorGlyph());
mixTo.setTooltip(new Tooltip(mixConfig.getMixToWalletName() + " is not open - open this wallet to mix to it!"));
}
} else {
mixTo.setText("Mix to...");
mixTo.setGraphic(null);
mixTo.setTooltip(null);
}
}
private List<Entry> getSelectedEntries() { private List<Entry> getSelectedEntries() {
return utxosTable.getSelectionModel().getSelectedCells().stream().map(tp -> (UtxoEntry)tp.getTreeItem().getValue()) return utxosTable.getSelectionModel().getSelectedCells().stream().map(tp -> (UtxoEntry)tp.getTreeItem().getValue())
.filter(utxoEntry -> utxoEntry.isSpendable() && !utxoEntry.isMixing()) .filter(utxoEntry -> utxoEntry.isSpendable() && !utxoEntry.isMixing())
@ -215,7 +249,7 @@ public class UtxosController extends WalletFormController implements Initializab
private void prepareWhirlpoolWallet(Wallet decryptedWallet) { private void prepareWhirlpoolWallet(Wallet decryptedWallet) {
Whirlpool whirlpool = AppServices.get().getWhirlpool(getWalletForm().getWalletId()); Whirlpool whirlpool = AppServices.get().getWhirlpool(getWalletForm().getWalletId());
whirlpool.setScode(decryptedWallet.getOrCreateMixConfig().getScode()); whirlpool.setScode(decryptedWallet.getMasterMixConfig().getScode());
whirlpool.setHDWallet(getWalletForm().getWalletId(), decryptedWallet); whirlpool.setHDWallet(getWalletForm().getWalletId(), decryptedWallet);
for(StandardAccount whirlpoolAccount : StandardAccount.WHIRLPOOL_ACCOUNTS) { for(StandardAccount whirlpoolAccount : StandardAccount.WHIRLPOOL_ACCOUNTS) {
@ -287,9 +321,8 @@ public class UtxosController extends WalletFormController implements Initializab
startupService.start(); startupService.start();
} }
Wallet masterWallet = getWalletForm().getWallet().isMasterWallet() ? getWalletForm().getWallet() : getWalletForm().getWallet().getMasterWallet(); getWalletForm().getWallet().getMasterMixConfig().setMixOnStartup(Boolean.TRUE);
masterWallet.getOrCreateMixConfig().setMixOnStartup(Boolean.TRUE); EventManager.get().post(new WalletMixConfigChangedEvent(getWalletForm().getWallet()));
EventManager.get().post(new WalletMixConfigChangedEvent(masterWallet));
} }
public void stopMixing(ActionEvent event) { public void stopMixing(ActionEvent event) {
@ -309,9 +342,36 @@ public class UtxosController extends WalletFormController implements Initializab
whirlpool.shutdown(); whirlpool.shutdown();
} }
Wallet masterWallet = getWalletForm().getWallet().isMasterWallet() ? getWalletForm().getWallet() : getWalletForm().getWallet().getMasterWallet(); getWalletForm().getWallet().getMasterMixConfig().setMixOnStartup(Boolean.FALSE);
masterWallet.getOrCreateMixConfig().setMixOnStartup(Boolean.FALSE); EventManager.get().post(new WalletMixConfigChangedEvent(getWalletForm().getWallet()));
EventManager.get().post(new WalletMixConfigChangedEvent(masterWallet)); }
public void showMixToDialog(ActionEvent event) {
MixToDialog mixToDialog = new MixToDialog(getWalletForm().getWallet());
Optional<Boolean> optApply = mixToDialog.showAndWait();
if(optApply.isPresent() && optApply.get()) {
Whirlpool whirlpool = AppServices.get().getWhirlpool(getWalletForm().getWallet());
MixConfig mixConfig = getWalletForm().getWallet().getMasterMixConfig();
try {
String mixToWalletId = AppServices.get().getWhirlpoolMixToWalletId(mixConfig);
whirlpool.setMixToWallet(mixToWalletId, mixConfig.getMinMixes());
} catch(NoSuchElementException e) {
mixConfig.setMixToWalletName(null);
mixConfig.setMixToWalletFile(null);
EventManager.get().post(new WalletMixConfigChangedEvent(getWalletForm().getWallet()));
whirlpool.setMixToWallet(null, null);
}
updateMixToButton();
if(whirlpool.isStarted()) {
Whirlpool.RestartService restartService = new Whirlpool.RestartService(whirlpool);
restartService.setOnFailed(workerStateEvent -> {
log.error("Failed to restart whirlpool", workerStateEvent.getSource().getException());
});
restartService.start();
}
}
} }
public void exportUtxos(ActionEvent event) { public void exportUtxos(ActionEvent event) {
@ -350,6 +410,19 @@ public class UtxosController extends WalletFormController implements Initializab
String.format(Locale.ENGLISH, "%d", value); String.format(Locale.ENGLISH, "%d", value);
} }
private static Glyph getExternalGlyph() {
Glyph externalGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXTERNAL_LINK_ALT);
externalGlyph.setFontSize(12);
return externalGlyph;
}
private static Glyph getErrorGlyph() {
Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_CIRCLE);
glyph.getStyleClass().add("failure");
glyph.setFontSize(12);
return glyph;
}
@Subscribe @Subscribe
public void walletNodesChanged(WalletNodesChangedEvent event) { public void walletNodesChanged(WalletNodesChangedEvent event) {
if(event.getWallet().equals(walletForm.getWallet())) { if(event.getWallet().equals(walletForm.getWallet())) {
@ -434,6 +507,11 @@ public class UtxosController extends WalletFormController implements Initializab
utxosChart.setVisible(event.isVisible() && !getWalletForm().getWallet().isWhirlpoolMixWallet()); utxosChart.setVisible(event.isVisible() && !getWalletForm().getWallet().isWhirlpoolMixWallet());
} }
@Subscribe
public void openWallets(OpenWalletsEvent event) {
Platform.runLater(this::updateMixToButton);
}
@Subscribe @Subscribe
public void whirlpoolMix(WhirlpoolMixEvent event) { public void whirlpoolMix(WhirlpoolMixEvent event) {
if(event.getWallet().equals(walletForm.getWallet())) { if(event.getWallet().equals(walletForm.getWallet())) {

View file

@ -24,6 +24,7 @@ import com.sparrowwallet.drongo.ExtendedKey;
import com.sparrowwallet.drongo.KeyPurpose; import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.Network; import com.sparrowwallet.drongo.Network;
import com.sparrowwallet.drongo.Utils; import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptType; import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.protocol.Sha256Hash; import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.protocol.Transaction; import com.sparrowwallet.drongo.protocol.Transaction;
@ -57,6 +58,9 @@ import java.util.stream.Collectors;
public class Whirlpool { public class Whirlpool {
private static final Logger log = LoggerFactory.getLogger(Whirlpool.class); private static final Logger log = LoggerFactory.getLogger(Whirlpool.class);
public static final int DEFAULT_MIXTO_MIN_MIXES = 5;
public static final int DEFAULT_MIXTO_RANDOM_FACTOR = 4;
private final HostAndPort torProxy; private final HostAndPort torProxy;
private final WhirlpoolServer whirlpoolServer; private final WhirlpoolServer whirlpoolServer;
private final JavaHttpClientService httpClientService; private final JavaHttpClientService httpClientService;
@ -66,7 +70,9 @@ public class Whirlpool {
private final WhirlpoolWalletConfig config; private final WhirlpoolWalletConfig config;
private HD_Wallet hdWallet; private HD_Wallet hdWallet;
private String walletId; private String walletId;
private String mixToWalletId;
private final BooleanProperty startingProperty = new SimpleBooleanProperty(false);
private final BooleanProperty mixingProperty = new SimpleBooleanProperty(false); private final BooleanProperty mixingProperty = new SimpleBooleanProperty(false);
public Whirlpool(Network network, HostAndPort torProxy) { public Whirlpool(Network network, HostAndPort torProxy) {
@ -342,14 +348,41 @@ public class Whirlpool {
config.setScode(scode); config.setScode(scode);
} }
public void setExternalDestination(ExternalDestination externalDestination) { public String getWalletId() {
if(whirlpoolWalletService.whirlpoolWallet() != null) { return walletId;
throw new IllegalStateException("Cannot set external destination while WhirlpoolWallet is running");
} }
public String getMixToWalletId() {
return mixToWalletId;
}
public void setMixToWallet(String mixToWalletId, Integer minMixes) {
if(mixToWalletId == null) {
config.setExternalDestination(null);
} else {
Wallet mixToWallet = getWallet(mixToWalletId);
if(mixToWallet == null) {
throw new IllegalStateException("Cannot find mix to wallet with id " + mixToWalletId);
}
if(mixToWallet.getPolicyType() != PolicyType.SINGLE) {
throw new IllegalStateException("Only single signature mix to wallets are currently supported");
}
List<ExtendedKey.Header> headers = ExtendedKey.Header.getHeaders(Network.get());
ExtendedKey.Header header = headers.stream().filter(head -> head.getDefaultScriptType().equals(mixToWallet.getScriptType()) && !head.isPrivateKey()).findFirst().orElse(ExtendedKey.Header.xpub);
ExtendedKey extPubKey = mixToWallet.getKeystores().get(0).getExtendedPublicKey();
String xpub = extPubKey.toString(header);
Integer highestUsedIndex = mixToWallet.getNode(KeyPurpose.RECEIVE).getHighestUsedIndex();
int mixes = minMixes == null ? DEFAULT_MIXTO_MIN_MIXES : minMixes;
ExternalDestination externalDestination = new ExternalDestination(xpub, 0, highestUsedIndex == null ? 0 : highestUsedIndex + 1, mixes, DEFAULT_MIXTO_RANDOM_FACTOR);
config.setExternalDestination(externalDestination); config.setExternalDestination(externalDestination);
} }
this.mixToWalletId = mixToWalletId;
}
public boolean isMixing() { public boolean isMixing() {
return mixingProperty.get(); return mixingProperty.get();
} }
@ -358,6 +391,14 @@ public class Whirlpool {
return mixingProperty; return mixingProperty;
} }
public boolean isStarting() {
return startingProperty.get();
}
public BooleanProperty startingProperty() {
return startingProperty;
}
@Subscribe @Subscribe
public void onMixSuccess(MixSuccessEvent e) { public void onMixSuccess(MixSuccessEvent e) {
WalletUtxo walletUtxo = getUtxo(e.getWhirlpoolUtxo()); WalletUtxo walletUtxo = getUtxo(e.getWhirlpoolUtxo());
@ -398,6 +439,7 @@ public class Whirlpool {
@Subscribe @Subscribe
public void onWalletStart(WalletStartEvent e) { public void onWalletStart(WalletStartEvent e) {
if(e.getWhirlpoolWallet() == whirlpoolWalletService.whirlpoolWallet()) { if(e.getWhirlpoolWallet() == whirlpoolWalletService.whirlpoolWallet()) {
log.info("Mixing to " + e.getWhirlpoolWallet().getConfig().getExternalDestination());
mixingProperty.set(true); mixingProperty.set(true);
} }
} }
@ -492,9 +534,11 @@ public class Whirlpool {
updateProgress(-1, 1); updateProgress(-1, 1);
updateMessage("Starting Whirlpool..."); updateMessage("Starting Whirlpool...");
whirlpool.startingProperty.set(true);
WhirlpoolWallet whirlpoolWallet = whirlpool.getWhirlpoolWallet(); WhirlpoolWallet whirlpoolWallet = whirlpool.getWhirlpoolWallet();
if(AppServices.onlineProperty().get()) { if(AppServices.onlineProperty().get()) {
whirlpoolWallet.start(); whirlpoolWallet.start();
whirlpool.startingProperty.set(false);
} }
return whirlpoolWallet; return whirlpoolWallet;
@ -524,6 +568,35 @@ public class Whirlpool {
} }
} }
public static class RestartService extends Service<Boolean> {
private final Whirlpool whirlpool;
public RestartService(Whirlpool whirlpool) {
this.whirlpool = whirlpool;
}
@Override
protected Task<Boolean> createTask() {
return new Task<>() {
protected Boolean call() throws Exception {
updateProgress(-1, 1);
updateMessage("Disconnecting from Whirlpool...");
whirlpool.shutdown();
updateMessage("Starting Whirlpool...");
whirlpool.startingProperty.set(true);
WhirlpoolWallet whirlpoolWallet = whirlpool.getWhirlpoolWallet();
if(AppServices.onlineProperty().get()) {
whirlpoolWallet.start();
whirlpool.startingProperty.set(false);
}
return true;
}
};
}
}
public static class WalletUtxo { public static class WalletUtxo {
public final Wallet wallet; public final Wallet wallet;
public final BlockTransactionHashIndex utxo; public final BlockTransactionHashIndex utxo;

View file

@ -83,7 +83,7 @@ public class WhirlpoolController {
this.walletId = walletId; this.walletId = walletId;
this.wallet = wallet; this.wallet = wallet;
this.utxoEntries = utxoEntries; this.utxoEntries = utxoEntries;
this.mixConfig = wallet.isMasterWallet() ? wallet.getOrCreateMixConfig() : wallet.getMasterWallet().getOrCreateMixConfig(); this.mixConfig = wallet.getMasterMixConfig();
step1.managedProperty().bind(step1.visibleProperty()); step1.managedProperty().bind(step1.visibleProperty());
step2.managedProperty().bind(step2.visibleProperty()); step2.managedProperty().bind(step2.visibleProperty());
@ -97,7 +97,7 @@ public class WhirlpoolController {
scode.setText(mixConfig.getScode() == null ? "" : mixConfig.getScode()); scode.setText(mixConfig.getScode() == null ? "" : mixConfig.getScode());
scode.textProperty().addListener((observable, oldValue, newValue) -> { scode.textProperty().addListener((observable, oldValue, newValue) -> {
mixConfig.setScode(newValue); mixConfig.setScode(newValue);
EventManager.get().post(new WalletMixConfigChangedEvent(wallet.isMasterWallet() ? wallet : wallet.getMasterWallet())); EventManager.get().post(new WalletMixConfigChangedEvent(wallet));
}); });
if(mixConfig.getScode() != null) { if(mixConfig.getScode() != null) {
@ -232,7 +232,7 @@ public class WhirlpoolController {
private void fetchTx0Preview(Pool pool) { private void fetchTx0Preview(Pool pool) {
if(mixConfig.getScode() == null) { if(mixConfig.getScode() == null) {
mixConfig.setScode(""); mixConfig.setScode("");
EventManager.get().post(new WalletMixConfigChangedEvent(wallet.isMasterWallet() ? wallet : wallet.getMasterWallet())); EventManager.get().post(new WalletMixConfigChangedEvent(wallet));
} }
Whirlpool whirlpool = AppServices.get().getWhirlpool(walletId); Whirlpool whirlpool = AppServices.get().getWhirlpool(walletId);

View file

@ -3,7 +3,6 @@ package com.sparrowwallet.sparrow.whirlpool;
import com.samourai.whirlpool.client.tx0.Tx0Preview; import com.samourai.whirlpool.client.tx0.Tx0Preview;
import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.wallet.UtxoEntry; import com.sparrowwallet.sparrow.wallet.UtxoEntry;
import javafx.beans.value.ChangeListener; import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
@ -54,8 +53,7 @@ public class WhirlpoolDialog extends Dialog<Tx0Preview> {
backButton.managedProperty().bind(backButton.visibleProperty()); backButton.managedProperty().bind(backButton.visibleProperty());
previewButton.managedProperty().bind(previewButton.visibleProperty()); previewButton.managedProperty().bind(previewButton.visibleProperty());
Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet(); if(wallet.getMasterMixConfig().getScode() == null) {
if(masterWallet.getOrCreateMixConfig().getScode() == null) {
backButton.setDisable(true); backButton.setDisable(true);
} }
previewButton.visibleProperty().bind(nextButton.visibleProperty().not()); previewButton.visibleProperty().bind(nextButton.visibleProperty().not());

View file

@ -178,7 +178,7 @@ public class SparrowDataSource extends WalletResponseDataSource {
return SparrowMinerFeeSupplier.getInstance(); return SparrowMinerFeeSupplier.getInstance();
} }
private Wallet getWallet(String zpub) { static Wallet getWallet(String zpub) {
return AppServices.get().getOpenWallets().keySet().stream() return AppServices.get().getOpenWallets().keySet().stream()
.filter(Wallet::isValid) .filter(Wallet::isValid)
.filter(wallet -> { .filter(wallet -> {

View file

@ -7,6 +7,7 @@ import com.samourai.whirlpool.client.wallet.beans.ExternalDestination;
import com.samourai.whirlpool.client.wallet.beans.WhirlpoolAccount; import com.samourai.whirlpool.client.wallet.beans.WhirlpoolAccount;
import com.samourai.whirlpool.client.wallet.data.walletState.WalletStateSupplier; import com.samourai.whirlpool.client.wallet.data.walletState.WalletStateSupplier;
import com.sparrowwallet.drongo.KeyPurpose; import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletNode; import com.sparrowwallet.drongo.wallet.WalletNode;
import com.sparrowwallet.sparrow.whirlpool.Whirlpool; import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
@ -17,12 +18,13 @@ import java.util.Map;
public class SparrowWalletStateSupplier implements WalletStateSupplier { public class SparrowWalletStateSupplier implements WalletStateSupplier {
private final String walletId; private final String walletId;
private final Map<String, IIndexHandler> indexHandlerWallets; private final Map<String, IIndexHandler> indexHandlerWallets;
// private int externalIndexDefault; private final ExternalDestination externalDestination;
private IIndexHandler externalIndexHandler;
public SparrowWalletStateSupplier(String walletId, ExternalDestination externalDestination) throws Exception { public SparrowWalletStateSupplier(String walletId, ExternalDestination externalDestination) throws Exception {
this.walletId = walletId; this.walletId = walletId;
this.indexHandlerWallets = new LinkedHashMap<>(); this.indexHandlerWallets = new LinkedHashMap<>();
// this.externalIndexDefault = externalDestination != null ? externalDestination.getStartIndex() : 0; this.externalDestination = externalDestination;
} }
@Override @Override
@ -39,7 +41,22 @@ public class SparrowWalletStateSupplier implements WalletStateSupplier {
@Override @Override
public IIndexHandler getIndexHandlerExternal() { public IIndexHandler getIndexHandlerExternal() {
throw new UnsupportedOperationException(); if(externalDestination == null) {
throw new IllegalStateException("External destination has not been set");
}
if(externalIndexHandler == null) {
Wallet externalWallet = SparrowDataSource.getWallet(externalDestination.getXpub());
if(externalWallet == null) {
throw new IllegalStateException("Cannot find wallet for external destination xpub " + externalDestination.getXpub());
}
KeyPurpose keyPurpose = KeyPurpose.fromChildNumber(new ChildNumber(externalDestination.getChain()));
WalletNode externalNode = externalWallet.getNode(keyPurpose);
externalIndexHandler = new SparrowIndexHandler(externalNode, externalDestination.getStartIndex());
}
return externalIndexHandler;
} }
@Override @Override

View file

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import tornadofx.control.Form?>
<?import tornadofx.control.Fieldset?>
<?import tornadofx.control.Field?>
<?import com.sparrowwallet.sparrow.control.HelpLabel?>
<?import javafx.geometry.Insets?>
<BorderPane stylesheets="@../general.css" styleClass="line-border" xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml" fx:controller="com.sparrowwallet.sparrow.wallet.MixToController">
<center>
<GridPane hgap="10.0" vgap="10.0">
<padding>
<Insets left="25.0" right="25.0" top="25.0" />
</padding>
<columnConstraints>
<ColumnConstraints percentWidth="100" />
</columnConstraints>
<rowConstraints>
<RowConstraints />
</rowConstraints>
<Form GridPane.columnIndex="0" GridPane.rowIndex="0">
<Fieldset inputGrow="SOMETIMES" text="Mix To Settings">
<Field text="Mix to wallet:">
<ComboBox fx:id="mixToWallets" prefWidth="140" promptText="None available" />
<HelpLabel helpText="Select a single signature wallet that is already open to mix to."/>
</Field>
<Field text="Minimum mixes:">
<Spinner fx:id="minMixes" editable="true" prefWidth="90" />
<HelpLabel helpText="The minimum number of mixes required before mixing to the selected wallet.\nTo ensure privacy, each mix beyond this number will have a 25% probability to mix to the selected wallet."/>
</Field>
</Fieldset>
</Form>
</GridPane>
</center>
</BorderPane>

View file

@ -50,6 +50,7 @@
<Glyph fontFamily="Font Awesome 5 Free Solid" icon="STOP_CIRCLE" fontSize="12" /> <Glyph fontFamily="Font Awesome 5 Free Solid" icon="STOP_CIRCLE" fontSize="12" />
</graphic> </graphic>
</Button> </Button>
<Button fx:id="mixTo" text="Mix to..." onAction="#showMixToDialog" />
</HBox> </HBox>
<Region HBox.hgrow="ALWAYS" /> <Region HBox.hgrow="ALWAYS" />
<HBox styleClass="utxos-buttons-box" spacing="20" alignment="BOTTOM_RIGHT"> <HBox styleClass="utxos-buttons-box" spacing="20" alignment="BOTTOM_RIGHT">