mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-12-24 12:46:45 +00:00
add mix to functionality
This commit is contained in:
parent
adb77771aa
commit
2fc551e35b
14 changed files with 454 additions and 36 deletions
2
drongo
2
drongo
|
@ -1 +1 @@
|
|||
Subproject commit 67836b2b557839317316a3e1c8d18b98a51d0e29
|
||||
Subproject commit b4f4cc8726de3e7b5f875816affe1e0f78f2fa25
|
|
@ -899,7 +899,7 @@ public class AppController implements Initializable {
|
|||
if(wallet.isWhirlpoolMasterWallet()) {
|
||||
String walletId = storage.getWalletId(wallet);
|
||||
Whirlpool whirlpool = AppServices.get().getWhirlpool(walletId);
|
||||
whirlpool.setScode(wallet.getOrCreateMixConfig().getScode());
|
||||
whirlpool.setScode(wallet.getMasterMixConfig().getScode());
|
||||
whirlpool.setHDWallet(storage.getWalletId(wallet), copy);
|
||||
}
|
||||
|
||||
|
@ -1186,6 +1186,14 @@ public class AppController implements Initializable {
|
|||
tab.setGraphic(tabLabel);
|
||||
tab.setContextMenu(getTabContextMenu(tab));
|
||||
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();
|
||||
subTabs.setSide(Side.RIGHT);
|
||||
|
|
|
@ -11,6 +11,7 @@ import com.sparrowwallet.drongo.protocol.Transaction;
|
|||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
import com.sparrowwallet.drongo.uri.BitcoinURI;
|
||||
import com.sparrowwallet.drongo.wallet.KeystoreSource;
|
||||
import com.sparrowwallet.drongo.wallet.MixConfig;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.control.TextUtils;
|
||||
import com.sparrowwallet.sparrow.control.TrayManager;
|
||||
|
@ -479,13 +480,25 @@ public class AppServices {
|
|||
private void startAllWhirlpool() {
|
||||
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());
|
||||
if(wallet.getMixConfig() != null && wallet.getMixConfig().getMixOnStartup() != Boolean.FALSE) {
|
||||
Whirlpool.StartupService startupService = new Whirlpool.StartupService(entry.getValue());
|
||||
startupService.setOnFailed(workerStateEvent -> {
|
||||
log.error("Failed to start whirlpool", workerStateEvent.getSource().getException());
|
||||
});
|
||||
startupService.start();
|
||||
Whirlpool whirlpool = 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 -> {
|
||||
log.error("Failed to start whirlpool", workerStateEvent.getSource().getException());
|
||||
});
|
||||
startupService.start();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
if(trayManager == null) {
|
||||
trayManager = new TrayManager();
|
||||
|
@ -977,12 +1005,20 @@ public class AppServices {
|
|||
public void walletOpened(WalletOpenedEvent event) {
|
||||
String walletId = event.getStorage().getWalletId(event.getWallet());
|
||||
Whirlpool whirlpool = whirlpoolMap.get(walletId);
|
||||
if(whirlpool != null && !whirlpool.isStarted() && isConnected() && event.getWallet().getMixConfig() != null && event.getWallet().getMixConfig().getMixOnStartup() != Boolean.FALSE) {
|
||||
Whirlpool.StartupService startupService = new Whirlpool.StartupService(whirlpool);
|
||||
startupService.setOnFailed(workerStateEvent -> {
|
||||
log.error("Failed to start whirlpool", workerStateEvent.getSource().getException());
|
||||
});
|
||||
startupService.start();
|
||||
if(whirlpool != null && !whirlpool.isStarted() && isConnected()) {
|
||||
startWhirlpool(event.getWallet(), whirlpool);
|
||||
}
|
||||
|
||||
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());
|
||||
});
|
||||
restartService.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1007,6 +1043,18 @@ public class AppServices {
|
|||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,6 @@ import com.sparrowwallet.drongo.wallet.Wallet;
|
|||
|
||||
public class WalletMixConfigChangedEvent extends WalletChangedEvent {
|
||||
public WalletMixConfigChangedEvent(Wallet wallet) {
|
||||
super(wallet);
|
||||
super(wallet.isMasterWallet() ? wallet : wallet.getMasterWallet());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -16,6 +16,7 @@ import com.sparrowwallet.sparrow.AppServices;
|
|||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.control.*;
|
||||
import com.sparrowwallet.sparrow.event.*;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
|
||||
|
@ -32,6 +33,7 @@ import javafx.scene.control.Tooltip;
|
|||
import javafx.scene.layout.HBox;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.stage.Stage;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
|
@ -58,6 +60,9 @@ public class UtxosController extends WalletFormController implements Initializab
|
|||
@FXML
|
||||
private Button stopMix;
|
||||
|
||||
@FXML
|
||||
private Button mixTo;
|
||||
|
||||
@FXML
|
||||
private Button sendSelected;
|
||||
|
||||
|
@ -73,6 +78,11 @@ public class UtxosController extends WalletFormController implements Initializab
|
|||
stopMix.setDisable(!newValue);
|
||||
};
|
||||
|
||||
private final ChangeListener<Boolean> mixingStartingListener = (observable, oldValue, newValue) -> {
|
||||
startMix.setDisable(newValue);
|
||||
mixTo.setDisable(newValue);
|
||||
};
|
||||
|
||||
@Override
|
||||
public void initialize(URL location, ResourceBundle resources) {
|
||||
EventManager.get().register(this);
|
||||
|
@ -93,10 +103,15 @@ public class UtxosController extends WalletFormController implements Initializab
|
|||
stopMix.setDisable(!newValue);
|
||||
startMix.setDisable(newValue);
|
||||
});
|
||||
mixTo.managedProperty().bind(mixTo.visibleProperty());
|
||||
mixTo.setVisible(getWalletForm().getWallet().getStandardAccountType() == StandardAccount.WHIRLPOOL_POSTMIX);
|
||||
|
||||
if(mixButtonsBox.isVisible()) {
|
||||
Whirlpool whirlpool = AppServices.get().getWhirlpool(getWalletForm().getWallet());
|
||||
if(whirlpool != null) {
|
||||
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() {
|
||||
return utxosTable.getSelectionModel().getSelectedCells().stream().map(tp -> (UtxoEntry)tp.getTreeItem().getValue())
|
||||
.filter(utxoEntry -> utxoEntry.isSpendable() && !utxoEntry.isMixing())
|
||||
|
@ -215,7 +249,7 @@ public class UtxosController extends WalletFormController implements Initializab
|
|||
|
||||
private void prepareWhirlpoolWallet(Wallet decryptedWallet) {
|
||||
Whirlpool whirlpool = AppServices.get().getWhirlpool(getWalletForm().getWalletId());
|
||||
whirlpool.setScode(decryptedWallet.getOrCreateMixConfig().getScode());
|
||||
whirlpool.setScode(decryptedWallet.getMasterMixConfig().getScode());
|
||||
whirlpool.setHDWallet(getWalletForm().getWalletId(), decryptedWallet);
|
||||
|
||||
for(StandardAccount whirlpoolAccount : StandardAccount.WHIRLPOOL_ACCOUNTS) {
|
||||
|
@ -287,9 +321,8 @@ public class UtxosController extends WalletFormController implements Initializab
|
|||
startupService.start();
|
||||
}
|
||||
|
||||
Wallet masterWallet = getWalletForm().getWallet().isMasterWallet() ? getWalletForm().getWallet() : getWalletForm().getWallet().getMasterWallet();
|
||||
masterWallet.getOrCreateMixConfig().setMixOnStartup(Boolean.TRUE);
|
||||
EventManager.get().post(new WalletMixConfigChangedEvent(masterWallet));
|
||||
getWalletForm().getWallet().getMasterMixConfig().setMixOnStartup(Boolean.TRUE);
|
||||
EventManager.get().post(new WalletMixConfigChangedEvent(getWalletForm().getWallet()));
|
||||
}
|
||||
|
||||
public void stopMixing(ActionEvent event) {
|
||||
|
@ -309,9 +342,36 @@ public class UtxosController extends WalletFormController implements Initializab
|
|||
whirlpool.shutdown();
|
||||
}
|
||||
|
||||
Wallet masterWallet = getWalletForm().getWallet().isMasterWallet() ? getWalletForm().getWallet() : getWalletForm().getWallet().getMasterWallet();
|
||||
masterWallet.getOrCreateMixConfig().setMixOnStartup(Boolean.FALSE);
|
||||
EventManager.get().post(new WalletMixConfigChangedEvent(masterWallet));
|
||||
getWalletForm().getWallet().getMasterMixConfig().setMixOnStartup(Boolean.FALSE);
|
||||
EventManager.get().post(new WalletMixConfigChangedEvent(getWalletForm().getWallet()));
|
||||
}
|
||||
|
||||
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) {
|
||||
|
@ -350,6 +410,19 @@ public class UtxosController extends WalletFormController implements Initializab
|
|||
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
|
||||
public void walletNodesChanged(WalletNodesChangedEvent event) {
|
||||
if(event.getWallet().equals(walletForm.getWallet())) {
|
||||
|
@ -434,6 +507,11 @@ public class UtxosController extends WalletFormController implements Initializab
|
|||
utxosChart.setVisible(event.isVisible() && !getWalletForm().getWallet().isWhirlpoolMixWallet());
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void openWallets(OpenWalletsEvent event) {
|
||||
Platform.runLater(this::updateMixToButton);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void whirlpoolMix(WhirlpoolMixEvent event) {
|
||||
if(event.getWallet().equals(walletForm.getWallet())) {
|
||||
|
|
|
@ -24,6 +24,7 @@ import com.sparrowwallet.drongo.ExtendedKey;
|
|||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
|
@ -57,6 +58,9 @@ import java.util.stream.Collectors;
|
|||
public class Whirlpool {
|
||||
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 WhirlpoolServer whirlpoolServer;
|
||||
private final JavaHttpClientService httpClientService;
|
||||
|
@ -66,7 +70,9 @@ public class Whirlpool {
|
|||
private final WhirlpoolWalletConfig config;
|
||||
private HD_Wallet hdWallet;
|
||||
private String walletId;
|
||||
private String mixToWalletId;
|
||||
|
||||
private final BooleanProperty startingProperty = new SimpleBooleanProperty(false);
|
||||
private final BooleanProperty mixingProperty = new SimpleBooleanProperty(false);
|
||||
|
||||
public Whirlpool(Network network, HostAndPort torProxy) {
|
||||
|
@ -342,12 +348,39 @@ public class Whirlpool {
|
|||
config.setScode(scode);
|
||||
}
|
||||
|
||||
public void setExternalDestination(ExternalDestination externalDestination) {
|
||||
if(whirlpoolWalletService.whirlpoolWallet() != null) {
|
||||
throw new IllegalStateException("Cannot set external destination while WhirlpoolWallet is running");
|
||||
public String getWalletId() {
|
||||
return walletId;
|
||||
}
|
||||
|
||||
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() {
|
||||
|
@ -358,6 +391,14 @@ public class Whirlpool {
|
|||
return mixingProperty;
|
||||
}
|
||||
|
||||
public boolean isStarting() {
|
||||
return startingProperty.get();
|
||||
}
|
||||
|
||||
public BooleanProperty startingProperty() {
|
||||
return startingProperty;
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onMixSuccess(MixSuccessEvent e) {
|
||||
WalletUtxo walletUtxo = getUtxo(e.getWhirlpoolUtxo());
|
||||
|
@ -398,6 +439,7 @@ public class Whirlpool {
|
|||
@Subscribe
|
||||
public void onWalletStart(WalletStartEvent e) {
|
||||
if(e.getWhirlpoolWallet() == whirlpoolWalletService.whirlpoolWallet()) {
|
||||
log.info("Mixing to " + e.getWhirlpoolWallet().getConfig().getExternalDestination());
|
||||
mixingProperty.set(true);
|
||||
}
|
||||
}
|
||||
|
@ -492,9 +534,11 @@ public class Whirlpool {
|
|||
updateProgress(-1, 1);
|
||||
updateMessage("Starting Whirlpool...");
|
||||
|
||||
whirlpool.startingProperty.set(true);
|
||||
WhirlpoolWallet whirlpoolWallet = whirlpool.getWhirlpoolWallet();
|
||||
if(AppServices.onlineProperty().get()) {
|
||||
whirlpoolWallet.start();
|
||||
whirlpool.startingProperty.set(false);
|
||||
}
|
||||
|
||||
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 final Wallet wallet;
|
||||
public final BlockTransactionHashIndex utxo;
|
||||
|
|
|
@ -83,7 +83,7 @@ public class WhirlpoolController {
|
|||
this.walletId = walletId;
|
||||
this.wallet = wallet;
|
||||
this.utxoEntries = utxoEntries;
|
||||
this.mixConfig = wallet.isMasterWallet() ? wallet.getOrCreateMixConfig() : wallet.getMasterWallet().getOrCreateMixConfig();
|
||||
this.mixConfig = wallet.getMasterMixConfig();
|
||||
|
||||
step1.managedProperty().bind(step1.visibleProperty());
|
||||
step2.managedProperty().bind(step2.visibleProperty());
|
||||
|
@ -97,7 +97,7 @@ public class WhirlpoolController {
|
|||
scode.setText(mixConfig.getScode() == null ? "" : mixConfig.getScode());
|
||||
scode.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
mixConfig.setScode(newValue);
|
||||
EventManager.get().post(new WalletMixConfigChangedEvent(wallet.isMasterWallet() ? wallet : wallet.getMasterWallet()));
|
||||
EventManager.get().post(new WalletMixConfigChangedEvent(wallet));
|
||||
});
|
||||
|
||||
if(mixConfig.getScode() != null) {
|
||||
|
@ -232,7 +232,7 @@ public class WhirlpoolController {
|
|||
private void fetchTx0Preview(Pool pool) {
|
||||
if(mixConfig.getScode() == null) {
|
||||
mixConfig.setScode("");
|
||||
EventManager.get().post(new WalletMixConfigChangedEvent(wallet.isMasterWallet() ? wallet : wallet.getMasterWallet()));
|
||||
EventManager.get().post(new WalletMixConfigChangedEvent(wallet));
|
||||
}
|
||||
|
||||
Whirlpool whirlpool = AppServices.get().getWhirlpool(walletId);
|
||||
|
|
|
@ -3,7 +3,6 @@ package com.sparrowwallet.sparrow.whirlpool;
|
|||
import com.samourai.whirlpool.client.tx0.Tx0Preview;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.wallet.UtxoEntry;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
|
@ -54,8 +53,7 @@ public class WhirlpoolDialog extends Dialog<Tx0Preview> {
|
|||
backButton.managedProperty().bind(backButton.visibleProperty());
|
||||
previewButton.managedProperty().bind(previewButton.visibleProperty());
|
||||
|
||||
Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet();
|
||||
if(masterWallet.getOrCreateMixConfig().getScode() == null) {
|
||||
if(wallet.getMasterMixConfig().getScode() == null) {
|
||||
backButton.setDisable(true);
|
||||
}
|
||||
previewButton.visibleProperty().bind(nextButton.visibleProperty().not());
|
||||
|
|
|
@ -178,7 +178,7 @@ public class SparrowDataSource extends WalletResponseDataSource {
|
|||
return SparrowMinerFeeSupplier.getInstance();
|
||||
}
|
||||
|
||||
private Wallet getWallet(String zpub) {
|
||||
static Wallet getWallet(String zpub) {
|
||||
return AppServices.get().getOpenWallets().keySet().stream()
|
||||
.filter(Wallet::isValid)
|
||||
.filter(wallet -> {
|
||||
|
|
|
@ -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.data.walletState.WalletStateSupplier;
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletNode;
|
||||
import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
|
||||
|
@ -17,12 +18,13 @@ import java.util.Map;
|
|||
public class SparrowWalletStateSupplier implements WalletStateSupplier {
|
||||
private final String walletId;
|
||||
private final Map<String, IIndexHandler> indexHandlerWallets;
|
||||
// private int externalIndexDefault;
|
||||
private final ExternalDestination externalDestination;
|
||||
private IIndexHandler externalIndexHandler;
|
||||
|
||||
public SparrowWalletStateSupplier(String walletId, ExternalDestination externalDestination) throws Exception {
|
||||
this.walletId = walletId;
|
||||
this.indexHandlerWallets = new LinkedHashMap<>();
|
||||
// this.externalIndexDefault = externalDestination != null ? externalDestination.getStartIndex() : 0;
|
||||
this.externalDestination = externalDestination;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -39,7 +41,22 @@ public class SparrowWalletStateSupplier implements WalletStateSupplier {
|
|||
|
||||
@Override
|
||||
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
|
||||
|
|
|
@ -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>
|
||||
|
|
@ -50,6 +50,7 @@
|
|||
<Glyph fontFamily="Font Awesome 5 Free Solid" icon="STOP_CIRCLE" fontSize="12" />
|
||||
</graphic>
|
||||
</Button>
|
||||
<Button fx:id="mixTo" text="Mix to..." onAction="#showMixToDialog" />
|
||||
</HBox>
|
||||
<Region HBox.hgrow="ALWAYS" />
|
||||
<HBox styleClass="utxos-buttons-box" spacing="20" alignment="BOTTOM_RIGHT">
|
||||
|
|
Loading…
Reference in a new issue