Browse Source

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

terminal
Craig Raw 3 years ago
parent
commit
88ebef97d4
  1. 4
      build.gradle
  2. 2
      drongo
  3. 10
      src/main/java/com/sparrowwallet/sparrow/AppController.java
  4. 26
      src/main/java/com/sparrowwallet/sparrow/wallet/UtxosController.java
  5. 36
      src/main/java/com/sparrowwallet/sparrow/whirlpool/Whirlpool.java
  6. 50
      src/main/java/com/sparrowwallet/sparrow/whirlpool/WhirlpoolServices.java
  7. 2
      src/main/java/com/sparrowwallet/sparrow/whirlpool/dataPersister/SparrowDataPersister.java
  8. 8
      src/main/java/com/sparrowwallet/sparrow/whirlpool/dataSource/SparrowWalletStateSupplier.java
  9. 2
      src/main/resources/com/sparrowwallet/sparrow/wallet/utxos.fxml

4
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')

2
drongo

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

10
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))

26
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);
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()) {
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();
AppServices.getWhirlpoolServices().startWhirlpool(getWalletForm().getWallet(), whirlpool, true);
}
getWalletForm().getWallet().getMasterMixConfig().setMixOnStartup(Boolean.TRUE);
EventManager.get().post(new WalletMasterMixConfigChangedEvent(getWalletForm().getWallet()));
}
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) {

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

50
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<String, Whirlpool> 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) {

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

8
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<String, IIndexHandler> 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");
}

2
src/main/resources/com/sparrowwallet/sparrow/wallet/utxos.fxml

@ -50,7 +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" />
<Button fx:id="mixTo" text="Mix to..." maxWidth="200" onAction="#showMixToDialog" />
</HBox>
<Region HBox.hgrow="ALWAYS" />
<HBox styleClass="utxos-buttons-box" spacing="20" alignment="BOTTOM_RIGHT">

Loading…
Cancel
Save