support mixing from all single sig wallets, handle tor proxy change, and other minor fixes

This commit is contained in:
Craig Raw 2021-09-03 17:16:37 +02:00
parent a42761981c
commit 88ebef97d4
9 changed files with 90 additions and 52 deletions

View file

@ -91,7 +91,7 @@ dependencies {
implementation('org.slf4j:jul-to-slf4j:1.7.30') { implementation('org.slf4j:jul-to-slf4j:1.7.30') {
exclude group: 'org.slf4j' 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') testImplementation('junit:junit:4.12')
} }
@ -387,7 +387,7 @@ extraJavaModuleInfo {
module('cbor-0.9.jar', 'co.nstant.in.cbor', '0.9') { module('cbor-0.9.jar', 'co.nstant.in.cbor', '0.9') {
exports('co.nstant.in.cbor') 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('com.google.common')
requires('net.sourceforge.streamsupport') requires('net.sourceforge.streamsupport')
requires('org.slf4j') requires('org.slf4j')

2
drongo

@ -1 +1 @@
Subproject commit 94d22b875868760a95222e0254ec10b59c71e04f Subproject commit 0b40c20ab252e29ac192bca34d834b8c3eed04a0

View file

@ -1738,8 +1738,16 @@ public class AppController implements Initializable {
}); });
Image image = new Image("image/sparrow-small.png", 50, 50, false, false); 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() Notifications notificationBuilder = Notifications.create()
.title("Sparrow - " + event.getWallet().getFullName()) .title("Sparrow - " + walletName)
.text(text) .text(text)
.graphic(new ImageView(image)) .graphic(new ImageView(image))
.hideAfter(Duration.seconds(15)) .hideAfter(Duration.seconds(15))

View file

@ -311,39 +311,29 @@ public class UtxosController extends WalletFormController implements Initializab
startMix.setDisable(true); startMix.setDisable(true);
stopMix.setDisable(false); 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); getWalletForm().getWallet().getMasterMixConfig().setMixOnStartup(Boolean.TRUE);
EventManager.get().post(new WalletMasterMixConfigChangedEvent(getWalletForm().getWallet())); 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) { public void stopMixing(ActionEvent event) {
stopMix.setDisable(true); stopMix.setDisable(true);
startMix.setDisable(false); startMix.setDisable(false);
getWalletForm().getWallet().getMasterMixConfig().setMixOnStartup(Boolean.FALSE);
EventManager.get().post(new WalletMasterMixConfigChangedEvent(getWalletForm().getWallet()));
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(getWalletForm().getWallet()); Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(getWalletForm().getWallet());
if(whirlpool.isStarted()) { if(whirlpool.isStarted()) {
Whirlpool.ShutdownService shutdownService = new Whirlpool.ShutdownService(whirlpool); AppServices.getWhirlpoolServices().stopWhirlpool(whirlpool, true);
shutdownService.setOnFailed(workerStateEvent -> {
log.error("Failed to stop whirlpool", workerStateEvent.getSource().getException());
AppServices.showErrorDialog("Failed to stop whirlpool", workerStateEvent.getSource().getException().getMessage());
});
shutdownService.start();
} else { } else {
//Ensure http clients are shutdown //Ensure http clients are shutdown
whirlpool.shutdown(); whirlpool.shutdown();
} }
getWalletForm().getWallet().getMasterMixConfig().setMixOnStartup(Boolean.FALSE);
EventManager.get().post(new WalletMasterMixConfigChangedEvent(getWalletForm().getWallet()));
} }
public void showMixToDialog(ActionEvent event) { public void showMixToDialog(ActionEvent event) {

View file

@ -63,7 +63,6 @@ public class Whirlpool {
public static final int DEFAULT_MIXTO_MIN_MIXES = 5; public static final int DEFAULT_MIXTO_MIN_MIXES = 5;
public static final int DEFAULT_MIXTO_RANDOM_FACTOR = 4; public static final int DEFAULT_MIXTO_RANDOM_FACTOR = 4;
private final HostAndPort torProxy;
private final WhirlpoolServer whirlpoolServer; private final WhirlpoolServer whirlpoolServer;
private final JavaHttpClientService httpClientService; private final JavaHttpClientService httpClientService;
private final JavaStompClientService stompClientService; private final JavaStompClientService stompClientService;
@ -78,19 +77,18 @@ public class Whirlpool {
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) {
this.torProxy = torProxy;
this.whirlpoolServer = WhirlpoolServer.valueOf(network.getName().toUpperCase()); this.whirlpoolServer = WhirlpoolServer.valueOf(network.getName().toUpperCase());
this.httpClientService = new JavaHttpClientService(torProxy); this.httpClientService = new JavaHttpClientService(torProxy);
this.stompClientService = new JavaStompClientService(httpClientService); this.stompClientService = new JavaStompClientService(httpClientService);
this.torClientService = new WhirlpoolTorClientService(); this.torClientService = new WhirlpoolTorClientService();
this.whirlpoolWalletService = new WhirlpoolWalletService(); this.whirlpoolWalletService = new WhirlpoolWalletService();
this.config = computeWhirlpoolWalletConfig(); this.config = computeWhirlpoolWalletConfig(torProxy);
WhirlpoolEventService.getInstance().register(this); WhirlpoolEventService.getInstance().register(this);
} }
private WhirlpoolWalletConfig computeWhirlpoolWalletConfig() { private WhirlpoolWalletConfig computeWhirlpoolWalletConfig(HostAndPort torProxy) {
DataPersisterFactory dataPersisterFactory = (whirlpoolWallet, bip44w) -> new SparrowDataPersister(whirlpoolWallet); DataPersisterFactory dataPersisterFactory = (whirlpoolWallet, bip44w) -> new SparrowDataPersister(whirlpoolWallet);
DataSourceFactory dataSourceFactory = (whirlpoolWallet, bip44w, dataPersister) -> new SparrowDataSource(whirlpoolWallet, bip44w, dataPersister); 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() { public boolean hasWallet() {
return hdWallet != null; return hdWallet != null;
} }
@ -271,12 +265,14 @@ public class Whirlpool {
if(wallet != null) { if(wallet != null) {
wallet = getStandardAccountWallet(whirlpoolUtxo.getAccount(), wallet); wallet = getStandardAccountWallet(whirlpoolUtxo.getAccount(), wallet);
if(wallet != null) {
for(BlockTransactionHashIndex utxo : wallet.getWalletUtxos().keySet()) { for(BlockTransactionHashIndex utxo : wallet.getWalletUtxos().keySet()) {
if(utxo.getHashAsString().equals(whirlpoolUtxo.getUtxo().tx_hash) && utxo.getIndex() == whirlpoolUtxo.getUtxo().tx_output_n) { if(utxo.getHashAsString().equals(whirlpoolUtxo.getUtxo().tx_hash) && utxo.getIndex() == whirlpoolUtxo.getUtxo().tx_output_n) {
return new WalletUtxo(wallet, utxo); return new WalletUtxo(wallet, utxo);
} }
} }
} }
}
return null; return null;
} }
@ -342,6 +338,24 @@ public class Whirlpool {
return out; 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() { public String getScode() {
return config.getScode(); return config.getScode();
} }

View file

@ -18,6 +18,7 @@ import org.slf4j.LoggerFactory;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.NoSuchElementException; import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public class WhirlpoolServices { public class WhirlpoolServices {
@ -39,7 +40,7 @@ public class WhirlpoolServices {
public Whirlpool getWhirlpool(String walletId) { public Whirlpool getWhirlpool(String walletId) {
Whirlpool whirlpool = whirlpoolMap.get(walletId); Whirlpool whirlpool = whirlpoolMap.get(walletId);
if(whirlpool == null) { 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); whirlpool = new Whirlpool(Network.get(), torProxy);
whirlpoolMap.put(walletId, whirlpool); whirlpoolMap.put(walletId, whirlpool);
} }
@ -47,21 +48,34 @@ public class WhirlpoolServices {
return whirlpool; 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() { 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 = AppServices.get().getWallet(entry.getKey()); Wallet wallet = AppServices.get().getWallet(entry.getKey());
Whirlpool whirlpool = entry.getValue(); 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) { if(wallet.getMasterMixConfig().getMixOnStartup() != Boolean.FALSE) {
HostAndPort torProxy = getTorProxy();
if(!Objects.equals(whirlpool.getTorProxy(), torProxy)) {
whirlpool.setTorProxy(getTorProxy());
}
try { try {
String mixToWalletId = getWhirlpoolMixToWalletId(wallet.getMasterMixConfig()); String mixToWalletId = getWhirlpoolMixToWalletId(wallet.getMasterMixConfig());
whirlpool.setMixToWallet(mixToWalletId, wallet.getMasterMixConfig().getMinMixes()); whirlpool.setMixToWallet(mixToWalletId, wallet.getMasterMixConfig().getMinMixes());
} catch(NoSuchElementException e) { } 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); Whirlpool.StartupService startupService = new Whirlpool.StartupService(whirlpool);
@ -72,15 +86,22 @@ public class WhirlpoolServices {
} }
} }
private void shutdownAllWhirlpool() { private void stopAllWhirlpool() {
for(Whirlpool whirlpool : whirlpoolMap.values().stream().filter(Whirlpool::isStarted).collect(Collectors.toList())) { for(Whirlpool whirlpool : whirlpoolMap.values().stream().filter(Whirlpool::isStarted).collect(Collectors.toList())) {
stopWhirlpool(whirlpool, false);
}
}
public void stopWhirlpool(Whirlpool whirlpool, boolean notifyOnFailure) {
Whirlpool.ShutdownService shutdownService = new Whirlpool.ShutdownService(whirlpool); Whirlpool.ShutdownService shutdownService = new Whirlpool.ShutdownService(whirlpool);
shutdownService.setOnFailed(workerStateEvent -> { shutdownService.setOnFailed(workerStateEvent -> {
log.error("Failed to shutdown whirlpool", workerStateEvent.getSource().getException()); log.error("Failed to stop whirlpool", workerStateEvent.getSource().getException());
if(notifyOnFailure) {
AppServices.showErrorDialog("Failed to stop whirlpool", workerStateEvent.getSource().getException().getMessage());
}
}); });
shutdownService.start(); shutdownService.start();
} }
}
public String getWhirlpoolMixToWalletId(MixConfig mixConfig) { public String getWhirlpoolMixToWalletId(MixConfig mixConfig) {
if(mixConfig == null || mixConfig.getMixToWalletFile() == null || mixConfig.getMixToWalletName() == null) { if(mixConfig == null || mixConfig.getMixToWalletFile() == null || mixConfig.getMixToWalletName() == null) {
@ -104,7 +125,7 @@ public class WhirlpoolServices {
@Subscribe @Subscribe
public void disconnection(DisconnectionEvent event) { public void disconnection(DisconnectionEvent event) {
shutdownAllWhirlpool(); stopAllWhirlpool();
} }
@Subscribe @Subscribe
@ -112,11 +133,14 @@ public class WhirlpoolServices {
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() && AppServices.isConnected()) { if(whirlpool != null && !whirlpool.isStarted() && AppServices.isConnected()) {
startWhirlpool(event.getWallet(), whirlpool); startWhirlpool(event.getWallet(), whirlpool, true);
} }
Whirlpool mixFromWhirlpool = whirlpoolMap.entrySet().stream() 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); .map(Map.Entry::getValue).findFirst().orElse(null);
if(mixFromWhirlpool != null) { if(mixFromWhirlpool != null) {

View file

@ -15,7 +15,7 @@ public class SparrowDataPersister implements DataPersister {
public SparrowDataPersister(WhirlpoolWallet whirlpoolWallet) throws Exception { public SparrowDataPersister(WhirlpoolWallet whirlpoolWallet) throws Exception {
WhirlpoolWalletConfig config = whirlpoolWallet.getConfig(); WhirlpoolWalletConfig config = whirlpoolWallet.getConfig();
String walletIdentifier = whirlpoolWallet.getWalletIdentifier(); String walletIdentifier = whirlpoolWallet.getWalletIdentifier();
this.walletStateSupplier = new SparrowWalletStateSupplier(walletIdentifier, config.getExternalDestination()); this.walletStateSupplier = new SparrowWalletStateSupplier(walletIdentifier, config);
this.utxoConfigSupplier = new UtxoConfigPersistedSupplier(new SparrowUtxoConfigPersister(walletIdentifier)); this.utxoConfigSupplier = new UtxoConfigPersistedSupplier(new SparrowUtxoConfigPersister(walletIdentifier));
} }

View file

@ -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.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.samourai.whirlpool.client.whirlpool.WhirlpoolClientConfig;
import com.sparrowwallet.drongo.KeyPurpose; import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.crypto.ChildNumber; import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.wallet.MixConfig; import com.sparrowwallet.drongo.wallet.MixConfig;
@ -19,13 +20,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 final ExternalDestination externalDestination; private final WhirlpoolClientConfig config;
private IIndexHandler externalIndexHandler; private IIndexHandler externalIndexHandler;
public SparrowWalletStateSupplier(String walletId, ExternalDestination externalDestination) throws Exception { public SparrowWalletStateSupplier(String walletId, WhirlpoolClientConfig config) throws Exception {
this.walletId = walletId; this.walletId = walletId;
this.indexHandlerWallets = new LinkedHashMap<>(); this.indexHandlerWallets = new LinkedHashMap<>();
this.externalDestination = externalDestination; this.config = config;
} }
@Override @Override
@ -53,6 +54,7 @@ public class SparrowWalletStateSupplier implements WalletStateSupplier {
@Override @Override
public IIndexHandler getIndexHandlerExternal() { public IIndexHandler getIndexHandlerExternal() {
ExternalDestination externalDestination = config.getExternalDestination();
if(externalDestination == null) { if(externalDestination == null) {
throw new IllegalStateException("External destination has not been set"); throw new IllegalStateException("External destination has not been set");
} }

View file

@ -50,7 +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" /> <Button fx:id="mixTo" text="Mix to..." maxWidth="200" 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">