From 88ebef97d4cd5ff26598ad6bc2e2dd1a6d58594f Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Fri, 3 Sep 2021 17:16:37 +0200 Subject: [PATCH] support mixing from all single sig wallets, handle tor proxy change, and other minor fixes --- build.gradle | 4 +- drongo | 2 +- .../sparrowwallet/sparrow/AppController.java | 10 +++- .../sparrow/wallet/UtxosController.java | 28 ++++------- .../sparrow/whirlpool/Whirlpool.java | 36 +++++++++---- .../sparrow/whirlpool/WhirlpoolServices.java | 50 ++++++++++++++----- .../dataPersister/SparrowDataPersister.java | 2 +- .../SparrowWalletStateSupplier.java | 8 +-- .../sparrowwallet/sparrow/wallet/utxos.fxml | 2 +- 9 files changed, 90 insertions(+), 52 deletions(-) diff --git a/build.gradle b/build.gradle index 9ea9c7b5..5f702c85 100644 --- a/build.gradle +++ b/build.gradle @@ -91,7 +91,7 @@ dependencies { implementation('org.slf4j:jul-to-slf4j:1.7.30') { exclude group: 'org.slf4j' } - implementation('com.sparrowwallet.nightjar:nightjar:0.2.13-SNAPSHOT') + implementation('com.sparrowwallet.nightjar:nightjar:0.2.16-SNAPSHOT') testImplementation('junit:junit:4.12') } @@ -387,7 +387,7 @@ extraJavaModuleInfo { module('cbor-0.9.jar', 'co.nstant.in.cbor', '0.9') { exports('co.nstant.in.cbor') } - module('nightjar-0.2.13-SNAPSHOT.jar', 'com.sparrowwallet.nightjar', '0.2.13-SNAPSHOT') { + module('nightjar-0.2.16-SNAPSHOT.jar', 'com.sparrowwallet.nightjar', '0.2.16-SNAPSHOT') { requires('com.google.common') requires('net.sourceforge.streamsupport') requires('org.slf4j') diff --git a/drongo b/drongo index 94d22b87..0b40c20a 160000 --- a/drongo +++ b/drongo @@ -1 +1 @@ -Subproject commit 94d22b875868760a95222e0254ec10b59c71e04f +Subproject commit 0b40c20ab252e29ac192bca34d834b8c3eed04a0 diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java index 396143bd..3f0bf3ce 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppController.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppController.java @@ -1738,8 +1738,16 @@ public class AppController implements Initializable { }); Image image = new Image("image/sparrow-small.png", 50, 50, false, false); + String walletName = event.getWallet().getMasterName(); + if(walletName.length() > 25) { + walletName = walletName.substring(0, 25) + "..."; + } + if(!event.getWallet().isMasterWallet()) { + walletName += " " + event.getWallet().getName(); + } + Notifications notificationBuilder = Notifications.create() - .title("Sparrow - " + event.getWallet().getFullName()) + .title("Sparrow - " + walletName) .text(text) .graphic(new ImageView(image)) .hideAfter(Duration.seconds(15)) diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/UtxosController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/UtxosController.java index cf7e42d4..3f1c3cff 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/UtxosController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/UtxosController.java @@ -311,39 +311,29 @@ public class UtxosController extends WalletFormController implements Initializab startMix.setDisable(true); stopMix.setDisable(false); - Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(getWalletForm().getWallet()); - if(whirlpool != null && !whirlpool.isStarted() && AppServices.isConnected()) { - Whirlpool.StartupService startupService = new Whirlpool.StartupService(whirlpool); - startupService.setOnFailed(workerStateEvent -> { - AppServices.showErrorDialog("Failed to start whirlpool", workerStateEvent.getSource().getException().getMessage()); - log.error("Failed to start whirlpool", workerStateEvent.getSource().getException()); - }); - startupService.start(); - } - getWalletForm().getWallet().getMasterMixConfig().setMixOnStartup(Boolean.TRUE); EventManager.get().post(new WalletMasterMixConfigChangedEvent(getWalletForm().getWallet())); + + Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(getWalletForm().getWallet()); + if(whirlpool != null && !whirlpool.isStarted() && AppServices.isConnected()) { + AppServices.getWhirlpoolServices().startWhirlpool(getWalletForm().getWallet(), whirlpool, true); + } } public void stopMixing(ActionEvent event) { stopMix.setDisable(true); startMix.setDisable(false); + getWalletForm().getWallet().getMasterMixConfig().setMixOnStartup(Boolean.FALSE); + EventManager.get().post(new WalletMasterMixConfigChangedEvent(getWalletForm().getWallet())); + Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(getWalletForm().getWallet()); if(whirlpool.isStarted()) { - Whirlpool.ShutdownService shutdownService = new Whirlpool.ShutdownService(whirlpool); - shutdownService.setOnFailed(workerStateEvent -> { - log.error("Failed to stop whirlpool", workerStateEvent.getSource().getException()); - AppServices.showErrorDialog("Failed to stop whirlpool", workerStateEvent.getSource().getException().getMessage()); - }); - shutdownService.start(); + AppServices.getWhirlpoolServices().stopWhirlpool(whirlpool, true); } else { //Ensure http clients are shutdown whirlpool.shutdown(); } - - getWalletForm().getWallet().getMasterMixConfig().setMixOnStartup(Boolean.FALSE); - EventManager.get().post(new WalletMasterMixConfigChangedEvent(getWalletForm().getWallet())); } public void showMixToDialog(ActionEvent event) { diff --git a/src/main/java/com/sparrowwallet/sparrow/whirlpool/Whirlpool.java b/src/main/java/com/sparrowwallet/sparrow/whirlpool/Whirlpool.java index e1e9deb7..aac1cdf4 100644 --- a/src/main/java/com/sparrowwallet/sparrow/whirlpool/Whirlpool.java +++ b/src/main/java/com/sparrowwallet/sparrow/whirlpool/Whirlpool.java @@ -63,7 +63,6 @@ public class Whirlpool { 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; private final JavaStompClientService stompClientService; @@ -78,19 +77,18 @@ public class Whirlpool { private final BooleanProperty mixingProperty = new SimpleBooleanProperty(false); public Whirlpool(Network network, HostAndPort torProxy) { - this.torProxy = torProxy; this.whirlpoolServer = WhirlpoolServer.valueOf(network.getName().toUpperCase()); this.httpClientService = new JavaHttpClientService(torProxy); this.stompClientService = new JavaStompClientService(httpClientService); this.torClientService = new WhirlpoolTorClientService(); this.whirlpoolWalletService = new WhirlpoolWalletService(); - this.config = computeWhirlpoolWalletConfig(); + this.config = computeWhirlpoolWalletConfig(torProxy); WhirlpoolEventService.getInstance().register(this); } - private WhirlpoolWalletConfig computeWhirlpoolWalletConfig() { + private WhirlpoolWalletConfig computeWhirlpoolWalletConfig(HostAndPort torProxy) { DataPersisterFactory dataPersisterFactory = (whirlpoolWallet, bip44w) -> new SparrowDataPersister(whirlpoolWallet); DataSourceFactory dataSourceFactory = (whirlpoolWallet, bip44w, dataPersister) -> new SparrowDataSource(whirlpoolWallet, bip44w, dataPersister); @@ -245,10 +243,6 @@ public class Whirlpool { } } - public HostAndPort getTorProxy() { - return torProxy; - } - public boolean hasWallet() { return hdWallet != null; } @@ -271,9 +265,11 @@ public class Whirlpool { if(wallet != null) { wallet = getStandardAccountWallet(whirlpoolUtxo.getAccount(), wallet); - for(BlockTransactionHashIndex utxo : wallet.getWalletUtxos().keySet()) { - if(utxo.getHashAsString().equals(whirlpoolUtxo.getUtxo().tx_hash) && utxo.getIndex() == whirlpoolUtxo.getUtxo().tx_output_n) { - return new WalletUtxo(wallet, utxo); + if(wallet != null) { + for(BlockTransactionHashIndex utxo : wallet.getWalletUtxos().keySet()) { + if(utxo.getHashAsString().equals(whirlpoolUtxo.getUtxo().tx_hash) && utxo.getIndex() == whirlpoolUtxo.getUtxo().tx_output_n) { + return new WalletUtxo(wallet, utxo); + } } } } @@ -342,6 +338,24 @@ public class Whirlpool { return out; } + public HostAndPort getTorProxy() { + return httpClientService.getTorProxy(); + } + + public void setTorProxy(HostAndPort torProxy) { + if(isStarted()) { + throw new IllegalStateException("Cannot set tor proxy on a started Whirlpool"); + } + + //Ensure all http clients are shutdown first + httpClientService.shutdown(); + + httpClientService.setTorProxy(torProxy); + String serverUrl = whirlpoolServer.getServerUrl(torProxy != null); + ServerApi serverApi = new ServerApi(serverUrl, httpClientService); + config.setServerApi(serverApi); + } + public String getScode() { return config.getScode(); } diff --git a/src/main/java/com/sparrowwallet/sparrow/whirlpool/WhirlpoolServices.java b/src/main/java/com/sparrowwallet/sparrow/whirlpool/WhirlpoolServices.java index 724abda2..d5c372a6 100644 --- a/src/main/java/com/sparrowwallet/sparrow/whirlpool/WhirlpoolServices.java +++ b/src/main/java/com/sparrowwallet/sparrow/whirlpool/WhirlpoolServices.java @@ -18,6 +18,7 @@ import org.slf4j.LoggerFactory; import java.util.HashMap; import java.util.Map; import java.util.NoSuchElementException; +import java.util.Objects; import java.util.stream.Collectors; public class WhirlpoolServices { @@ -39,7 +40,7 @@ public class WhirlpoolServices { public Whirlpool getWhirlpool(String walletId) { Whirlpool whirlpool = whirlpoolMap.get(walletId); if(whirlpool == null) { - HostAndPort torProxy = AppServices.isTorRunning() ? HostAndPort.fromParts("localhost", TorService.PROXY_PORT) : (Config.get().getProxyServer() == null || Config.get().getProxyServer().isEmpty() || !Config.get().isUseProxy() ? null : HostAndPort.fromString(Config.get().getProxyServer())); + HostAndPort torProxy = getTorProxy(); whirlpool = new Whirlpool(Network.get(), torProxy); whirlpoolMap.put(walletId, whirlpool); } @@ -47,21 +48,34 @@ public class WhirlpoolServices { return whirlpool; } + private HostAndPort getTorProxy() { + return AppServices.isTorRunning() ? + HostAndPort.fromParts("localhost", TorService.PROXY_PORT) : + (Config.get().getProxyServer() == null || Config.get().getProxyServer().isEmpty() || !Config.get().isUseProxy() ? null : HostAndPort.fromString(Config.get().getProxyServer())); + } + private void startAllWhirlpool() { for(Map.Entry entry : whirlpoolMap.entrySet().stream().filter(entry -> entry.getValue().hasWallet() && !entry.getValue().isStarted()).collect(Collectors.toList())) { Wallet wallet = AppServices.get().getWallet(entry.getKey()); Whirlpool whirlpool = entry.getValue(); - startWhirlpool(wallet, whirlpool); + startWhirlpool(wallet, whirlpool, false); } } - private void startWhirlpool(Wallet wallet, Whirlpool whirlpool) { + public void startWhirlpool(Wallet wallet, Whirlpool whirlpool, boolean notifyIfMixToMissing) { if(wallet.getMasterMixConfig().getMixOnStartup() != Boolean.FALSE) { + HostAndPort torProxy = getTorProxy(); + if(!Objects.equals(whirlpool.getTorProxy(), torProxy)) { + whirlpool.setTorProxy(getTorProxy()); + } + try { String mixToWalletId = getWhirlpoolMixToWalletId(wallet.getMasterMixConfig()); whirlpool.setMixToWallet(mixToWalletId, wallet.getMasterMixConfig().getMinMixes()); } catch(NoSuchElementException e) { - AppServices.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."); + if(notifyIfMixToMissing) { + AppServices.showWarningDialog("Mix to wallet not open", wallet.getMasterName() + " 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); @@ -72,16 +86,23 @@ public class WhirlpoolServices { } } - private void shutdownAllWhirlpool() { + private void stopAllWhirlpool() { for(Whirlpool whirlpool : whirlpoolMap.values().stream().filter(Whirlpool::isStarted).collect(Collectors.toList())) { - Whirlpool.ShutdownService shutdownService = new Whirlpool.ShutdownService(whirlpool); - shutdownService.setOnFailed(workerStateEvent -> { - log.error("Failed to shutdown whirlpool", workerStateEvent.getSource().getException()); - }); - shutdownService.start(); + stopWhirlpool(whirlpool, false); } } + public void stopWhirlpool(Whirlpool whirlpool, boolean notifyOnFailure) { + Whirlpool.ShutdownService shutdownService = new Whirlpool.ShutdownService(whirlpool); + shutdownService.setOnFailed(workerStateEvent -> { + log.error("Failed to stop whirlpool", workerStateEvent.getSource().getException()); + if(notifyOnFailure) { + AppServices.showErrorDialog("Failed to stop whirlpool", workerStateEvent.getSource().getException().getMessage()); + } + }); + shutdownService.start(); + } + public String getWhirlpoolMixToWalletId(MixConfig mixConfig) { if(mixConfig == null || mixConfig.getMixToWalletFile() == null || mixConfig.getMixToWalletName() == null) { return null; @@ -104,7 +125,7 @@ public class WhirlpoolServices { @Subscribe public void disconnection(DisconnectionEvent event) { - shutdownAllWhirlpool(); + stopAllWhirlpool(); } @Subscribe @@ -112,11 +133,14 @@ public class WhirlpoolServices { String walletId = event.getStorage().getWalletId(event.getWallet()); Whirlpool whirlpool = whirlpoolMap.get(walletId); if(whirlpool != null && !whirlpool.isStarted() && AppServices.isConnected()) { - startWhirlpool(event.getWallet(), whirlpool); + startWhirlpool(event.getWallet(), whirlpool, true); } Whirlpool mixFromWhirlpool = whirlpoolMap.entrySet().stream() - .filter(entry -> event.getStorage().getWalletFile().equals(AppServices.get().getWallet(entry.getKey()).getMasterMixConfig().getMixToWalletFile())) + .filter(entry -> { + MixConfig mixConfig = AppServices.get().getWallet(entry.getKey()).getMasterMixConfig(); + return event.getStorage().getWalletFile().equals(mixConfig.getMixToWalletFile()) && event.getWallet().getName().equals(mixConfig.getMixToWalletName()); + }) .map(Map.Entry::getValue).findFirst().orElse(null); if(mixFromWhirlpool != null) { diff --git a/src/main/java/com/sparrowwallet/sparrow/whirlpool/dataPersister/SparrowDataPersister.java b/src/main/java/com/sparrowwallet/sparrow/whirlpool/dataPersister/SparrowDataPersister.java index bca9abf8..fbe1e4c2 100644 --- a/src/main/java/com/sparrowwallet/sparrow/whirlpool/dataPersister/SparrowDataPersister.java +++ b/src/main/java/com/sparrowwallet/sparrow/whirlpool/dataPersister/SparrowDataPersister.java @@ -15,7 +15,7 @@ public class SparrowDataPersister implements DataPersister { public SparrowDataPersister(WhirlpoolWallet whirlpoolWallet) throws Exception { WhirlpoolWalletConfig config = whirlpoolWallet.getConfig(); String walletIdentifier = whirlpoolWallet.getWalletIdentifier(); - this.walletStateSupplier = new SparrowWalletStateSupplier(walletIdentifier, config.getExternalDestination()); + this.walletStateSupplier = new SparrowWalletStateSupplier(walletIdentifier, config); this.utxoConfigSupplier = new UtxoConfigPersistedSupplier(new SparrowUtxoConfigPersister(walletIdentifier)); } diff --git a/src/main/java/com/sparrowwallet/sparrow/whirlpool/dataSource/SparrowWalletStateSupplier.java b/src/main/java/com/sparrowwallet/sparrow/whirlpool/dataSource/SparrowWalletStateSupplier.java index dfab6617..6bde174c 100644 --- a/src/main/java/com/sparrowwallet/sparrow/whirlpool/dataSource/SparrowWalletStateSupplier.java +++ b/src/main/java/com/sparrowwallet/sparrow/whirlpool/dataSource/SparrowWalletStateSupplier.java @@ -6,6 +6,7 @@ import com.samourai.wallet.hd.Chain; 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.samourai.whirlpool.client.whirlpool.WhirlpoolClientConfig; import com.sparrowwallet.drongo.KeyPurpose; import com.sparrowwallet.drongo.crypto.ChildNumber; import com.sparrowwallet.drongo.wallet.MixConfig; @@ -19,13 +20,13 @@ import java.util.Map; public class SparrowWalletStateSupplier implements WalletStateSupplier { private final String walletId; private final Map indexHandlerWallets; - private final ExternalDestination externalDestination; + private final WhirlpoolClientConfig config; private IIndexHandler externalIndexHandler; - public SparrowWalletStateSupplier(String walletId, ExternalDestination externalDestination) throws Exception { + public SparrowWalletStateSupplier(String walletId, WhirlpoolClientConfig config) throws Exception { this.walletId = walletId; this.indexHandlerWallets = new LinkedHashMap<>(); - this.externalDestination = externalDestination; + this.config = config; } @Override @@ -53,6 +54,7 @@ public class SparrowWalletStateSupplier implements WalletStateSupplier { @Override public IIndexHandler getIndexHandlerExternal() { + ExternalDestination externalDestination = config.getExternalDestination(); if(externalDestination == null) { throw new IllegalStateException("External destination has not been set"); } diff --git a/src/main/resources/com/sparrowwallet/sparrow/wallet/utxos.fxml b/src/main/resources/com/sparrowwallet/sparrow/wallet/utxos.fxml index e88abfd8..af752dae 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/wallet/utxos.fxml +++ b/src/main/resources/com/sparrowwallet/sparrow/wallet/utxos.fxml @@ -50,7 +50,7 @@ -