diff --git a/build.gradle b/build.gradle index e697bb8d..c1fcae99 100644 --- a/build.gradle +++ b/build.gradle @@ -123,8 +123,9 @@ dependencies { implementation('org.slf4j:jul-to-slf4j:2.0.12') { exclude group: 'org.slf4j' } - implementation('com.sparrowwallet.nightjar:nightjar:0.2.40') implementation('com.sparrowwallet.bokmakierie:bokmakierie:1.0') + implementation('io.samourai.code.whirlpool:whirlpool-client:1.0.6') + implementation('io.samourai.code.wallet:java-http-client:2.0.2') implementation('io.reactivex.rxjava2:rxjava:2.2.15') implementation('io.reactivex.rxjava2:rxjavafx:2.2.2') implementation('org.apache.commons:commons-lang3:3.7') @@ -212,8 +213,6 @@ jlink { uses 'org.flywaydb.core.extensibility.FlywayExtension' uses 'org.flywaydb.core.internal.database.DatabaseType' uses 'org.eclipse.jetty.http.HttpFieldPreEncoder' - uses 'org.eclipse.jetty.websocket.api.extensions.Extension' - uses 'org.eclipse.jetty.websocket.common.RemoteEndpointFactory' } options = ['--strip-native-commands', '--strip-java-debug-attributes', '--compress', '2', '--no-header-files', '--no-man-pages', '--ignore-signing-information', '--exclude-files', '**.png', '--exclude-resources', 'glob:/com.sparrowwallet.merged.module/META-INF/*'] @@ -493,65 +492,21 @@ extraJavaModuleInfo { exports('co.nstant.in.cbor.model') exports('co.nstant.in.cbor.builder') } - module('nightjar-0.2.40.jar', 'com.sparrowwallet.nightjar', '0.2.40') { - requires('com.google.common') - requires('net.sourceforge.streamsupport') - requires('org.slf4j') - requires('org.bouncycastle.provider') - requires('com.fasterxml.jackson.databind') - requires('com.fasterxml.jackson.annotation') - requires('com.fasterxml.jackson.core') - requires('ch.qos.logback.classic') - requires('org.json') - requires('io.reactivex.rxjava2') - exports('com.samourai.http.client') - exports('com.samourai.tor.client') - exports('com.samourai.wallet.api.backend') - exports('com.samourai.wallet.api.backend.beans') - exports('com.samourai.wallet.client.indexHandler') - exports('com.samourai.wallet.hd') - exports('com.samourai.wallet.util') - exports('com.samourai.wallet.bip47.rpc') - exports('com.samourai.wallet.bip47.rpc.java') - exports('com.samourai.wallet.cahoots') - exports('com.samourai.wallet.cahoots.psbt') - exports('com.samourai.wallet.cahoots.stonewallx2') - exports('com.samourai.soroban.cahoots') - exports('com.samourai.soroban.client') - exports('com.samourai.soroban.client.cahoots') - exports('com.samourai.soroban.client.meeting') - exports('com.samourai.soroban.client.rpc') - exports('com.samourai.wallet.send') - exports('com.samourai.whirlpool.client.event') - exports('com.samourai.whirlpool.client.wallet') - exports('com.samourai.whirlpool.client.wallet.beans') - exports('com.samourai.whirlpool.client.wallet.data.dataSource') - exports('com.samourai.whirlpool.client.wallet.data.dataPersister') - exports('com.samourai.whirlpool.client.whirlpool') - exports('com.samourai.whirlpool.client.whirlpool.beans') - exports('com.samourai.whirlpool.client.wallet.data.pool') - exports('com.samourai.whirlpool.client.wallet.data.utxo') - exports('com.samourai.whirlpool.client.wallet.data.utxoConfig') - exports('com.samourai.whirlpool.client.wallet.data.supplier') - exports('com.samourai.whirlpool.client.mix.handler') - exports('com.samourai.whirlpool.client.mix.listener') - exports('com.samourai.whirlpool.protocol.beans') - exports('com.samourai.whirlpool.protocol.rest') - exports('com.samourai.whirlpool.client.tx0') - exports('com.samourai.wallet.segwit.bech32') - exports('com.samourai.whirlpool.client.wallet.data.chain') - exports('com.samourai.whirlpool.client.wallet.data.wallet') - exports('com.samourai.whirlpool.client.wallet.data.minerFee') - exports('com.samourai.whirlpool.client.wallet.data.walletState') - exports('com.sparrowwallet.nightjar.http') - exports('com.sparrowwallet.nightjar.stomp') - exports('com.sparrowwallet.nightjar.tor') + module('commons-codec-1.10.jar', 'commons.codec', '1.10') { + exports('org.apache.commons.codec') } - module('throwing-supplier-1.0.3.jar', 'zeroleak.throwingsupplier', '1.0.3') { - exports('com.zeroleak.throwingsupplier') + module('logback-core-1.2.13.jar', 'ch.qos.logback.core', '1.2.13') { + exports('ch.qos.logback.core') } - module('okhttp-2.7.5.jar', 'com.squareup.okhttp', '2.7.5') { - exports('com.squareup.okhttp') + module('jackson-datatype-jsr310-2.13.2.jar', 'jackson-datatype-jsr310', '2.13.2') { + exports('com.fasterxml.jackson.datatype.jsr310') + } + module('json-20240205.jar', 'org.json', '20240205') { + exports('org.json') + } + module('scrypt-1.4.0.jar', 'scrypt', '1.4.0') { + exports('com.lambdaworks.codec') + exports('com.lambdaworks.crypto') } module('okio-1.6.0.jar', 'com.squareup.okio', '1.6.0') { exports('okio') @@ -559,22 +514,12 @@ extraJavaModuleInfo { module('java-jwt-3.8.1.jar', 'com.auth0.jwt', '3.8.1') { exports('com.auth0.jwt') } - module('json-20180130.jar', 'org.json', '1.0') { - exports('org.json') - } - module('scrypt-1.4.0.jar', 'com.lambdaworks.scrypt', '1.4.0') { - exports('com.lambdaworks.codec') - exports('com.lambdaworks.crypto') - } module('streamsupport-1.7.0.jar', 'net.sourceforge.streamsupport', '1.7.0') { requires('jdk.unsupported') exports('java8.util') exports('java8.util.function') exports('java8.util.stream') } - module('protobuf-java-2.6.1.jar', 'com.google.protobuf', '2.6.1') { - exports('com.google.protobuf') - } module('commons-text-1.2.jar', 'org.apache.commons.text', '1.2') { exports('org.apache.commons.text') } diff --git a/src/main/java/com/sparrowwallet/sparrow/AppServices.java b/src/main/java/com/sparrowwallet/sparrow/AppServices.java index ac0ec31c..66c8e22a 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppServices.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppServices.java @@ -167,6 +167,11 @@ public class AppServices { connectionService.cancel(); ratesService.cancel(); versionCheckService.cancel(); + + if(httpClientService != null) { + HttpClientService.ShutdownService shutdownService = new HttpClientService.ShutdownService(httpClientService); + shutdownService.start(); + } } } }; @@ -542,11 +547,10 @@ public class AppServices { } public static HttpClientService getHttpClientService() { + HostAndPort torProxy = getTorProxy(); if(httpClientService == null) { - HostAndPort torProxy = getTorProxy(); httpClientService = new HttpClientService(torProxy); } else { - HostAndPort torProxy = getTorProxy(); if(!Objects.equals(httpClientService.getTorProxy(), torProxy)) { httpClientService.setTorProxy(getTorProxy()); } diff --git a/src/main/java/com/sparrowwallet/sparrow/control/MixStatusCell.java b/src/main/java/com/sparrowwallet/sparrow/control/MixStatusCell.java index 79271a2e..5beed7f6 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/MixStatusCell.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/MixStatusCell.java @@ -70,7 +70,7 @@ public class MixStatusCell extends TreeTableCell { } private void setMixFail(MixFailReason mixFailReason, String mixError, Long mixErrorTimestamp) { - if(mixFailReason != MixFailReason.CANCEL) { + if(mixFailReason.isError()) { long elapsed = mixErrorTimestamp == null ? 0L : System.currentTimeMillis() - mixErrorTimestamp; if(elapsed >= ERROR_DISPLAY_MILLIS) { //Old error, don't set again. @@ -116,22 +116,10 @@ public class MixStatusCell extends TreeTableCell { progressIndicator.setProgress(mixProgress.getMixStep().getProgressPercent() == 100 ? -1 : mixProgress.getMixStep().getProgressPercent() / 100.0); setGraphic(progressIndicator); Tooltip tt = new Tooltip(); - String status = mixProgress.getMixStep().getMessage().substring(0, 1).toUpperCase(Locale.ROOT) + mixProgress.getMixStep().getMessage().substring(1); + String status = mixProgress.getMixStep().getMessage().replaceAll("_", " "); + status = status.substring(0, 1).toUpperCase(Locale.ROOT) + status.substring(1).toLowerCase(Locale.ROOT); tt.setText(status); setTooltip(tt); - - if(mixProgress.getMixStep() == MixStep.REGISTERED_INPUT) { - tt.setOnShowing(event -> { - Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(utxoEntry.getWallet()); - Whirlpool.RegisteredInputsService registeredInputsService = new Whirlpool.RegisteredInputsService(whirlpool, mixProgress.getPoolId()); - registeredInputsService.setOnSucceeded(eventStateHandler -> { - if(registeredInputsService.getValue() != null) { - tt.setText(status + " (1 of " + registeredInputsService.getValue() + ")"); - } - }); - registeredInputsService.start(); - }); - } } else { setGraphic(null); setTooltip(null); diff --git a/src/main/java/com/sparrowwallet/sparrow/io/db/DbPersistence.java b/src/main/java/com/sparrowwallet/sparrow/io/db/DbPersistence.java index 7c3da094..8b214ee8 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/db/DbPersistence.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/db/DbPersistence.java @@ -256,7 +256,10 @@ public class DbPersistence implements Persistence { } for(Sha256Hash txid : referencedTxIds) { BlockTransaction blkTx = wallet.getTransactions().get(txid); - blockTransactionDao.addOrUpdate(wallet, txid, blkTx); + //May be null for a nested wallet if still updating + if(blkTx != null) { + blockTransactionDao.addOrUpdate(wallet, txid, blkTx); + } } if(!dirtyPersistables.clearHistory) { DetachedLabelDao detachedLabelDao = handle.attach(DetachedLabelDao.class); diff --git a/src/main/java/com/sparrowwallet/sparrow/net/BroadcastSource.java b/src/main/java/com/sparrowwallet/sparrow/net/BroadcastSource.java index 42663981..d338736f 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/BroadcastSource.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/BroadcastSource.java @@ -1,11 +1,11 @@ package com.sparrowwallet.sparrow.net; import com.google.common.net.HostAndPort; +import com.samourai.wallet.httpClient.HttpResponseException; import com.sparrowwallet.drongo.Network; import com.sparrowwallet.drongo.Utils; import com.sparrowwallet.drongo.protocol.Sha256Hash; import com.sparrowwallet.drongo.protocol.Transaction; -import com.sparrowwallet.nightjar.http.JavaHttpException; import com.sparrowwallet.sparrow.AppServices; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -158,7 +158,7 @@ public enum BroadcastSource { } catch(Exception e) { throw new BroadcastException("Could not retrieve txid from broadcast, server returned: " + response); } - } catch(JavaHttpException e) { + } catch(HttpResponseException e) { throw new BroadcastException("Could not broadcast transaction, server returned " + e.getStatusCode() + ": " + e.getResponseBody()); } catch(Exception e) { log.error("Could not post transaction via " + getName(), e); diff --git a/src/main/java/com/sparrowwallet/sparrow/net/ExchangeSource.java b/src/main/java/com/sparrowwallet/sparrow/net/ExchangeSource.java index 4bc5b58a..8a62dd2f 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/ExchangeSource.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/ExchangeSource.java @@ -1,6 +1,6 @@ package com.sparrowwallet.sparrow.net; -import com.sparrowwallet.nightjar.http.JavaHttpException; +import com.samourai.wallet.httpClient.HttpResponseException; import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.event.ExchangeRatesUpdatedEvent; import javafx.concurrent.ScheduledService; @@ -107,7 +107,7 @@ public enum ExchangeSource { if(log.isDebugEnabled()) { log.warn("Error retrieving historical currency rates", e); } else { - if(e instanceof JavaHttpException javaHttpException && javaHttpException.getStatusCode() == 404) { + if(e instanceof HttpResponseException httpException && httpException.getStatusCode() == 404) { log.warn("Error retrieving historical currency rates (" + e.getMessage() + "). BTC-" + currency.getCurrencyCode() + " may not be supported by " + this); } else { log.warn("Error retrieving historical currency rates (" + e.getMessage() + ")"); diff --git a/src/main/java/com/sparrowwallet/sparrow/net/HttpClientService.java b/src/main/java/com/sparrowwallet/sparrow/net/HttpClientService.java index 69d14510..1eed8425 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/HttpClientService.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/HttpClientService.java @@ -1,57 +1,52 @@ package com.sparrowwallet.sparrow.net; import com.google.common.net.HostAndPort; -import com.samourai.http.client.HttpUsage; -import com.samourai.http.client.IHttpClient; -import com.sparrowwallet.nightjar.http.JavaHttpClientService; +import com.samourai.http.client.JettyHttpClientService; +import com.samourai.wallet.httpClient.HttpUsage; +import com.samourai.wallet.httpClient.IHttpClient; +import com.samourai.wallet.util.AsyncUtil; +import com.samourai.wallet.util.ThreadUtil; +import com.samourai.whirlpool.client.utils.ClientUtils; import io.reactivex.Observable; -import java8.util.Optional; import javafx.concurrent.Service; import javafx.concurrent.Task; import java.util.Map; +import java.util.Optional; -public class HttpClientService { - private final JavaHttpClientService httpClientService; +public class HttpClientService extends JettyHttpClientService { + private static final int REQUEST_TIMEOUT = 120000; public HttpClientService(HostAndPort torProxy) { - this.httpClientService = new JavaHttpClientService(torProxy, 120000); + super(REQUEST_TIMEOUT, new HttpProxySupplier(torProxy)); } public T requestJson(String url, Class responseType, Map headers) throws Exception { - IHttpClient httpClient = httpClientService.getHttpClient(HttpUsage.COORDINATOR_REST); - return httpClient.getJson(url, responseType, headers); + return getHttpClient(HttpUsage.BACKEND).getJson(url, responseType, headers); } public Observable> postJson(String url, Class responseType, Map headers, Object body) { - IHttpClient httpClient = httpClientService.getHttpClient(HttpUsage.COORDINATOR_REST); - return httpClient.postJson(url, responseType, headers, body); + return getHttpClient(HttpUsage.BACKEND).postJson(url, responseType, headers, body).toObservable(); } public String postString(String url, Map headers, String contentType, String content) throws Exception { - IHttpClient httpClient = httpClientService.getHttpClient(HttpUsage.COORDINATOR_REST); - return httpClient.postString(url, headers, contentType, content); - } - - public void changeIdentity() { - HostAndPort torProxy = getTorProxy(); - if(torProxy != null) { - TorUtils.changeIdentity(torProxy); - } + IHttpClient httpClient = getHttpClient(HttpUsage.BACKEND); + return AsyncUtil.getInstance().blockingGet(httpClient.postString(url, headers, contentType, content)).get(); } public HostAndPort getTorProxy() { - return httpClientService.getTorProxy(); + return getHttpProxySupplier().getTorProxy(); } public void setTorProxy(HostAndPort torProxy) { //Ensure all http clients are shutdown first - httpClientService.shutdown(); - httpClientService.setTorProxy(torProxy); + stop(); + getHttpProxySupplier()._setTorProxy(torProxy); } - public void shutdown() { - httpClientService.shutdown(); + @Override + public HttpProxySupplier getHttpProxySupplier() { + return (HttpProxySupplier)super.getHttpProxySupplier(); } public static class ShutdownService extends Service { @@ -65,7 +60,7 @@ public class HttpClientService { protected Task createTask() { return new Task<>() { protected Boolean call() throws Exception { - httpClientService.shutdown(); + httpClientService.stop(); return true; } }; diff --git a/src/main/java/com/sparrowwallet/sparrow/net/HttpProxySupplier.java b/src/main/java/com/sparrowwallet/sparrow/net/HttpProxySupplier.java new file mode 100644 index 00000000..5a832652 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/net/HttpProxySupplier.java @@ -0,0 +1,51 @@ +package com.sparrowwallet.sparrow.net; + +import com.google.common.net.HostAndPort; +import com.samourai.http.client.IHttpProxySupplier; +import com.samourai.wallet.httpClient.HttpProxy; +import com.samourai.wallet.httpClient.HttpProxyProtocol; +import com.samourai.wallet.httpClient.HttpUsage; + +import java.util.Optional; + +public class HttpProxySupplier implements IHttpProxySupplier { + private HostAndPort torProxy; + private HttpProxy httpProxy; + + public HttpProxySupplier(HostAndPort torProxy) { + this.torProxy = torProxy; + this.httpProxy = computeHttpProxy(torProxy); + } + + private HttpProxy computeHttpProxy(HostAndPort hostAndPort) { + if (hostAndPort == null) { + return null; + } + + return new HttpProxy(HttpProxyProtocol.SOCKS, hostAndPort.getHost(), hostAndPort.getPort()); + } + + public HostAndPort getTorProxy() { + return torProxy; + } + + // shouldnt call directly but use httpClientService.setTorProxy() + public void _setTorProxy(HostAndPort hostAndPort) { + // set proxy + this.torProxy = hostAndPort; + this.httpProxy = computeHttpProxy(hostAndPort); + } + + @Override + public Optional getHttpProxy(HttpUsage httpUsage) { + return Optional.ofNullable(httpProxy); + } + + @Override + public void changeIdentity() { + HostAndPort torProxy = getTorProxy(); + if(torProxy != null) { + TorUtils.changeIdentity(torProxy); + } + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/net/TorUtils.java b/src/main/java/com/sparrowwallet/sparrow/net/TorUtils.java index 1f4fa27c..e012acd7 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/TorUtils.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/TorUtils.java @@ -10,7 +10,10 @@ import org.slf4j.LoggerFactory; import java.io.*; import java.net.Socket; import java.net.SocketTimeoutException; +import java.nio.file.AccessDeniedException; +import java.nio.file.FileSystemException; import java.nio.file.Files; +import java.nio.file.NoSuchFileException; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -37,6 +40,10 @@ public class TorUtils { log.warn("Error authenticating to Tor at " + control + ", server returned " + e.getMessage()); } catch(SocketTimeoutException e) { log.warn("Timeout reading from " + control + ", is this a Tor ControlPort?"); + } catch(AccessDeniedException e) { + log.warn("Permission denied reading Tor cookie file at " + e.getFile()); + } catch(FileSystemException e) { + log.warn("Error reading Tor cookie file at " + e.getFile()); } catch(Exception e) { log.warn("Error connecting to " + control + ", no Tor ControlPort configured?"); } diff --git a/src/main/java/com/sparrowwallet/sparrow/payjoin/Payjoin.java b/src/main/java/com/sparrowwallet/sparrow/payjoin/Payjoin.java index 33eba470..613af32e 100644 --- a/src/main/java/com/sparrowwallet/sparrow/payjoin/Payjoin.java +++ b/src/main/java/com/sparrowwallet/sparrow/payjoin/Payjoin.java @@ -2,7 +2,7 @@ package com.sparrowwallet.sparrow.payjoin; import com.google.common.collect.ImmutableMap; import com.google.gson.Gson; -import com.sparrowwallet.drongo.KeyPurpose; +import com.samourai.wallet.httpClient.HttpResponseException; import com.sparrowwallet.drongo.protocol.Script; import com.sparrowwallet.drongo.protocol.Transaction; import com.sparrowwallet.drongo.protocol.TransactionInput; @@ -14,7 +14,6 @@ import com.sparrowwallet.drongo.psbt.PSBTParseException; import com.sparrowwallet.drongo.uri.BitcoinURI; import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.drongo.wallet.WalletNode; -import com.sparrowwallet.nightjar.http.JavaHttpException; import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.net.HttpClientService; import com.sparrowwallet.sparrow.net.Protocol; @@ -23,7 +22,8 @@ import javafx.concurrent.Task; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.*; +import java.io.FileNotFoundException; +import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.util.*; @@ -87,7 +87,7 @@ public class Payjoin { checkProposal(psbt, proposalPsbt, changeOutputIndex, maxAdditionalFeeContribution, allowOutputSubstitution); return proposalPsbt; - } catch(JavaHttpException e) { + } catch(HttpResponseException e) { Gson gson = new Gson(); PayjoinReceiverError payjoinReceiverError = gson.fromJson(e.getResponseBody(), PayjoinReceiverError.class); log.warn("Payjoin receiver returned an error of " + payjoinReceiverError.getErrorCode() + " (" + payjoinReceiverError.getMessage() + ")"); diff --git a/src/main/java/com/sparrowwallet/sparrow/paynym/PayNymService.java b/src/main/java/com/sparrowwallet/sparrow/paynym/PayNymService.java index cd9b3c2d..d7a8f697 100644 --- a/src/main/java/com/sparrowwallet/sparrow/paynym/PayNymService.java +++ b/src/main/java/com/sparrowwallet/sparrow/paynym/PayNymService.java @@ -52,7 +52,7 @@ public class PayNymService { return AppServices.getHttpClientService().postJson(url, Map.class, headers, body) .subscribeOn(Schedulers.io()) .observeOn(JavaFxScheduler.platform()) - .map(Optional::get); + .map(o -> o.get()); } public static Observable> updateToken(PaymentCode paymentCode) { @@ -74,7 +74,7 @@ public class PayNymService { return AppServices.getHttpClientService().postJson(url, Map.class, headers, body) .subscribeOn(Schedulers.io()) .observeOn(JavaFxScheduler.platform()) - .map(Optional::get); + .map(o -> o.get()); } public static void claimPayNym(Wallet wallet, Map createMap, boolean segwit) { @@ -123,7 +123,7 @@ public class PayNymService { return AppServices.getHttpClientService().postJson(url, Map.class, headers, body) .subscribeOn(Schedulers.io()) .observeOn(JavaFxScheduler.platform()) - .map(Optional::get); + .map(o -> o.get()); } public static Observable> addPaymentCode(PaymentCode paymentCode, String authToken, String signature, boolean segwit) { @@ -152,7 +152,7 @@ public class PayNymService { return AppServices.getHttpClientService().postJson(url, Map.class, headers, body) .subscribeOn(Schedulers.io()) .observeOn(JavaFxScheduler.platform()) - .map(Optional::get); + .map(o -> o.get()); } public static Observable> followPaymentCode(com.samourai.wallet.bip47.rpc.PaymentCode paymentCode, String authToken, String signature) { @@ -176,7 +176,7 @@ public class PayNymService { return AppServices.getHttpClientService().postJson(url, Map.class, headers, body) .subscribeOn(Schedulers.io()) .observeOn(JavaFxScheduler.platform()) - .map(Optional::get); + .map(o -> o.get()); } public static Observable> fetchPayNym(String nymIdentifier, boolean compact) { @@ -194,7 +194,7 @@ public class PayNymService { return AppServices.getHttpClientService().postJson(url, Map.class, headers, body) .subscribeOn(Schedulers.io()) .observeOn(JavaFxScheduler.platform()) - .map(Optional::get); + .map(o -> o.get()); } public static Observable getPayNym(String nymIdentifier) { diff --git a/src/main/java/com/sparrowwallet/sparrow/soroban/CounterpartyController.java b/src/main/java/com/sparrowwallet/sparrow/soroban/CounterpartyController.java index 11d71df4..fa80ae7f 100644 --- a/src/main/java/com/sparrowwallet/sparrow/soroban/CounterpartyController.java +++ b/src/main/java/com/sparrowwallet/sparrow/soroban/CounterpartyController.java @@ -1,24 +1,32 @@ package com.sparrowwallet.sparrow.soroban; -import com.samourai.soroban.cahoots.CahootsContext; +import com.google.common.base.Throwables; import com.samourai.soroban.client.cahoots.OnlineCahootsMessage; -import com.samourai.soroban.client.cahoots.SorobanCahootsService; +import com.samourai.soroban.client.meeting.SorobanRequestMessage; +import com.samourai.soroban.client.wallet.SorobanWalletService; +import com.samourai.soroban.client.wallet.counterparty.SorobanWalletCounterparty; import com.samourai.wallet.bip47.rpc.PaymentCode; import com.samourai.wallet.cahoots.Cahoots; +import com.samourai.wallet.cahoots.CahootsContext; import com.samourai.wallet.cahoots.CahootsType; import com.sparrowwallet.drongo.protocol.Transaction; import com.sparrowwallet.drongo.psbt.PSBTParseException; -import com.sparrowwallet.drongo.wallet.*; +import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex; +import com.sparrowwallet.drongo.wallet.Wallet; +import com.sparrowwallet.drongo.wallet.WalletNode; import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.control.*; import com.sparrowwallet.sparrow.paynym.PayNymDialog; import com.sparrowwallet.sparrow.paynym.PayNymService; +import io.reactivex.functions.Consumer; import io.reactivex.rxjavafx.schedulers.JavaFxScheduler; import io.reactivex.schedulers.Schedulers; import javafx.application.Platform; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.collections.FXCollections; +import javafx.concurrent.Service; +import javafx.concurrent.Task; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.scene.control.*; @@ -31,6 +39,7 @@ import org.slf4j.LoggerFactory; import java.util.HashMap; import java.util.Map; import java.util.Optional; +import java.util.concurrent.TimeoutException; import static com.sparrowwallet.sparrow.AppServices.showErrorDialog; import static com.sparrowwallet.sparrow.soroban.Soroban.TIMEOUT_MS; @@ -226,41 +235,52 @@ public class CounterpartyController extends SorobanController { private void startCounterpartyMeetingReceive() { Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId); - SparrowCahootsWallet counterpartyCahootsWallet = soroban.getCahootsWallet(wallet, 1); + SorobanWalletService sorobanWalletService = soroban.getSorobanWalletService(); - try { - SorobanCahootsService sorobanMeetingService = soroban.getSorobanCahootsService(counterpartyCahootsWallet); - sorobanMeetingService.receiveMeetingRequest(TIMEOUT_MS) + SparrowCahootsWallet cahootsWallet = soroban.getCahootsWallet(wallet); + SorobanWalletCounterparty sorobanWalletCounterparty = sorobanWalletService.getSorobanWalletCounterparty(cahootsWallet); + sorobanWalletCounterparty.setTimeoutMeetingMs(TIMEOUT_MS); + + Service receiveMeetingService = new Service<>() { + @Override + protected Task createTask() { + return new Task<>() { + @Override + protected SorobanRequestMessage call() throws Exception { + return sorobanWalletCounterparty.receiveMeetingRequest(); + } + }; + } + }; + receiveMeetingService.setOnSucceeded(event -> { + SorobanRequestMessage requestMessage = receiveMeetingService.getValue(); + PaymentCode paymentCodeInitiator = requestMessage.getSender(); + CahootsType cahootsType = requestMessage.getType(); + updateMixPartner(paymentCodeInitiator, cahootsType); + Boolean accepted = (Boolean)Platform.enterNestedEventLoop(meetingAccepted); + + sorobanWalletCounterparty.sendMeetingResponse(requestMessage, accepted) .subscribeOn(Schedulers.io()) .observeOn(JavaFxScheduler.platform()) - .subscribe(requestMessage -> { - String code = requestMessage.getSender(); - CahootsType cahootsType = requestMessage.getType(); - PaymentCode paymentCodeInitiator = new PaymentCode(code); - updateMixPartner(paymentCodeInitiator, cahootsType); - Boolean accepted = (Boolean)Platform.enterNestedEventLoop(meetingAccepted); - sorobanMeetingService.sendMeetingResponse(paymentCodeInitiator, requestMessage, accepted) - .subscribeOn(Schedulers.io()) - .observeOn(JavaFxScheduler.platform()) - .subscribe(responseMessage -> { - requestUserAttention(); - if(accepted) { - startCounterpartyCollaboration(counterpartyCahootsWallet, paymentCodeInitiator, cahootsType); - followPaymentCode(paymentCodeInitiator); - } - }, error -> { - log.error("Error sending meeting response", error); - mixingPartner.setVisible(false); - requestUserAttention(); - }); + .subscribe(responseMessage -> { + requestUserAttention(); + if(accepted) { + startCounterpartyCollaboration(sorobanWalletCounterparty, paymentCodeInitiator, cahootsType, cahootsWallet.getAccount()); + followPaymentCode(paymentCodeInitiator); + } }, error -> { - log.error("Failed to receive meeting request", error); + log.error("Error sending meeting response", error); mixingPartner.setVisible(false); requestUserAttention(); }); - } catch(Exception e) { - log.error("Error sending meeting response", e); - } + }); + receiveMeetingService.setOnFailed(event -> { + Throwable e = event.getSource().getException(); + log.error("Failed to receive meeting request", e); + mixingPartner.setVisible(false); + requestUserAttention(); + }); + receiveMeetingService.start(); } private void updateMixPartner(PaymentCode paymentCodeInitiator, CahootsType cahootsType) { @@ -290,54 +310,67 @@ public class CounterpartyController extends SorobanController { meetingReceived.set(Boolean.TRUE); } - private void startCounterpartyCollaboration(SparrowCahootsWallet counterpartyCahootsWallet, PaymentCode initiatorPaymentCode, CahootsType cahootsType) { + private void startCounterpartyCollaboration(SorobanWalletCounterparty sorobanWalletCounterparty, PaymentCode initiatorPaymentCode, CahootsType cahootsType, int account) { sorobanProgressLabel.setText("Creating mix transaction..."); + SparrowCahootsWallet cahootsWallet = (SparrowCahootsWallet)sorobanWalletCounterparty.getCahootsWallet(); - Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId); Map walletUtxos = wallet.getSpendableUtxos(); for(Map.Entry entry : walletUtxos.entrySet()) { - counterpartyCahootsWallet.addUtxo(entry.getValue(), wallet.getWalletTransaction(entry.getKey().getHash()), (int)entry.getKey().getIndex()); + cahootsWallet.addUtxo(entry.getValue(), wallet.getWalletTransaction(entry.getKey().getHash()), (int)entry.getKey().getIndex()); } try { - SorobanCahootsService sorobanCahootsService = soroban.getSorobanCahootsService(counterpartyCahootsWallet); - CahootsContext cahootsContext = cahootsType == CahootsType.STONEWALLX2 ? CahootsContext.newCounterpartyStonewallx2() : CahootsContext.newCounterpartyStowaway(); - sorobanCahootsService.contributor(counterpartyCahootsWallet.getAccount(), cahootsContext, initiatorPaymentCode, TIMEOUT_MS) - .subscribeOn(Schedulers.io()) - .observeOn(JavaFxScheduler.platform()) - .subscribe(sorobanMessage -> { - OnlineCahootsMessage cahootsMessage = (OnlineCahootsMessage)sorobanMessage; - if(cahootsMessage != null) { - Cahoots cahoots = cahootsMessage.getCahoots(); - sorobanProgressBar.setProgress((double)(cahoots.getStep() + 1) / 5); + CahootsContext cahootsContext = CahootsContext.newCounterparty(cahootsWallet, cahootsType, account); + Consumer onProgress = cahootsMessage -> { + if(cahootsMessage != null) { + Platform.runLater(() -> { + Cahoots cahoots = cahootsMessage.getCahoots(); + sorobanProgressBar.setProgress((double)(cahoots.getStep() + 1) / 5); - if(cahoots.getStep() == 3) { - sorobanProgressLabel.setText("Your mix partner is reviewing the transaction..."); - step3Timer.start(); - } else if(cahoots.getStep() >= 4) { - try { - Transaction transaction = getTransaction(cahoots); - if(transaction != null) { - transactionProperty.set(transaction); - updateTransactionDiagram(transactionDiagram, wallet, null, transaction); - next(); - } - } catch(PSBTParseException e) { - log.error("Invalid collaborative PSBT created", e); - step3Desc.setText("Invalid transaction created."); - sorobanProgressLabel.setVisible(false); - } - } + if(cahoots.getStep() == 3) { + sorobanProgressLabel.setText("Your mix partner is reviewing the transaction..."); + step3Timer.start(); + } else if(cahoots.getStep() >= 4) { + try { + Transaction transaction = getTransaction(cahoots); + if(transaction != null) { + transactionProperty.set(transaction); + updateTransactionDiagram(transactionDiagram, wallet, null, transaction); + next(); } - }, error -> { - log.error("Error creating mix transaction", error); - String cutFrom = "Exception: "; - int index = error.getMessage().lastIndexOf(cutFrom); - String msg = index < 0 ? error.getMessage() : error.getMessage().substring(index + cutFrom.length()); - msg = msg.replace("#Cahoots", "mix transaction"); - step3Desc.setText(msg); + } catch(PSBTParseException e) { + log.error("Invalid collaborative PSBT created", e); + step3Desc.setText("Invalid transaction created."); sorobanProgressLabel.setVisible(false); - }); + } + } + }); + } + }; + + Service cahootsService = new Service<>() { + @Override + protected Task createTask() { + return new Task<>() { + @Override + protected Cahoots call() throws Exception { + return sorobanWalletCounterparty.counterparty(cahootsContext, initiatorPaymentCode, onProgress); + } + }; + } + }; + cahootsService.setOnFailed(event -> { + Throwable error = Throwables.getRootCause(event.getSource().getException()); + log.error("Error creating mix transaction", error); + String cutFrom = "Exception: "; + String message = error.getMessage() == null ? (error instanceof TimeoutException || step3Timer.getProgress() == 0d ? "Timed out receiving response" : "Error receiving response") : error.getMessage(); + int index = message.lastIndexOf(cutFrom); + String msg = index < 0 ? message : message.substring(index + cutFrom.length()); + msg = msg.replace("#Cahoots", "mix transaction"); + step3Desc.setText(msg); + sorobanProgressLabel.setVisible(false); + }); + cahootsService.start(); } catch(Exception e) { log.error("Error creating mix transaction", e); sorobanProgressLabel.setText(e.getMessage()); diff --git a/src/main/java/com/sparrowwallet/sparrow/soroban/InitiatorController.java b/src/main/java/com/sparrowwallet/sparrow/soroban/InitiatorController.java index 169c2381..87d9329c 100644 --- a/src/main/java/com/sparrowwallet/sparrow/soroban/InitiatorController.java +++ b/src/main/java/com/sparrowwallet/sparrow/soroban/InitiatorController.java @@ -1,12 +1,17 @@ package com.sparrowwallet.sparrow.soroban; import com.google.common.eventbus.Subscribe; -import com.samourai.soroban.cahoots.CahootsContext; +import com.samourai.soroban.client.OnlineSorobanInteraction; +import com.samourai.soroban.client.meeting.SorobanResponseMessage; +import com.samourai.soroban.client.wallet.SorobanWalletService; +import com.samourai.soroban.client.wallet.sender.CahootsSorobanInitiatorListener; +import com.samourai.wallet.cahoots.CahootsContext; import com.samourai.soroban.client.cahoots.OnlineCahootsMessage; -import com.samourai.soroban.client.cahoots.SorobanCahootsService; import com.samourai.wallet.bip47.rpc.PaymentCode; import com.samourai.wallet.cahoots.Cahoots; import com.samourai.wallet.cahoots.CahootsType; +import com.samourai.wallet.cahoots.TxBroadcastInteraction; +import com.samourai.wallet.sorobanClient.SorobanInteraction; import com.sparrowwallet.drongo.SecureString; import com.sparrowwallet.drongo.crypto.ECKey; import com.sparrowwallet.drongo.crypto.EncryptionType; @@ -33,8 +38,6 @@ import com.sparrowwallet.sparrow.paynym.PayNymAddress; import com.sparrowwallet.sparrow.paynym.PayNymDialog; import com.sparrowwallet.sparrow.paynym.PayNymService; import io.reactivex.Observable; -import io.reactivex.rxjavafx.schedulers.JavaFxScheduler; -import io.reactivex.schedulers.Schedulers; import javafx.application.Platform; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; @@ -42,6 +45,8 @@ import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.beans.value.ChangeListener; import javafx.collections.FXCollections; +import javafx.concurrent.Service; +import javafx.concurrent.Task; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.scene.control.*; @@ -58,11 +63,11 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.*; +import java.util.concurrent.TimeoutException; import java.util.function.UnaryOperator; import static com.sparrowwallet.sparrow.AppServices.showErrorDialog; import static com.sparrowwallet.sparrow.paynym.PayNymController.PAYNYM_REGEX; -import static com.sparrowwallet.sparrow.soroban.Soroban.TIMEOUT_MS; public class InitiatorController extends SorobanController { private static final Logger log = LoggerFactory.getLogger(InitiatorController.class); @@ -199,7 +204,7 @@ public class InitiatorController extends SorobanController { meetingFail.setVisible(false); step2Desc.setText("Retrying..."); sorobanProgressLabel.setVisible(true); - startInitiatorMeetingRequest(AppServices.getSorobanServices().getSoroban(walletId), wallet); + startInitiatorMeetAndInitiate(AppServices.getSorobanServices().getSoroban(walletId), wallet); step2Timer.start(); }); @@ -221,7 +226,7 @@ public class InitiatorController extends SorobanController { step2.visibleProperty().addListener((observable, oldValue, visible) -> { if(visible) { - startInitiatorMeetingRequest(); + startInitiatorMeetAndInitiate(); step2Timer.start(); } }); @@ -358,7 +363,7 @@ public class InitiatorController extends SorobanController { }); } - private void startInitiatorMeetingRequest() { + private void startInitiatorMeetAndInitiate() { Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId); if(soroban.getHdWallet() == null) { if(wallet.isEncrypted()) { @@ -377,7 +382,7 @@ public class InitiatorController extends SorobanController { try { soroban.setHDWallet(copy); - startInitiatorMeetingRequest(soroban, wallet); + startInitiatorMeetAndInitiate(soroban, wallet); } finally { key.clear(); encryptionFullKey.clear(); @@ -389,7 +394,7 @@ public class InitiatorController extends SorobanController { if(keyDerivationService.getException() instanceof InvalidPasswordException) { Optional optResponse = showErrorDialog("Invalid Password", "The wallet password was invalid. Try again?", ButtonType.CANCEL, ButtonType.OK); if(optResponse.isPresent() && optResponse.get().equals(ButtonType.OK)) { - Platform.runLater(this::startInitiatorMeetingRequest); + Platform.runLater(this::startInitiatorMeetAndInitiate); } } else { log.error("Error deriving wallet key", keyDerivationService.getException()); @@ -403,54 +408,123 @@ public class InitiatorController extends SorobanController { } } else { soroban.setHDWallet(wallet); - startInitiatorMeetingRequest(soroban, wallet); + startInitiatorMeetAndInitiate(soroban, wallet); } } else { - startInitiatorMeetingRequest(soroban, wallet); + startInitiatorMeetAndInitiate(soroban, wallet); } } - private void startInitiatorMeetingRequest(Soroban soroban, Wallet wallet) { - SparrowCahootsWallet initiatorCahootsWallet = soroban.getCahootsWallet(wallet, (long)walletTransaction.getFeeRate()); - + private void startInitiatorMeetAndInitiate(Soroban soroban, Wallet wallet) { getPaymentCodeCounterparty().subscribe(paymentCodeCounterparty -> { - try { - SorobanCahootsService sorobanMeetingService = soroban.getSorobanCahootsService(initiatorCahootsWallet); - sorobanMeetingService.sendMeetingRequest(paymentCodeCounterparty, cahootsType) - .subscribeOn(Schedulers.io()) - .observeOn(JavaFxScheduler.platform()) - .subscribe(meetingRequest -> { - sorobanProgressLabel.setText("Waiting for mix partner..."); - sorobanMeetingService.receiveMeetingResponse(paymentCodeCounterparty, meetingRequest, TIMEOUT_MS) - .subscribeOn(Schedulers.io()) - .observeOn(JavaFxScheduler.platform()) - .subscribe(sorobanResponse -> { - requestUserAttention(); - if(sorobanResponse.isAccept()) { - sorobanProgressBar.setProgress(0.1); - sorobanProgressLabel.setText("Mix partner accepted!"); - startInitiatorCollaborative(initiatorCahootsWallet, paymentCodeCounterparty); - } else { - step2Desc.setText("Mix partner declined."); - sorobanProgressLabel.setVisible(false); - } - }, error -> { - log.error("Error receiving meeting response", error); - step2Desc.setText(getErrorMessage(error)); - sorobanProgressLabel.setVisible(false); - meetingFail.setVisible(true); - requestUserAttention(); - }); - }, error -> { - log.error("Error sending meeting request", error); - step2Desc.setText(getErrorMessage(error)); - sorobanProgressLabel.setVisible(false); - meetingFail.setVisible(true); - requestUserAttention(); - }); - } catch(Exception e) { - log.error("Error sending meeting request", e); + SparrowCahootsWallet cahootsWallet = soroban.getCahootsWallet(wallet); + Map firstSetUtxos = walletTransaction.isCoinControlUsed() ? walletTransaction.getSelectedUtxoSets().get(0) : wallet.getSpendableUtxos(); + for(Map.Entry entry : firstSetUtxos.entrySet()) { + cahootsWallet.addUtxo(entry.getValue(), wallet.getWalletTransaction(entry.getKey().getHash()), (int)entry.getKey().getIndex()); } + + Payment payment = walletTransaction.getPayments().get(0); + long feePerB = (long)walletTransaction.getFeeRate(); + CahootsContext cahootsContext = CahootsContext.newInitiator(cahootsWallet, cahootsType, cahootsWallet.getAccount(), feePerB, payment.getAmount(), payment.getAddress().getAddress(), paymentCodeCounterparty.toString()); + + CahootsSorobanInitiatorListener listener = new CahootsSorobanInitiatorListener() { + @Override + public void onResponse(SorobanResponseMessage sorobanResponse) throws Exception { + super.onResponse(sorobanResponse); + + Platform.runLater(() -> { + requestUserAttention(); + if(sorobanResponse.isAccept()) { + sorobanProgressBar.setProgress(0.1); + sorobanProgressLabel.setText("Mix partner accepted!"); + } else { + step2Desc.setText("Mix partner declined."); + sorobanProgressLabel.setVisible(false); + } + }); + } + + @Override + public void onInteraction(OnlineSorobanInteraction interaction) throws Exception { + SorobanInteraction originInteraction = interaction.getInteraction(); + if(originInteraction instanceof TxBroadcastInteraction) { + Platform.runLater(() -> { + try { + Boolean accepted = (Boolean)Platform.enterNestedEventLoop(transactionAccepted); + if(accepted) { + interaction.sorobanAccept(); + } else { + interaction.sorobanReject("Mix partner declined to broadcast the transaction."); + } + } catch(Exception e) { + log.error("Error accepting Soroban interaction", e); + } + }); + } else { + throw new Exception("Unknown interaction: "+originInteraction.getTypeInteraction()); + } + } + + @Override + public void progress(OnlineCahootsMessage cahootsMessage) { + super.progress(cahootsMessage); + + if(cahootsMessage != null) { + Platform.runLater(() -> { + Cahoots cahoots = cahootsMessage.getCahoots(); + sorobanProgressBar.setProgress((double)(cahoots.getStep() + 1) / 5); + + if(cahoots.getStep() >= 3) { + try { + Transaction transaction = getTransaction(cahoots); + if(transaction != null) { + transactionProperty.set(transaction); + if(cahoots.getStep() == 3) { + next(); + step3Timer.start(e -> { + if(stepProperty.get() != Step.BROADCAST && stepProperty.get() != Step.REBROADCAST) { + step3Desc.setText("Transaction declined due to timeout."); + transactionAccepted.set(Boolean.FALSE); + } + }); + } else if(cahoots.getStep() == 4) { + next(); + broadcastTransaction(); + } + } + } catch(PSBTParseException e) { + log.error("Invalid collaborative PSBT created", e); + step2Desc.setText("Invalid transaction created."); + sorobanProgressLabel.setVisible(false); + } + } + }); + } + } + }; + SorobanWalletService sorobanWalletService = soroban.getSorobanWalletService(); + sorobanProgressLabel.setText("Waiting for mix partner..."); + + Service cahootsService = new Service<>() { + @Override + protected Task createTask() { + return new Task<>() { + @Override + protected Cahoots call() throws Exception { + return sorobanWalletService.getSorobanWalletInitiator(cahootsWallet).meetAndInitiate(cahootsContext, paymentCodeCounterparty, listener); + } + }; + } + }; + cahootsService.setOnFailed(event -> { + Throwable error = event.getSource().getException(); + log.error("Error receiving meeting response", error); + step2Desc.setText(getErrorMessage(error)); + sorobanProgressLabel.setVisible(false); + meetingFail.setVisible(true); + requestUserAttention(); + }); + cahootsService.start(); }, error -> { log.error("Could not retrieve payment code", error); if(error.getMessage().endsWith("404")) { @@ -464,77 +538,6 @@ public class InitiatorController extends SorobanController { }); } - private void startInitiatorCollaborative(SparrowCahootsWallet initiatorCahootsWallet, PaymentCode paymentCodeCounterparty) { - Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId); - - Payment payment = walletTransaction.getPayments().get(0); - Map firstSetUtxos = walletTransaction.isCoinControlUsed() ? walletTransaction.getSelectedUtxoSets().get(0) : wallet.getSpendableUtxos(); - for(Map.Entry entry : firstSetUtxos.entrySet()) { - initiatorCahootsWallet.addUtxo(entry.getValue(), wallet.getWalletTransaction(entry.getKey().getHash()), (int)entry.getKey().getIndex()); - } - - SorobanCahootsService sorobanCahootsService = soroban.getSorobanCahootsService(initiatorCahootsWallet); - CahootsContext cahootsContext = cahootsType == CahootsType.STONEWALLX2 ? - CahootsContext.newInitiatorStonewallx2(payment.getAmount(), payment.getAddress().toString()) : - CahootsContext.newInitiatorStowaway(payment.getAmount()); - - sorobanCahootsService.getSorobanService().getOnInteraction() - .observeOn(JavaFxScheduler.platform()) - .subscribe(interaction -> { - Boolean accepted = (Boolean)Platform.enterNestedEventLoop(transactionAccepted); - if(accepted) { - interaction.sorobanAccept(); - } else { - interaction.sorobanReject("Mix partner declined to broadcast the transaction."); - } - }); - - try { - sorobanCahootsService.initiator(initiatorCahootsWallet.getAccount(), cahootsContext, paymentCodeCounterparty, TIMEOUT_MS) - .subscribeOn(Schedulers.io()) - .observeOn(JavaFxScheduler.platform()) - .subscribe(sorobanMessage -> { - OnlineCahootsMessage cahootsMessage = (OnlineCahootsMessage)sorobanMessage; - if(cahootsMessage != null) { - Cahoots cahoots = cahootsMessage.getCahoots(); - sorobanProgressBar.setProgress((double)(cahoots.getStep() + 1) / 5); - - if(cahoots.getStep() >= 3) { - try { - Transaction transaction = getTransaction(cahoots); - if(transaction != null) { - transactionProperty.set(transaction); - if(cahoots.getStep() == 3) { - next(); - step3Timer.start(e -> { - if(stepProperty.get() != Step.BROADCAST && stepProperty.get() != Step.REBROADCAST) { - step3Desc.setText("Transaction declined due to timeout."); - transactionAccepted.set(Boolean.FALSE); - } - }); - } else if(cahoots.getStep() == 4) { - next(); - broadcastTransaction(); - } - } - } catch(PSBTParseException e) { - log.error("Invalid collaborative PSBT created", e); - step2Desc.setText("Invalid transaction created."); - sorobanProgressLabel.setVisible(false); - } - } - } - }, - error -> { - log.error("Error creating mix transaction", error); - step2Desc.setText(getErrorMessage(error)); - sorobanProgressLabel.setVisible(false); - }); - } catch(Exception e) { - log.error("Soroban communication error", e); - } - } - public void broadcastTransaction() { stepProperty.set(Step.BROADCAST); @@ -682,9 +685,10 @@ public class InitiatorController extends SorobanController { } private static String getErrorMessage(Throwable error) { + String message = error.getMessage() == null ? (error instanceof TimeoutException ? "Timed out receiving meeting response" : "Error receiving meeting response") : error.getMessage(); String cutFrom = "Exception: "; - int index = error.getMessage().lastIndexOf(cutFrom); - String msg = index < 0 ? error.getMessage() : error.getMessage().substring(index + cutFrom.length()); + int index = message.lastIndexOf(cutFrom); + String msg = index < 0 ? message : message.substring(index + cutFrom.length()); msg = msg.replace("#Cahoots", "mix transaction"); msg = msg.endsWith(".") ? msg : msg + "."; return msg; diff --git a/src/main/java/com/sparrowwallet/sparrow/soroban/Soroban.java b/src/main/java/com/sparrowwallet/sparrow/soroban/Soroban.java index e5ca0d57..b933b40e 100644 --- a/src/main/java/com/sparrowwallet/sparrow/soroban/Soroban.java +++ b/src/main/java/com/sparrowwallet/sparrow/soroban/Soroban.java @@ -1,48 +1,36 @@ package com.sparrowwallet.sparrow.soroban; -import com.google.common.net.HostAndPort; -import com.samourai.http.client.HttpUsage; -import com.samourai.http.client.IHttpClient; -import com.samourai.soroban.client.SorobanServer; -import com.samourai.soroban.client.cahoots.SorobanCahootsService; -import com.samourai.soroban.client.rpc.RpcClient; -import com.samourai.wallet.bip47.rpc.java.Bip47UtilJava; -import com.samourai.wallet.cahoots.CahootsWallet; +import com.samourai.soroban.client.SorobanConfig; +import com.samourai.soroban.client.wallet.SorobanWalletService; import com.samourai.wallet.hd.HD_Wallet; -import com.samourai.wallet.hd.HD_WalletFactoryGeneric; -import com.sparrowwallet.drongo.Drongo; +import com.samourai.wallet.util.ExtLibJConfig; import com.sparrowwallet.drongo.Network; import com.sparrowwallet.drongo.protocol.ScriptType; -import com.sparrowwallet.drongo.wallet.Keystore; import com.sparrowwallet.drongo.wallet.Wallet; -import com.sparrowwallet.nightjar.http.JavaHttpClientService; import com.sparrowwallet.sparrow.AppServices; -import javafx.concurrent.Service; -import javafx.concurrent.Task; +import com.sparrowwallet.sparrow.whirlpool.Whirlpool; +import com.sparrowwallet.sparrow.whirlpool.dataSource.SparrowChainSupplier; +import org.bitcoinj.core.NetworkParameters; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.security.Provider; -import java.util.*; +import java.util.List; public class Soroban { private static final Logger log = LoggerFactory.getLogger(Soroban.class); - protected static final HD_WalletFactoryGeneric hdWalletFactory = HD_WalletFactoryGeneric.getInstance(); - protected static final Bip47UtilJava bip47Util = Bip47UtilJava.getInstance(); - protected static final Provider PROVIDER_JAVA = Drongo.getProvider(); protected static final int TIMEOUT_MS = 60000; public static final List SOROBAN_NETWORKS = List.of(Network.MAINNET, Network.TESTNET); - private final SorobanServer sorobanServer; - private final JavaHttpClientService httpClientService; + private final SorobanWalletService sorobanWalletService; private HD_Wallet hdWallet; private int bip47Account; + private SparrowChainSupplier chainSupplier; - public Soroban(Network network, HostAndPort torProxy) { - this.sorobanServer = SorobanServer.valueOf(network.getName().toUpperCase(Locale.ROOT)); - this.httpClientService = new JavaHttpClientService(torProxy); + public Soroban() { + SorobanConfig sorobanConfig = AppServices.getWhirlpoolServices().getSorobanConfig(); + this.sorobanWalletService = sorobanConfig.getSorobanWalletService(); } public HD_Wallet getHdWallet() { @@ -50,25 +38,13 @@ public class Soroban { } public void setHDWallet(Wallet wallet) { - if(wallet.isEncrypted()) { - throw new IllegalStateException("Wallet cannot be encrypted"); - } - - try { - Keystore keystore = wallet.getKeystores().get(0); - ScriptType scriptType = wallet.getScriptType(); - int purpose = scriptType.getDefaultDerivation().get(0).num(); - List words = keystore.getSeed().getMnemonicCode(); - String passphrase = keystore.getSeed().getPassphrase() == null ? "" : keystore.getSeed().getPassphrase().asString(); - byte[] seed = hdWalletFactory.computeSeedFromWords(words); - hdWallet = new HD_Wallet(purpose, new ArrayList<>(words), sorobanServer.getParams(), seed, passphrase); - bip47Account = wallet.isMasterWallet() ? wallet.getAccountIndex() : wallet.getMasterWallet().getAccountIndex(); - } catch(Exception e) { - throw new IllegalStateException("Could not create Soroban HD wallet ", e); - } + ExtLibJConfig extLibJConfig = sorobanWalletService.getSorobanService().getSorobanConfig().getExtLibJConfig(); + NetworkParameters params = extLibJConfig.getSamouraiNetwork().getParams(); + hdWallet = Whirlpool.computeHdWallet(wallet, params); + bip47Account = wallet.isMasterWallet() ? wallet.getAccountIndex() : wallet.getMasterWallet().getAccountIndex(); } - public SparrowCahootsWallet getCahootsWallet(Wallet wallet, double feeRate) { + public SparrowCahootsWallet getCahootsWallet(Wallet wallet) { if(wallet.getScriptType() != ScriptType.P2WPKH) { throw new IllegalArgumentException("Wallet must be P2WPKH"); } @@ -87,7 +63,11 @@ public class Soroban { } try { - return new SparrowCahootsWallet(wallet, hdWallet, bip47Account, sorobanServer, (long)feeRate); + if(chainSupplier == null) { + chainSupplier = new SparrowChainSupplier(wallet.getStoredBlockHeight()); + chainSupplier.open(); + } + return new SparrowCahootsWallet(chainSupplier, wallet, hdWallet, bip47Account); } catch(Exception e) { log.error("Could not create cahoots wallet", e); } @@ -95,41 +75,17 @@ public class Soroban { return null; } - public SorobanCahootsService getSorobanCahootsService(CahootsWallet cahootsWallet) { - IHttpClient httpClient = httpClientService.getHttpClient(HttpUsage.COORDINATOR_REST); - RpcClient rpcClient = new RpcClient(httpClient, httpClientService.getTorProxy() != null, sorobanServer.getParams()); - return new SorobanCahootsService(bip47Util, PROVIDER_JAVA, cahootsWallet, rpcClient); + public int getBip47Account() { + return bip47Account; } - public HostAndPort getTorProxy() { - return httpClientService.getTorProxy(); + public SorobanWalletService getSorobanWalletService() { + return sorobanWalletService; } - public void setTorProxy(HostAndPort torProxy) { - //Ensure all http clients are shutdown first - httpClientService.shutdown(); - httpClientService.setTorProxy(torProxy); - } - - public void shutdown() { - httpClientService.shutdown(); - } - - public static class ShutdownService extends Service { - private final Soroban soroban; - - public ShutdownService(Soroban soroban) { - this.soroban = soroban; - } - - @Override - protected Task createTask() { - return new Task<>() { - protected Boolean call() throws Exception { - soroban.shutdown(); - return true; - } - }; + public void close() { + if(chainSupplier != null) { + chainSupplier.close(); } } } diff --git a/src/main/java/com/sparrowwallet/sparrow/soroban/SorobanServices.java b/src/main/java/com/sparrowwallet/sparrow/soroban/SorobanServices.java index 68484109..ae8384b0 100644 --- a/src/main/java/com/sparrowwallet/sparrow/soroban/SorobanServices.java +++ b/src/main/java/com/sparrowwallet/sparrow/soroban/SorobanServices.java @@ -1,7 +1,6 @@ package com.sparrowwallet.sparrow.soroban; import com.google.common.eventbus.Subscribe; -import com.google.common.net.HostAndPort; import com.sparrowwallet.drongo.Network; import com.sparrowwallet.drongo.protocol.ScriptType; import com.sparrowwallet.drongo.wallet.DeterministicSeed; @@ -9,17 +8,12 @@ import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.WalletTabData; import com.sparrowwallet.sparrow.event.WalletTabsClosedEvent; -import com.sparrowwallet.sparrow.io.Config; import com.sparrowwallet.sparrow.io.Storage; -import com.sparrowwallet.sparrow.net.TorService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.HashMap; import java.util.Map; -import java.util.Objects; - -import static com.sparrowwallet.sparrow.AppServices.getTorProxy; public class SorobanServices { private static final Logger log = LoggerFactory.getLogger(SorobanServices.class); @@ -40,14 +34,8 @@ public class SorobanServices { public Soroban getSoroban(String walletId) { Soroban soroban = sorobanMap.get(walletId); if(soroban == null) { - HostAndPort torProxy = getTorProxy(); - soroban = new Soroban(Network.get(), torProxy); + soroban = new Soroban(); sorobanMap.put(walletId, soroban); - } else { - HostAndPort torProxy = getTorProxy(); - if(!Objects.equals(soroban.getTorProxy(), torProxy)) { - soroban.setTorProxy(getTorProxy()); - } } return soroban; @@ -67,11 +55,7 @@ public class SorobanServices { String walletId = walletTabData.getStorage().getWalletId(walletTabData.getWallet()); Soroban soroban = sorobanMap.remove(walletId); if(soroban != null) { - Soroban.ShutdownService shutdownService = new Soroban.ShutdownService(soroban); - shutdownService.setOnFailed(failedEvent -> { - log.error("Failed to shutdown soroban", failedEvent.getSource().getException()); - }); - shutdownService.start(); + soroban.close(); } } } diff --git a/src/main/java/com/sparrowwallet/sparrow/soroban/SparrowCahootsWallet.java b/src/main/java/com/sparrowwallet/sparrow/soroban/SparrowCahootsWallet.java index 9f8750cb..d15cd9c0 100644 --- a/src/main/java/com/sparrowwallet/sparrow/soroban/SparrowCahootsWallet.java +++ b/src/main/java/com/sparrowwallet/sparrow/soroban/SparrowCahootsWallet.java @@ -1,12 +1,14 @@ package com.sparrowwallet.sparrow.soroban; -import com.samourai.soroban.client.SorobanServer; import com.samourai.wallet.api.backend.beans.UnspentOutput; +import com.samourai.wallet.bip47.rpc.BIP47Wallet; import com.samourai.wallet.bip47.rpc.PaymentAddress; import com.samourai.wallet.bip47.rpc.PaymentCode; import com.samourai.wallet.bip47.rpc.java.Bip47UtilJava; +import com.samourai.wallet.bipFormat.BipFormat; +import com.samourai.wallet.cahoots.AbstractCahootsWallet; import com.samourai.wallet.cahoots.CahootsUtxo; -import com.samourai.wallet.cahoots.SimpleCahootsWallet; +import com.samourai.wallet.chain.ChainSupplier; import com.samourai.wallet.hd.HD_Address; import com.samourai.wallet.hd.HD_Wallet; import com.samourai.wallet.send.MyTransactionOutPoint; @@ -17,21 +19,27 @@ import com.sparrowwallet.drongo.wallet.StandardAccount; import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.drongo.wallet.WalletNode; import com.sparrowwallet.sparrow.whirlpool.Whirlpool; -import org.apache.commons.lang3.tuple.Pair; +import org.bitcoinj.core.NetworkParameters; +import java.util.HashMap; import java.util.LinkedList; import java.util.List; +import java.util.Map; -public class SparrowCahootsWallet extends SimpleCahootsWallet { +public class SparrowCahootsWallet extends AbstractCahootsWallet { private final Wallet wallet; + private final HD_Wallet bip84w; private final int account; - private final int bip47Account; + private final List utxos; + private final Map lastWalletNodes = new HashMap<>(); - public SparrowCahootsWallet(Wallet wallet, HD_Wallet bip84w, int bip47Account, SorobanServer sorobanServer, long feePerB) throws Exception { - super(bip84w, sorobanServer.getParams(), wallet.getFreshNode(KeyPurpose.CHANGE).getIndex(), feePerB); + public SparrowCahootsWallet(ChainSupplier chainSupplier, Wallet wallet, HD_Wallet bip84w, int bip47Account) { + super(chainSupplier, bip84w.getFingerprint(), new BIP47Wallet(bip84w).getAccount(bip47Account)); this.wallet = wallet; + this.bip84w = bip84w; this.account = wallet.getAccountIndex(); - this.bip47Account = bip47Account; + this.utxos = new LinkedList<>(); + bip84w.getAccount(account).getReceive().setAddrIdx(wallet.getFreshNode(KeyPurpose.RECEIVE).getIndex()); bip84w.getAccount(account).getChange().setAddrIdx(wallet.getFreshNode(KeyPurpose.CHANGE).getIndex()); @@ -42,32 +50,6 @@ public class SparrowCahootsWallet extends SimpleCahootsWallet { } } - public void addUtxo(WalletNode node, BlockTransaction blockTransaction, int index) { - if(node.getWallet().getScriptType() != ScriptType.P2WPKH) { - return; - } - - UnspentOutput unspentOutput = Whirlpool.getUnspentOutput(node, blockTransaction, index); - MyTransactionOutPoint myTransactionOutPoint = unspentOutput.computeOutpoint(getParams()); - - CahootsUtxo cahootsUtxo; - if(node.getWallet().isBip47()) { - try { - String strPaymentCode = node.getWallet().getKeystores().get(0).getExternalPaymentCode().toString(); - HD_Address hdAddress = getBip47Wallet().getAccount(getBip47Account()).addressAt(node.getIndex()); - PaymentAddress paymentAddress = Bip47UtilJava.getInstance().getPaymentAddress(new PaymentCode(strPaymentCode), 0, hdAddress, getParams()); - cahootsUtxo = new CahootsUtxo(myTransactionOutPoint, node.getDerivationPath(), paymentAddress.getReceiveECKey()); - } catch(Exception e) { - throw new IllegalStateException("Cannot add BIP47 UTXO", e); - } - } else { - HD_Address hdAddress = getBip84Wallet().getAddressAt(account, unspentOutput); - cahootsUtxo = new CahootsUtxo(myTransactionOutPoint, node.getDerivationPath(), hdAddress.getECKey()); - } - - addUtxo(account, cahootsUtxo); - } - public Wallet getWallet() { return wallet; } @@ -77,28 +59,29 @@ public class SparrowCahootsWallet extends SimpleCahootsWallet { } @Override - protected List fetchUtxos(int account) { - List utxos = super.fetchUtxos(account); - if(utxos == null) { - utxos = new LinkedList<>(); + protected String doFetchAddressReceive(int account, boolean increment, BipFormat bipFormat) throws Exception { + if(account == StandardAccount.WHIRLPOOL_POSTMIX.getAccountNumber()) { + // force change chain + return getAddress(account, increment, KeyPurpose.CHANGE); } + return getAddress(account, increment, KeyPurpose.RECEIVE); + } + + @Override + protected String doFetchAddressChange(int account, boolean increment, BipFormat bipFormat) throws Exception { + return getAddress(account, increment, KeyPurpose.CHANGE); + } + + @Override + public List getUtxosWpkhByAccount(int account) { return utxos; } - @Override - public Pair fetchReceiveIndex(int account) throws Exception { - if(account == StandardAccount.WHIRLPOOL_POSTMIX.getAccountNumber()) { - // force change chain - return Pair.of(getWallet(account).getFreshNode(KeyPurpose.CHANGE).getIndex(), 1); - } - - return Pair.of(getWallet(account).getFreshNode(KeyPurpose.RECEIVE).getIndex(), 0); - } - - @Override - public Pair fetchChangeIndex(int account) throws Exception { - return Pair.of(getWallet(account).getFreshNode(KeyPurpose.CHANGE).getIndex(), 1); + private String getAddress(int account, boolean increment, KeyPurpose keyPurpose) { + WalletNode addressNode = getWallet(account).getFreshNode(keyPurpose, increment ? lastWalletNodes.get(keyPurpose) : null); + lastWalletNodes.put(keyPurpose, addressNode); + return addressNode.getAddress().getAddress(); } private Wallet getWallet(int account) { @@ -108,9 +91,30 @@ public class SparrowCahootsWallet extends SimpleCahootsWallet { return wallet; } + public void addUtxo(WalletNode node, BlockTransaction blockTransaction, int index) { + if(node.getWallet().getScriptType() != ScriptType.P2WPKH) { + return; + } - @Override - public int getBip47Account() { - return bip47Account; + NetworkParameters params = getBip47Account().getParams(); + UnspentOutput unspentOutput = Whirlpool.getUnspentOutput(node, blockTransaction, index); + MyTransactionOutPoint myTransactionOutPoint = unspentOutput.computeOutpoint(params); + + CahootsUtxo cahootsUtxo; + if(node.getWallet().isBip47()) { + try { + String strPaymentCode = node.getWallet().getKeystores().get(0).getExternalPaymentCode().toString(); + HD_Address hdAddress = getBip47Account().addressAt(node.getIndex()); + PaymentAddress paymentAddress = Bip47UtilJava.getInstance().getPaymentAddress(new PaymentCode(strPaymentCode), 0, hdAddress, params); + cahootsUtxo = new CahootsUtxo(myTransactionOutPoint, node.getDerivationPath(), null, paymentAddress.getReceiveECKey().getPrivKeyBytes()); + } catch(Exception e) { + throw new IllegalStateException("Cannot add BIP47 UTXO", e); + } + } else { + HD_Address hdAddress = bip84w.getAddressAt(account, unspentOutput); + cahootsUtxo = new CahootsUtxo(myTransactionOutPoint, node.getDerivationPath(), null, hdAddress.getECKey().getPrivKeyBytes()); + } + + utxos.add(cahootsUtxo); } } diff --git a/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/MixPoolDialog.java b/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/MixPoolDialog.java index 4cd31961..6142fafe 100644 --- a/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/MixPoolDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/MixPoolDialog.java @@ -206,7 +206,7 @@ public class MixPoolDialog extends WalletDialog { }); tx0PreviewsService.setOnSucceeded(workerStateEvent -> { tx0Previews = tx0PreviewsService.getValue(); - Tx0Preview tx0Preview = tx0Previews.getTx0Preview(pool.getPoolId()); + Tx0Preview tx0Preview = tx0Previews.getTx0Preview(this.pool.getSelectedItem() == null ? pool.getPoolId() : this.pool.getSelectedItem().pool.getPoolId()); tx0PreviewProperty.set(tx0Preview); }); tx0PreviewsService.setOnFailed(workerStateEvent -> { diff --git a/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/table/MixTableCell.java b/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/table/MixTableCell.java index b4d02397..4bcc84e7 100644 --- a/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/table/MixTableCell.java +++ b/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/table/MixTableCell.java @@ -43,7 +43,7 @@ public class MixTableCell extends TableCell { private String getMixFail(UtxoEntry.MixStatus mixStatus) { long elapsed = mixStatus.getMixErrorTimestamp() == null ? 0L : System.currentTimeMillis() - mixStatus.getMixErrorTimestamp(); - if(mixStatus.getMixFailReason() == MixFailReason.CANCEL || elapsed >= ERROR_DISPLAY_MILLIS) { + if(!mixStatus.getMixFailReason().isError() || elapsed >= ERROR_DISPLAY_MILLIS) { return getMixCountOnly(mixStatus); } diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletTransactionsEntry.java b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletTransactionsEntry.java index 72a30f17..5926f211 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletTransactionsEntry.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletTransactionsEntry.java @@ -129,9 +129,10 @@ public class WalletTransactionsEntry extends Entry { private static void getWalletTransactions(Wallet wallet, Map walletTransactionMap, WalletNode purposeNode) { KeyPurpose keyPurpose = purposeNode.getKeyPurpose(); List childNodes = new ArrayList<>(purposeNode.getChildren()); + Wallet transactionsWallet = wallet.isNested() ? wallet.getMasterWallet() : wallet; for(WalletNode addressNode : childNodes) { for(BlockTransactionHashIndex hashIndex : addressNode.getTransactionOutputs()) { - BlockTransaction inputTx = wallet.getWalletTransaction(hashIndex.getHash()); + BlockTransaction inputTx = transactionsWallet.getWalletTransaction(hashIndex.getHash()); //A null inputTx here means the wallet is still updating - ignore as the WalletHistoryChangedEvent will run this again if(inputTx != null) { WalletTransaction inputWalletTx = walletTransactionMap.get(inputTx); @@ -142,7 +143,7 @@ public class WalletTransactionsEntry extends Entry { inputWalletTx.incoming.put(hashIndex, keyPurpose); if(hashIndex.getSpentBy() != null) { - BlockTransaction outputTx = wallet.getWalletTransaction(hashIndex.getSpentBy().getHash()); + BlockTransaction outputTx = transactionsWallet.getWalletTransaction(hashIndex.getSpentBy().getHash()); if(outputTx != null) { WalletTransaction outputWalletTx = walletTransactionMap.get(outputTx); if(outputWalletTx == null) { diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletUtxosEntry.java b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletUtxosEntry.java index 6c3208ac..4b6f4ca0 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletUtxosEntry.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletUtxosEntry.java @@ -6,6 +6,7 @@ import com.sparrowwallet.drongo.wallet.WalletNode; import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.io.Config; import com.sparrowwallet.sparrow.whirlpool.Whirlpool; +import javafx.application.Platform; import java.util.*; import java.util.stream.Collectors; @@ -92,7 +93,8 @@ public class WalletUtxosEntry extends Entry { calculateDuplicates(); calculateDust(); - updateMixProgress(); + //Update mix status after SparrowUtxoSupplier has refreshed + Platform.runLater(this::updateMixProgress); } public long getBalance() { diff --git a/src/main/java/com/sparrowwallet/sparrow/whirlpool/Whirlpool.java b/src/main/java/com/sparrowwallet/sparrow/whirlpool/Whirlpool.java index 21291026..2b71fb9c 100644 --- a/src/main/java/com/sparrowwallet/sparrow/whirlpool/Whirlpool.java +++ b/src/main/java/com/sparrowwallet/sparrow/whirlpool/Whirlpool.java @@ -1,25 +1,35 @@ package com.sparrowwallet.sparrow.whirlpool; import com.google.common.eventbus.Subscribe; -import com.google.common.net.HostAndPort; -import com.samourai.tor.client.TorClientService; +import com.samourai.soroban.client.SorobanConfig; import com.samourai.wallet.api.backend.beans.UnspentOutput; +import com.samourai.wallet.bipFormat.BIP_FORMAT; +import com.samourai.wallet.bipWallet.WalletSupplier; +import com.samourai.wallet.constants.BIP_WALLETS; +import com.samourai.wallet.constants.SamouraiAccount; +import com.samourai.wallet.constants.SamouraiNetwork; import com.samourai.wallet.hd.HD_Wallet; import com.samourai.wallet.hd.HD_WalletFactoryGeneric; +import com.samourai.wallet.util.AsyncUtil; +import com.samourai.wallet.util.FormatsUtilGeneric; import com.samourai.whirlpool.client.event.*; import com.samourai.whirlpool.client.mix.handler.IPostmixHandler; -import com.samourai.whirlpool.client.tx0.*; +import com.samourai.whirlpool.client.tx0.Tx0; +import com.samourai.whirlpool.client.tx0.Tx0Config; +import com.samourai.whirlpool.client.tx0.Tx0Info; +import com.samourai.whirlpool.client.tx0.Tx0Previews; import com.samourai.whirlpool.client.wallet.WhirlpoolEventService; import com.samourai.whirlpool.client.wallet.WhirlpoolWallet; import com.samourai.whirlpool.client.wallet.WhirlpoolWalletConfig; import com.samourai.whirlpool.client.wallet.WhirlpoolWalletService; import com.samourai.whirlpool.client.wallet.beans.*; +import com.samourai.whirlpool.client.wallet.data.WhirlpoolInfo; +import com.samourai.whirlpool.client.wallet.data.coordinator.CoordinatorSupplier; import com.samourai.whirlpool.client.wallet.data.dataPersister.DataPersisterFactory; +import com.samourai.whirlpool.client.wallet.data.dataSource.DataSourceConfig; import com.samourai.whirlpool.client.wallet.data.dataSource.DataSourceFactory; -import com.samourai.whirlpool.client.wallet.data.pool.ExpirablePoolSupplier; import com.samourai.whirlpool.client.wallet.data.utxo.UtxoSupplier; -import com.samourai.whirlpool.client.wallet.data.utxoConfig.UtxoConfig; -import com.samourai.whirlpool.client.whirlpool.ServerApi; +import com.samourai.whirlpool.client.whirlpool.WhirlpoolClientConfig; import com.samourai.whirlpool.client.whirlpool.beans.Pool; import com.sparrowwallet.drongo.ExtendedKey; import com.sparrowwallet.drongo.KeyPurpose; @@ -27,18 +37,19 @@ import com.sparrowwallet.drongo.Network; import com.sparrowwallet.drongo.Utils; import com.sparrowwallet.drongo.protocol.*; import com.sparrowwallet.drongo.wallet.*; -import com.sparrowwallet.nightjar.http.JavaHttpClientService; -import com.sparrowwallet.nightjar.stomp.JavaStompClientService; import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.event.WhirlpoolMixEvent; import com.sparrowwallet.sparrow.event.WhirlpoolMixSuccessEvent; import com.sparrowwallet.sparrow.wallet.UtxoEntry; import com.sparrowwallet.sparrow.whirlpool.dataPersister.SparrowDataPersister; +import com.sparrowwallet.sparrow.whirlpool.dataSource.SparrowChainSupplier; import com.sparrowwallet.sparrow.whirlpool.dataSource.SparrowDataSource; import com.sparrowwallet.sparrow.whirlpool.dataSource.SparrowMinerFeeSupplier; import com.sparrowwallet.sparrow.whirlpool.dataSource.SparrowPostmixHandler; -import com.sparrowwallet.sparrow.whirlpool.tor.SparrowTorClientService; +import io.reactivex.Single; +import io.reactivex.rxjavafx.schedulers.JavaFxScheduler; +import io.reactivex.schedulers.Schedulers; import javafx.application.Platform; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; @@ -46,9 +57,11 @@ import javafx.concurrent.ScheduledService; import javafx.concurrent.Service; import javafx.concurrent.Task; import javafx.util.Duration; +import org.bitcoinj.core.NetworkParameters; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.lang.reflect.Field; import java.util.*; import java.util.stream.Collectors; @@ -58,17 +71,11 @@ public class Whirlpool { public static final List WHIRLPOOL_NETWORKS = List.of(Network.MAINNET, Network.TESTNET); public static final int DEFAULT_MIXTO_MIN_MIXES = 3; public static final int DEFAULT_MIXTO_RANDOM_FACTOR = 4; - protected static final int TIMEOUT_MS = 60000; - private final WhirlpoolServer whirlpoolServer; - private final JavaHttpClientService httpClientService; - private final JavaStompClientService stompClientService; - private final TorClientService torClientService; private final WhirlpoolWalletService whirlpoolWalletService; private final WhirlpoolWalletConfig config; - private final Tx0ParamService tx0ParamService; - private final ExpirablePoolSupplier poolSupplier; - private final Tx0Service tx0Service; + private WhirlpoolInfo whirlpoolInfo; + private Tx0Info tx0Info; private Tx0FeeTarget tx0FeeTarget = Tx0FeeTarget.BLOCKS_4; private Tx0FeeTarget mixFeeTarget = Tx0FeeTarget.BLOCKS_4; private HD_Wallet hdWallet; @@ -83,64 +90,61 @@ public class Whirlpool { private final BooleanProperty stoppingProperty = new SimpleBooleanProperty(false); private final BooleanProperty mixingProperty = new SimpleBooleanProperty(false); - public Whirlpool(Network network, HostAndPort torProxy) { - this.whirlpoolServer = WhirlpoolServer.valueOf(network.getName().toUpperCase(Locale.ROOT)); - this.httpClientService = new JavaHttpClientService(torProxy, TIMEOUT_MS); - this.stompClientService = new JavaStompClientService(httpClientService); - this.torClientService = new SparrowTorClientService(this); - + public Whirlpool(Integer storedBlockHeight) { this.whirlpoolWalletService = new WhirlpoolWalletService(); - this.config = computeWhirlpoolWalletConfig(torProxy); - this.tx0ParamService = new Tx0ParamService(SparrowMinerFeeSupplier.getInstance(), config); - this.poolSupplier = new ExpirablePoolSupplier(config.getRefreshPoolsDelay(), config.getServerApi(), tx0ParamService); - this.tx0Service = new Tx0Service(config); + this.config = computeWhirlpoolWalletConfig(storedBlockHeight); + this.tx0Info = null; // instantiated by getTx0Info() + this.whirlpoolInfo = null; // instantiated by getWhirlpoolInfo() WhirlpoolEventService.getInstance().register(this); } - private WhirlpoolWalletConfig computeWhirlpoolWalletConfig(HostAndPort torProxy) { - DataPersisterFactory dataPersisterFactory = (whirlpoolWallet, bip44w) -> new SparrowDataPersister(whirlpoolWallet, config.getPersistDelaySeconds()); - DataSourceFactory dataSourceFactory = (whirlpoolWallet, bip44w, dataPersister) -> new SparrowDataSource(whirlpoolWallet, bip44w, dataPersister); + private WhirlpoolWalletConfig computeWhirlpoolWalletConfig(Integer storedBlockHeight) { + SorobanConfig sorobanConfig = AppServices.getWhirlpoolServices().getSorobanConfig(); + DataSourceConfig dataSourceConfig = computeDataSourceConfig(storedBlockHeight); + DataSourceFactory dataSourceFactory = (whirlpoolWallet, bip44w, passphrase, walletStateSupplier, utxoConfigSupplier) -> new SparrowDataSource(whirlpoolWallet, bip44w, walletStateSupplier, utxoConfigSupplier, dataSourceConfig); - boolean onion = (torProxy != null); - String serverUrl = whirlpoolServer.getServerUrl(onion); - ServerApi serverApi = new ServerApi(serverUrl, httpClientService); - - WhirlpoolWalletConfig whirlpoolWalletConfig = new WhirlpoolWalletConfig(dataSourceFactory, httpClientService, stompClientService, torClientService, serverApi, whirlpoolServer.getParams(), false); + WhirlpoolWalletConfig whirlpoolWalletConfig = new WhirlpoolWalletConfig(dataSourceFactory, sorobanConfig, false); + DataPersisterFactory dataPersisterFactory = (whirlpoolWallet, bip44w) -> new SparrowDataPersister(whirlpoolWallet, whirlpoolWalletConfig.getPersistDelaySeconds()); whirlpoolWalletConfig.setDataPersisterFactory(dataPersisterFactory); whirlpoolWalletConfig.setPartner("SPARROW"); whirlpoolWalletConfig.setIndexRangePostmix(IndexRange.FULL); return whirlpoolWalletConfig; } - public Pool getPool(String poolId) { - try { - return getPools(null).stream().filter(pool -> pool.getPoolId().equals(poolId)).findFirst().orElse(null); - } catch(Exception e) { - log.error("Error retrieving pools", e); + private DataSourceConfig computeDataSourceConfig(Integer storedBlockHeight) { + return new DataSourceConfig(SparrowMinerFeeSupplier.getInstance(), new SparrowChainSupplier(storedBlockHeight), BIP_FORMAT.PROVIDER, BIP_WALLETS.WHIRLPOOL); + } + + private WhirlpoolInfo getWhirlpoolInfo() { + if(whirlpoolInfo == null) { + whirlpoolInfo = new WhirlpoolInfo(SparrowMinerFeeSupplier.getInstance(), config); } - return null; + return whirlpoolInfo; } public Collection getPools(Long totalUtxoValue) throws Exception { - this.poolSupplier.load(); + CoordinatorSupplier coordinatorSupplier = getWhirlpoolInfo().getCoordinatorSupplier(); + coordinatorSupplier.load(); if(totalUtxoValue == null) { - return poolSupplier.getPools(); + return coordinatorSupplier.getPools(); } - return tx0ParamService.findPools(poolSupplier.getPools(), totalUtxoValue); + return coordinatorSupplier.findPoolsForTx0(totalUtxoValue); } public Tx0Previews getTx0Previews(Collection utxos) throws Exception { + Tx0Info tx0Info = getTx0Info(); + // preview all pools - Tx0Config tx0Config = computeTx0Config(); - return tx0Service.tx0Previews(utxos, tx0Config); + Tx0Config tx0Config = computeTx0Config(tx0Info); + return tx0Info.tx0Previews(tx0Config, utxos); } public Tx0 broadcastTx0(Pool pool, Collection utxos) throws Exception { WhirlpoolWallet whirlpoolWallet = getWhirlpoolWallet(); - whirlpoolWallet.start(); + whirlpoolWallet.startAsync().subscribeOn(Schedulers.io()).observeOn(JavaFxScheduler.platform()); UtxoSupplier utxoSupplier = whirlpoolWallet.getUtxoSupplier(); List whirlpoolUtxos = utxos.stream().map(ref -> utxoSupplier.findUtxo(ref.getHashAsString(), (int)ref.getIndex())).filter(Objects::nonNull).collect(Collectors.toList()); @@ -148,15 +152,48 @@ public class Whirlpool { throw new IllegalStateException("Failed to find UTXOs in Whirlpool wallet"); } - Tx0Config tx0Config = computeTx0Config(); - return whirlpoolWallet.tx0(whirlpoolUtxos, pool, tx0Config); + Tx0Info tx0Info = getTx0Info(); + + WalletSupplier walletSupplier = whirlpoolWallet.getWalletSupplier(); + Tx0Config tx0Config = computeTx0Config(tx0Info); + Tx0 tx0 = tx0Info.tx0(walletSupplier, utxoSupplier, whirlpoolUtxos, pool, tx0Config); + + //Clear tx0 for new fee addresses + clearTx0Info(); + return tx0; } - private Tx0Config computeTx0Config() { - return new Tx0Config(tx0ParamService, poolSupplier, tx0FeeTarget, mixFeeTarget, WhirlpoolAccount.BADBANK); + private Tx0Info getTx0Info() throws Exception { + if(tx0Info == null) { + tx0Info = fetchTx0Info(); + } + + return tx0Info; + } + + private Tx0Info fetchTx0Info() throws Exception { + return AsyncUtil.getInstance().blockingGet( + Single.fromCallable(() -> getWhirlpoolInfo().fetchTx0Info(getScode())) + .subscribeOn(Schedulers.io()).observeOn(JavaFxScheduler.platform())); + } + + private void clearTx0Info() { + tx0Info = null; + } + + private Tx0Config computeTx0Config(Tx0Info tx0Info) { + Tx0Config tx0Config = tx0Info.getTx0Config(tx0FeeTarget, mixFeeTarget); + tx0Config.setChangeWallet(SamouraiAccount.BADBANK); + return tx0Config; } public void setHDWallet(String walletId, Wallet wallet) { + NetworkParameters params = config.getSamouraiNetwork().getParams(); + this.hdWallet = computeHdWallet(wallet, params); + this.walletId = walletId; + } + + public static HD_Wallet computeHdWallet(Wallet wallet, NetworkParameters params) { if(wallet.isEncrypted()) { throw new IllegalStateException("Wallet cannot be encrypted"); } @@ -169,8 +206,7 @@ public class Whirlpool { String passphrase = keystore.getSeed().getPassphrase() == null ? "" : keystore.getSeed().getPassphrase().asString(); HD_WalletFactoryGeneric hdWalletFactory = HD_WalletFactoryGeneric.getInstance(); byte[] seed = hdWalletFactory.computeSeedFromWords(words); - this.walletId = walletId; - hdWallet = new HD_Wallet(purpose, words, config.getNetworkParameters(), seed, passphrase); + return hdWalletFactory.getHD(purpose, seed, passphrase, params); } catch(Exception e) { throw new IllegalStateException("Could not create Whirlpool HD wallet ", e); } @@ -187,7 +223,7 @@ public class Whirlpool { try { WhirlpoolWallet whirlpoolWallet = new WhirlpoolWallet(config, Utils.hexToBytes(hdWallet.getSeedHex()), hdWallet.getPassphrase(), walletId); - return whirlpoolWalletService.openWallet(whirlpoolWallet); + return whirlpoolWalletService.openWallet(whirlpoolWallet, hdWallet.getPassphrase()); } catch(Exception e) { throw new WhirlpoolException("Could not create whirlpool wallet ", e); } @@ -199,18 +235,6 @@ public class Whirlpool { } } - public UtxoMixData getMixData(BlockTransactionHashIndex txo) { - if(whirlpoolWalletService.whirlpoolWallet() != null) { - WhirlpoolUtxo whirlpoolUtxo = whirlpoolWalletService.whirlpoolWallet().getUtxoSupplier().findUtxo(txo.getHashAsString(), (int)txo.getIndex()); - if (whirlpoolUtxo != null) { - UtxoConfig utxoConfig = whirlpoolUtxo.getUtxoConfigOrDefault(); - return new UtxoMixData(utxoConfig.getMixsDone(), null); - } - } - - return null; - } - public void mix(BlockTransactionHashIndex utxo) throws WhirlpoolException { if(whirlpoolWalletService.whirlpoolWallet() == null) { throw new WhirlpoolException("Whirlpool wallet not yet created"); @@ -266,7 +290,7 @@ public class Whirlpool { public void refreshUtxos() { if(whirlpoolWalletService.whirlpoolWallet() != null) { - whirlpoolWalletService.whirlpoolWallet().refreshUtxos(); + whirlpoolWalletService.whirlpoolWallet().refreshUtxosAsync().subscribeOn(Schedulers.io()).observeOn(JavaFxScheduler.platform()); } } @@ -321,7 +345,7 @@ public class Whirlpool { log.warn("Wallet is not started, but mixingProperty is true"); WhirlpoolEventService.getInstance().post(new WalletStopEvent(whirlpoolWalletService.whirlpoolWallet())); } else if(whirlpoolWalletService.whirlpoolWallet().getMixingState().getUtxosMixing().isEmpty() && - !whirlpoolWalletService.whirlpoolWallet().getUtxoSupplier().findUtxos(WhirlpoolAccount.PREMIX, WhirlpoolAccount.POSTMIX).isEmpty()) { + !whirlpoolWalletService.whirlpoolWallet().getUtxoSupplier().findUtxos(SamouraiAccount.PREMIX, SamouraiAccount.POSTMIX).isEmpty()) { log.warn("No UTXOs mixing, but mixingProperty is true"); //Will automatically restart AppServices.getWhirlpoolServices().stopWhirlpool(this, false); @@ -352,7 +376,6 @@ public class Whirlpool { public void shutdown() { whirlpoolWalletService.closeWallet(); - httpClientService.shutdown(); } public StartupService createStartupService() { @@ -389,7 +412,7 @@ public class Whirlpool { return AppServices.get().getOpenWallets().entrySet().stream().filter(entry -> entry.getValue().getWalletId(entry.getKey()).equals(walletId)).map(Map.Entry::getKey).findFirst().orElse(null); } - public static Wallet getStandardAccountWallet(WhirlpoolAccount whirlpoolAccount, Wallet wallet) { + public static Wallet getStandardAccountWallet(SamouraiAccount whirlpoolAccount, Wallet wallet) { StandardAccount standardAccount = getStandardAccount(whirlpoolAccount); if(StandardAccount.isWhirlpoolAccount(standardAccount) || wallet.getStandardAccountType() != standardAccount) { Wallet standardWallet = wallet.getChildWallet(standardAccount); @@ -403,12 +426,12 @@ public class Whirlpool { return wallet; } - public static StandardAccount getStandardAccount(WhirlpoolAccount whirlpoolAccount) { - if(whirlpoolAccount == WhirlpoolAccount.PREMIX) { + public static StandardAccount getStandardAccount(SamouraiAccount whirlpoolAccount) { + if(whirlpoolAccount == SamouraiAccount.PREMIX) { return StandardAccount.WHIRLPOOL_PREMIX; - } else if(whirlpoolAccount == WhirlpoolAccount.POSTMIX) { + } else if(whirlpoolAccount == SamouraiAccount.POSTMIX) { return StandardAccount.WHIRLPOOL_POSTMIX; - } else if(whirlpoolAccount == WhirlpoolAccount.BADBANK) { + } else if(whirlpoolAccount == SamouraiAccount.BADBANK) { return StandardAccount.WHIRLPOOL_BADBANK; } @@ -442,37 +465,21 @@ public class Whirlpool { throw new IllegalStateException("Cannot mix outputs from a wallet with multiple keystores"); } + SamouraiNetwork samouraiNetwork = AppServices.getWhirlpoolServices().getSamouraiNetwork(); + boolean testnet = FormatsUtilGeneric.getInstance().isTestNet(samouraiNetwork.getParams()); + UnspentOutput.Xpub xpub = new UnspentOutput.Xpub(); - List headers = ExtendedKey.Header.getHeaders(Network.get()); - ExtendedKey.Header header = headers.stream().filter(head -> head.getDefaultScriptType().equals(wallet.getScriptType()) && !head.isPrivateKey()).findFirst().orElse(ExtendedKey.Header.xpub); + ExtendedKey.Header header = testnet ? ExtendedKey.Header.tpub : ExtendedKey.Header.xpub; xpub.m = wallet.getKeystores().get(0).getExtendedPublicKey().toString(header); - xpub.path = node.getDerivationPath().toUpperCase(Locale.ROOT); + xpub.path = node.getWallet().isBip47() ? null : node.getDerivationPath().toUpperCase(Locale.ROOT); out.xpub = xpub; 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 void refreshTorCircuits() { - torClientService.changeIdentity(); + AppServices.getHttpClientService().changeIdentity(); } public String getScode() { @@ -530,16 +537,10 @@ public class Whirlpool { throw new IllegalStateException("Cannot find mix to wallet with id " + mixToWalletId); } - Integer highestUsedIndex = mixToWallet.getNode(KeyPurpose.RECEIVE).getHighestUsedIndex(); - int startIndex = highestUsedIndex == null ? 0 : highestUsedIndex + 1; int mixes = minMixes == null ? DEFAULT_MIXTO_MIN_MIXES : minMixes; - if(mixToWallet.getMixConfig() != null) { - startIndex = Math.max(startIndex, mixToWallet.getMixConfig().getReceiveIndex()); - } - - IPostmixHandler postmixHandler = new SparrowPostmixHandler(whirlpoolWalletService, mixToWallet, KeyPurpose.RECEIVE, startIndex); - ExternalDestination externalDestination = new ExternalDestination(postmixHandler, 0, startIndex, mixes, DEFAULT_MIXTO_RANDOM_FACTOR); + IPostmixHandler postmixHandler = new SparrowPostmixHandler(whirlpoolWalletService, mixToWallet, KeyPurpose.RECEIVE); + ExternalDestination externalDestination = new ExternalDestination(postmixHandler, 0, mixes, DEFAULT_MIXTO_RANDOM_FACTOR); config.setExternalDestination(externalDestination); } @@ -580,7 +581,8 @@ public class Whirlpool { @Subscribe public void onMixSuccess(MixSuccessEvent e) { - WalletUtxo walletUtxo = getUtxo(e.getWhirlpoolUtxo()); + WhirlpoolUtxo whirlpoolUtxo = e.getMixParams().getWhirlpoolUtxo(); + WalletUtxo walletUtxo = getUtxo(whirlpoolUtxo); if(walletUtxo != null) { log.debug("Mix success, new utxo " + e.getReceiveUtxo().getHash() + ":" + e.getReceiveUtxo().getIndex()); Platform.runLater(() -> EventManager.get().post(new WhirlpoolMixSuccessEvent(walletUtxo.wallet, walletUtxo.utxo, e.getReceiveUtxo(), getReceiveNode(e, walletUtxo)))); @@ -589,7 +591,7 @@ public class Whirlpool { private WalletNode getReceiveNode(MixSuccessEvent e, WalletUtxo walletUtxo) { for(WalletNode walletNode : walletUtxo.wallet.getNode(KeyPurpose.RECEIVE).getChildren()) { - if(walletNode.getAddress().toString().equals(e.getMixProgress().getDestination().getAddress())) { + if(walletNode.getAddress().toString().equals(e.getReceiveDestination().getAddress())) { return walletNode; } } @@ -599,19 +601,22 @@ public class Whirlpool { @Subscribe public void onMixFail(MixFailEvent e) { - WalletUtxo walletUtxo = getUtxo(e.getWhirlpoolUtxo()); + WhirlpoolUtxo whirlpoolUtxo = e.getMixParams().getWhirlpoolUtxo(); + WalletUtxo walletUtxo = getUtxo(whirlpoolUtxo); if(walletUtxo != null) { - log.debug("Mix failed for utxo " + e.getWhirlpoolUtxo().getUtxo().tx_hash + ":" + e.getWhirlpoolUtxo().getUtxo().tx_output_n + " " + e.getMixFailReason()); + log.debug("Mix failed for utxo " + whirlpoolUtxo.getUtxo().tx_hash + ":" + whirlpoolUtxo.getUtxo().tx_output_n + " " + e.getMixFailReason()); Platform.runLater(() -> EventManager.get().post(new WhirlpoolMixEvent(walletUtxo.wallet, walletUtxo.utxo, e.getMixFailReason(), e.getError()))); } } @Subscribe public void onMixProgress(MixProgressEvent e) { - WalletUtxo walletUtxo = getUtxo(e.getWhirlpoolUtxo()); + WhirlpoolUtxo whirlpoolUtxo = e.getMixParams().getWhirlpoolUtxo(); + MixProgress mixProgress = whirlpoolUtxo.getUtxoState().getMixProgress(); + WalletUtxo walletUtxo = getUtxo(whirlpoolUtxo); if(walletUtxo != null && isMixing()) { - log.debug("Mix progress for utxo " + e.getWhirlpoolUtxo().getUtxo().tx_hash + ":" + e.getWhirlpoolUtxo().getUtxo().tx_output_n + " " + e.getWhirlpoolUtxo().getMixsDone() + " " + e.getMixProgress().getMixStep() + " " + e.getWhirlpoolUtxo().getUtxoState().getStatus()); - Platform.runLater(() -> EventManager.get().post(new WhirlpoolMixEvent(walletUtxo.wallet, walletUtxo.utxo, e.getMixProgress()))); + log.debug("Mix progress for utxo " + whirlpoolUtxo.getUtxo().tx_hash + ":" + whirlpoolUtxo.getUtxo().tx_output_n + " " + whirlpoolUtxo.getMixsDone() + " " + mixProgress.getMixStep() + " " + whirlpoolUtxo.getUtxoState().getStatus()); + Platform.runLater(() -> EventManager.get().post(new WhirlpoolMixEvent(walletUtxo.wallet, walletUtxo.utxo, mixProgress))); } } @@ -624,7 +629,7 @@ public class Whirlpool { if(resyncMixesDone) { Wallet wallet = AppServices.get().getWallet(walletId); if(wallet != null) { - Wallet postmixWallet = getStandardAccountWallet(WhirlpoolAccount.POSTMIX, wallet); + Wallet postmixWallet = getStandardAccountWallet(SamouraiAccount.POSTMIX, wallet); resyncMixesDone(this, postmixWallet); resyncMixesDone = false; } @@ -709,7 +714,7 @@ public class Whirlpool { updateMessage("Broadcasting premix transaction..."); Tx0 tx0 = whirlpool.broadcastTx0(pool, utxos); - return Sha256Hash.wrap(tx0.getTxid()); + return Sha256Hash.wrap(tx0.getTx().getHashAsString()); } }; } @@ -733,7 +738,7 @@ public class Whirlpool { whirlpool.startingProperty.set(true); WhirlpoolWallet whirlpoolWallet = whirlpool.getWhirlpoolWallet(); if(AppServices.onlineProperty().get()) { - whirlpoolWallet.start(); + whirlpoolWallet.startAsync().subscribeOn(Schedulers.io()).observeOn(JavaFxScheduler.platform()).subscribe(); } return whirlpoolWallet; @@ -771,30 +776,6 @@ public class Whirlpool { } } - public static class RegisteredInputsService extends Service { - private final Whirlpool whirlpool; - private final String poolId; - - public RegisteredInputsService(Whirlpool whirlpool, String poolId) { - this.whirlpool = whirlpool; - this.poolId = poolId; - } - - @Override - protected Task createTask() { - return new Task<>() { - protected Integer call() { - Pool pool = whirlpool.getPool(poolId); - if(pool != null) { - return pool.getNbRegistered(); - } - - return null; - } - }; - } - } - public static class WalletUtxo { public final Wallet wallet; public final BlockTransactionHashIndex utxo; diff --git a/src/main/java/com/sparrowwallet/sparrow/whirlpool/WhirlpoolController.java b/src/main/java/com/sparrowwallet/sparrow/whirlpool/WhirlpoolController.java index 30c2bc42..64c17df6 100644 --- a/src/main/java/com/sparrowwallet/sparrow/whirlpool/WhirlpoolController.java +++ b/src/main/java/com/sparrowwallet/sparrow/whirlpool/WhirlpoolController.java @@ -182,7 +182,7 @@ public class WhirlpoolController { selectedPool.setVisible(false); } else { poolFee.setValue(newValue.getFeeValue()); - poolAnonset.setText(newValue.getMixAnonymitySet() + " UTXOs"); + poolAnonset.setText(newValue.getAnonymitySet() + " UTXOs"); selectedPool.setVisible(true); fetchTx0Preview(newValue); } @@ -340,7 +340,7 @@ public class WhirlpoolController { }); tx0PreviewsService.setOnSucceeded(workerStateEvent -> { tx0Previews = tx0PreviewsService.getValue(); - Tx0Preview tx0Preview = tx0Previews.getTx0Preview(pool.getPoolId()); + Tx0Preview tx0Preview = tx0Previews.getTx0Preview(this.pool.getValue() == null ? pool.getPoolId() : this.pool.getValue().getPoolId()); tx0PreviewProperty.set(tx0Preview); }); tx0PreviewsService.setOnFailed(workerStateEvent -> { diff --git a/src/main/java/com/sparrowwallet/sparrow/whirlpool/WhirlpoolServices.java b/src/main/java/com/sparrowwallet/sparrow/whirlpool/WhirlpoolServices.java index 571919c8..9bb4e43c 100644 --- a/src/main/java/com/sparrowwallet/sparrow/whirlpool/WhirlpoolServices.java +++ b/src/main/java/com/sparrowwallet/sparrow/whirlpool/WhirlpoolServices.java @@ -2,7 +2,12 @@ package com.sparrowwallet.sparrow.whirlpool; import com.google.common.eventbus.Subscribe; import com.google.common.net.HostAndPort; +import com.samourai.soroban.client.SorobanConfig; +import com.samourai.soroban.client.rpc.RpcClientService; +import com.samourai.wallet.constants.SamouraiNetwork; +import com.samourai.wallet.util.ExtLibJConfig; import com.samourai.whirlpool.client.wallet.WhirlpoolEventService; +import com.sparrowwallet.drongo.Drongo; import com.sparrowwallet.drongo.Network; import com.sparrowwallet.drongo.protocol.ScriptType; import com.sparrowwallet.drongo.wallet.DeterministicSeed; @@ -14,6 +19,7 @@ import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.WalletTabData; import com.sparrowwallet.sparrow.event.*; import com.sparrowwallet.sparrow.io.Storage; +import com.sparrowwallet.sparrow.net.HttpClientService; import com.sparrowwallet.sparrow.soroban.Soroban; import javafx.application.Platform; import javafx.scene.input.KeyCode; @@ -24,18 +30,40 @@ import javafx.util.Duration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.lang.reflect.Field; import java.net.SocketTimeoutException; import java.util.*; import java.util.concurrent.TimeoutException; import java.util.stream.Collectors; +import static com.sparrowwallet.sparrow.AppServices.getHttpClientService; import static com.sparrowwallet.sparrow.AppServices.getTorProxy; +import static org.bitcoinj.crypto.MnemonicCode.SPARROW_FIX_NFKD_MNEMONIC; public class WhirlpoolServices { private static final Logger log = LoggerFactory.getLogger(WhirlpoolServices.class); private final Map whirlpoolMap = new HashMap<>(); + private final SorobanConfig sorobanConfig; + + public WhirlpoolServices() { + ExtLibJConfig extLibJConfig = computeExtLibJConfig(); + this.sorobanConfig = new SorobanConfig(extLibJConfig); + System.setProperty(SPARROW_FIX_NFKD_MNEMONIC, "true"); + } + + private ExtLibJConfig computeExtLibJConfig() { + HttpClientService httpClientService = AppServices.getHttpClientService(); + boolean onion = (getTorProxy() != null); + SamouraiNetwork samouraiNetwork = getSamouraiNetwork(); + return new ExtLibJConfig(samouraiNetwork, onion, Drongo.getProvider(), httpClientService); + } + + public SamouraiNetwork getSamouraiNetwork() { + return SamouraiNetwork.valueOf(Network.get().getName().toUpperCase(Locale.ROOT)); + } + public Whirlpool getWhirlpool(Wallet wallet) { Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet(); for(Map.Entry entry : AppServices.get().getOpenWallets().entrySet()) { @@ -50,14 +78,9 @@ public class WhirlpoolServices { public Whirlpool getWhirlpool(String walletId) { Whirlpool whirlpool = whirlpoolMap.get(walletId); if(whirlpool == null) { - HostAndPort torProxy = getTorProxy(); - whirlpool = new Whirlpool(Network.get(), torProxy); + Wallet wallet = AppServices.get().getWallet(walletId); + whirlpool = new Whirlpool(wallet == null ? null : wallet.getStoredBlockHeight()); whirlpoolMap.put(walletId, whirlpool); - } else if(!whirlpool.isStarted()) { - HostAndPort torProxy = getTorProxy(); - if(!Objects.equals(whirlpool.getTorProxy(), torProxy)) { - whirlpool.setTorProxy(getTorProxy()); - } } return whirlpool; @@ -87,11 +110,6 @@ public class WhirlpoolServices { 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()); @@ -122,6 +140,7 @@ public class WhirlpoolServices { } if(exception instanceof TimeoutException || exception instanceof SocketTimeoutException) { EventManager.get().post(new StatusEvent("Error connecting to Whirlpool server, will retry soon...")); + HostAndPort torProxy = getTorProxy(); if(torProxy != null) { whirlpool.refreshTorCircuits(); } @@ -205,6 +224,10 @@ public class WhirlpoolServices { @Subscribe public void newConnection(ConnectionEvent event) { + ExtLibJConfig extLibJConfig = sorobanConfig.getExtLibJConfig(); + extLibJConfig.setOnion(getTorProxy() != null); + getHttpClientService(); //Ensure proxy is updated + startAllWhirlpool(); bindDebugAccelerator(); } @@ -300,4 +323,8 @@ public class WhirlpoolServices { }); } } + + public SorobanConfig getSorobanConfig() { + return sorobanConfig; + } } 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 7f6d942e..cb60a102 100644 --- a/src/main/java/com/sparrowwallet/sparrow/whirlpool/dataPersister/SparrowDataPersister.java +++ b/src/main/java/com/sparrowwallet/sparrow/whirlpool/dataPersister/SparrowDataPersister.java @@ -4,7 +4,7 @@ import com.samourai.wallet.util.AbstractOrchestrator; import com.samourai.whirlpool.client.wallet.WhirlpoolWallet; import com.samourai.whirlpool.client.wallet.WhirlpoolWalletConfig; import com.samourai.whirlpool.client.wallet.data.dataPersister.DataPersister; -import com.samourai.whirlpool.client.wallet.data.utxoConfig.UtxoConfigPersistedSupplier; +import com.samourai.whirlpool.client.wallet.data.utxoConfig.UtxoConfigPersistableSupplier; import com.samourai.whirlpool.client.wallet.data.utxoConfig.UtxoConfigSupplier; import com.samourai.whirlpool.client.wallet.data.walletState.WalletStateSupplier; import com.sparrowwallet.sparrow.whirlpool.dataSource.SparrowWalletStateSupplier; @@ -24,7 +24,7 @@ public class SparrowDataPersister implements DataPersister { WhirlpoolWalletConfig config = whirlpoolWallet.getConfig(); String walletIdentifier = whirlpoolWallet.getWalletIdentifier(); this.walletStateSupplier = new SparrowWalletStateSupplier(walletIdentifier, config); - this.utxoConfigSupplier = new UtxoConfigPersistedSupplier(new SparrowUtxoConfigPersister(walletIdentifier)); + this.utxoConfigSupplier = new UtxoConfigPersistableSupplier(new SparrowUtxoConfigPersister(walletIdentifier)); this.persistDelaySeconds = persistDelaySeconds; } diff --git a/src/main/java/com/sparrowwallet/sparrow/whirlpool/dataPersister/SparrowUtxoConfigPersister.java b/src/main/java/com/sparrowwallet/sparrow/whirlpool/dataPersister/SparrowUtxoConfigPersister.java index 0282e0f1..8458abfe 100644 --- a/src/main/java/com/sparrowwallet/sparrow/whirlpool/dataPersister/SparrowUtxoConfigPersister.java +++ b/src/main/java/com/sparrowwallet/sparrow/whirlpool/dataPersister/SparrowUtxoConfigPersister.java @@ -4,7 +4,7 @@ import com.google.common.collect.MapDifference; import com.google.common.collect.Maps; import com.samourai.whirlpool.client.wallet.data.utxoConfig.UtxoConfigData; import com.samourai.whirlpool.client.wallet.data.utxoConfig.UtxoConfigPersisted; -import com.samourai.whirlpool.client.wallet.data.utxoConfig.UtxoConfigPersister; +import com.samourai.whirlpool.client.wallet.data.utxoConfig.UtxoConfigPersisterFile; import com.sparrowwallet.drongo.protocol.Sha256Hash; import com.sparrowwallet.drongo.wallet.UtxoMixData; import com.sparrowwallet.drongo.wallet.Wallet; @@ -19,7 +19,7 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; -public class SparrowUtxoConfigPersister extends UtxoConfigPersister { +public class SparrowUtxoConfigPersister extends UtxoConfigPersisterFile { private static final Logger log = LoggerFactory.getLogger(SparrowUtxoConfigPersister.class); private final String walletId; diff --git a/src/main/java/com/sparrowwallet/sparrow/whirlpool/dataSource/SparrowChainSupplier.java b/src/main/java/com/sparrowwallet/sparrow/whirlpool/dataSource/SparrowChainSupplier.java new file mode 100644 index 00000000..93142b82 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/whirlpool/dataSource/SparrowChainSupplier.java @@ -0,0 +1,47 @@ +package com.sparrowwallet.sparrow.whirlpool.dataSource; + +import com.google.common.eventbus.Subscribe; +import com.samourai.wallet.api.backend.beans.WalletResponse; +import com.samourai.wallet.chain.ChainSupplier; +import com.sparrowwallet.drongo.Utils; +import com.sparrowwallet.drongo.protocol.Sha256Hash; +import com.sparrowwallet.sparrow.AppServices; +import com.sparrowwallet.sparrow.EventManager; +import com.sparrowwallet.sparrow.event.NewBlockEvent; + +public class SparrowChainSupplier implements ChainSupplier { + private final int storedBlockHeight; + private WalletResponse.InfoBlock latestBlock; + + public SparrowChainSupplier(Integer storedBlockHeight) { + this.storedBlockHeight = AppServices.getCurrentBlockHeight() == null ? (storedBlockHeight != null ? storedBlockHeight : 0) : AppServices.getCurrentBlockHeight(); + } + + public void open() { + this.latestBlock = computeLatestBlock(); + EventManager.get().register(this); + } + + public void close() { + EventManager.get().unregister(this); + } + + private WalletResponse.InfoBlock computeLatestBlock() { + WalletResponse.InfoBlock latestBlock = new WalletResponse.InfoBlock(); + latestBlock.height = AppServices.getCurrentBlockHeight() == null ? storedBlockHeight : AppServices.getCurrentBlockHeight(); + latestBlock.hash = AppServices.getLatestBlockHeader() == null ? Sha256Hash.ZERO_HASH.toString() : + Utils.bytesToHex(Sha256Hash.twiceOf(AppServices.getLatestBlockHeader().bitcoinSerialize()).getReversedBytes()); + latestBlock.time = AppServices.getLatestBlockHeader() == null ? 1 : AppServices.getLatestBlockHeader().getTime(); + return latestBlock; + } + + @Override + public WalletResponse.InfoBlock getLatestBlock() { + return latestBlock; + } + + @Subscribe + public void newBlock(NewBlockEvent event) { + this.latestBlock = computeLatestBlock(); + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/whirlpool/dataSource/SparrowDataSource.java b/src/main/java/com/sparrowwallet/sparrow/whirlpool/dataSource/SparrowDataSource.java index 6504ee17..cebf000e 100644 --- a/src/main/java/com/sparrowwallet/sparrow/whirlpool/dataSource/SparrowDataSource.java +++ b/src/main/java/com/sparrowwallet/sparrow/whirlpool/dataSource/SparrowDataSource.java @@ -1,235 +1,111 @@ package com.sparrowwallet.sparrow.whirlpool.dataSource; import com.google.common.eventbus.Subscribe; -import com.samourai.wallet.api.backend.MinerFeeTarget; -import com.samourai.wallet.api.backend.beans.UnspentOutput; -import com.samourai.wallet.api.backend.beans.WalletResponse; +import com.samourai.wallet.api.backend.IPushTx; +import com.samourai.wallet.api.backend.ISweepBackend; +import com.samourai.wallet.api.backend.seenBackend.ISeenBackend; +import com.samourai.wallet.api.backend.seenBackend.SeenBackendWithFallback; import com.samourai.wallet.hd.HD_Wallet; -import com.samourai.whirlpool.client.tx0.Tx0ParamService; +import com.samourai.wallet.httpClient.HttpUsage; +import com.samourai.wallet.httpClient.IHttpClient; +import com.samourai.wallet.util.ExtLibJConfig; import com.samourai.whirlpool.client.wallet.WhirlpoolWallet; -import com.samourai.whirlpool.client.wallet.beans.WhirlpoolUtxo; -import com.samourai.whirlpool.client.wallet.data.chain.ChainSupplier; -import com.samourai.whirlpool.client.wallet.data.dataPersister.DataPersister; -import com.samourai.whirlpool.client.wallet.data.dataSource.WalletResponseDataSource; -import com.samourai.whirlpool.client.wallet.data.minerFee.MinerFeeSupplier; -import com.samourai.whirlpool.client.wallet.data.pool.PoolSupplier; -import com.samourai.whirlpool.client.wallet.data.utxo.BasicUtxoSupplier; -import com.samourai.whirlpool.client.wallet.data.utxo.UtxoData; +import com.samourai.whirlpool.client.wallet.WhirlpoolWalletConfig; +import com.samourai.whirlpool.client.wallet.data.coordinator.CoordinatorSupplier; +import com.samourai.whirlpool.client.wallet.data.dataSource.AbstractDataSource; +import com.samourai.whirlpool.client.wallet.data.dataSource.DataSourceConfig; +import com.samourai.whirlpool.client.wallet.data.utxo.UtxoSupplier; import com.samourai.whirlpool.client.wallet.data.utxoConfig.UtxoConfigSupplier; -import com.samourai.whirlpool.client.wallet.data.wallet.WalletSupplier; +import com.samourai.whirlpool.client.wallet.data.walletState.WalletStateSupplier; import com.sparrowwallet.drongo.ExtendedKey; -import com.sparrowwallet.drongo.KeyPurpose; import com.sparrowwallet.drongo.Network; import com.sparrowwallet.drongo.Utils; -import com.sparrowwallet.drongo.protocol.Sha256Hash; import com.sparrowwallet.drongo.protocol.Transaction; -import com.sparrowwallet.drongo.protocol.TransactionInput; -import com.sparrowwallet.drongo.protocol.TransactionOutput; -import com.sparrowwallet.drongo.wallet.*; +import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.EventManager; -import com.sparrowwallet.sparrow.event.NewBlockEvent; import com.sparrowwallet.sparrow.event.WalletAddressesChangedEvent; import com.sparrowwallet.sparrow.event.WalletHistoryChangedEvent; import com.sparrowwallet.sparrow.net.ElectrumServer; import com.sparrowwallet.sparrow.whirlpool.Whirlpool; import javafx.application.Platform; +import org.bitcoinj.core.NetworkParameters; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.*; +import java.util.Collection; +import java.util.List; -public class SparrowDataSource extends WalletResponseDataSource { +public class SparrowDataSource extends AbstractDataSource { private static final Logger log = LoggerFactory.getLogger(SparrowDataSource.class); - private final String walletIdentifierPrefix; + private final ISeenBackend seenBackend; + private final IPushTx pushTx; + private final SparrowUtxoSupplier utxoSupplier; public SparrowDataSource( WhirlpoolWallet whirlpoolWallet, HD_Wallet bip44w, - DataPersister dataPersister) + WalletStateSupplier walletStateSupplier, + UtxoConfigSupplier utxoConfigSupplier, + DataSourceConfig dataSourceConfig) throws Exception { - super(whirlpoolWallet, bip44w, dataPersister); - - // prefix matching :master, :Premix, :Postmix - this.walletIdentifierPrefix = getWhirlpoolWallet().getWalletIdentifier().replace(":master", ""); + super(whirlpoolWallet, bip44w, walletStateSupplier, dataSourceConfig); + this.seenBackend = computeSeenBackend(whirlpoolWallet.getConfig()); + this.pushTx = computePushTx(); + NetworkParameters params = whirlpoolWallet.getConfig().getSamouraiNetwork().getParams(); + this.utxoSupplier = new SparrowUtxoSupplier(walletSupplier, utxoConfigSupplier, dataSourceConfig, params); } - @Override - public void open() throws Exception { - super.open(); - EventManager.get().register(this); + private ISeenBackend computeSeenBackend(WhirlpoolWalletConfig whirlpoolWalletConfig) { + ExtLibJConfig extLibJConfig = whirlpoolWalletConfig.getSorobanConfig().getExtLibJConfig(); + IHttpClient httpClient = extLibJConfig.getHttpClientService().getHttpClient(HttpUsage.BACKEND); + ISeenBackend sparrowSeenBackend = new SparrowSeenBackend(getWhirlpoolWallet().getWalletIdentifier(), httpClient); + NetworkParameters params = whirlpoolWalletConfig.getSamouraiNetwork().getParams(); + return SeenBackendWithFallback.withOxt(sparrowSeenBackend, params); } - @Override - public void close() throws Exception { - EventManager.get().unregister(this); - super.close(); - } - - @Override - protected WalletResponse fetchWalletResponse() throws Exception { - WalletResponse walletResponse = new WalletResponse(); - walletResponse.wallet = new WalletResponse.Wallet(); - - Map allTransactions = new HashMap<>(); - Map allTransactionsZpubs = new HashMap<>(); - List addresses = new ArrayList<>(); - List txes = new ArrayList<>(); - List unspentOutputs = new ArrayList<>(); - int storedBlockHeight = 0; - - String[] zpubs = getWalletSupplier().getPubs(true); - for(String zpub : zpubs) { - Wallet wallet = getWallet(zpub); - if(wallet == null) { - log.debug("No wallet for " + zpub + " found"); - continue; - } - - Map walletTransactions = wallet.getWalletTransactions(); - allTransactions.putAll(walletTransactions); - walletTransactions.keySet().forEach(txid -> allTransactionsZpubs.put(txid, zpub)); - if(wallet.getStoredBlockHeight() != null) { - storedBlockHeight = Math.max(storedBlockHeight, wallet.getStoredBlockHeight()); - } - - WalletResponse.Address address = new WalletResponse.Address(); - List headers = ExtendedKey.Header.getHeaders(Network.get()); - ExtendedKey.Header header = headers.stream().filter(head -> head.getDefaultScriptType().equals(wallet.getScriptType()) && !head.isPrivateKey()).findFirst().orElse(ExtendedKey.Header.xpub); - address.address = wallet.getKeystores().get(0).getExtendedPublicKey().toString(header); - int receiveIndex = wallet.getNode(KeyPurpose.RECEIVE).getHighestUsedIndex() == null ? 0 : wallet.getNode(KeyPurpose.RECEIVE).getHighestUsedIndex() + 1; - address.account_index = wallet.getMixConfig() != null ? Math.max(receiveIndex, wallet.getMixConfig().getReceiveIndex()) : receiveIndex; - int changeIndex = wallet.getNode(KeyPurpose.CHANGE).getHighestUsedIndex() == null ? 0 : wallet.getNode(KeyPurpose.CHANGE).getHighestUsedIndex() + 1; - address.change_index = wallet.getMixConfig() != null ? Math.max(changeIndex, wallet.getMixConfig().getChangeIndex()) : changeIndex; - address.n_tx = walletTransactions.size(); - addresses.add(address); - - for(Map.Entry utxo : wallet.getSpendableUtxos().entrySet()) { - BlockTransaction blockTransaction = wallet.getWalletTransaction(utxo.getKey().getHash()); - if(blockTransaction != null) { - unspentOutputs.add(Whirlpool.getUnspentOutput(utxo.getValue(), blockTransaction, (int)utxo.getKey().getIndex())); - } - } - } - - for(BlockTransaction blockTransaction : allTransactions.values()) { - WalletResponse.Tx tx = new WalletResponse.Tx(); - tx.block_height = blockTransaction.getHeight(); - tx.hash = blockTransaction.getHashAsString(); - tx.locktime = blockTransaction.getTransaction().getLocktime(); - tx.version = (int)blockTransaction.getTransaction().getVersion(); - - tx.inputs = new WalletResponse.TxInput[blockTransaction.getTransaction().getInputs().size()]; - for(int i = 0; i < blockTransaction.getTransaction().getInputs().size(); i++) { - TransactionInput txInput = blockTransaction.getTransaction().getInputs().get(i); - tx.inputs[i] = new WalletResponse.TxInput(); - tx.inputs[i].vin = txInput.getIndex(); - tx.inputs[i].sequence = txInput.getSequenceNumber(); - if(allTransactionsZpubs.containsKey(txInput.getOutpoint().getHash())) { - tx.inputs[i].prev_out = new WalletResponse.TxOut(); - tx.inputs[i].prev_out.txid = txInput.getOutpoint().getHash().toString(); - tx.inputs[i].prev_out.vout = (int)txInput.getOutpoint().getIndex(); - - BlockTransaction spentTransaction = allTransactions.get(txInput.getOutpoint().getHash()); - if(spentTransaction != null) { - TransactionOutput spentOutput = spentTransaction.getTransaction().getOutputs().get((int)txInput.getOutpoint().getIndex()); - tx.inputs[i].prev_out.value = spentOutput.getValue(); - } - - tx.inputs[i].prev_out.xpub = new UnspentOutput.Xpub(); - tx.inputs[i].prev_out.xpub.m = allTransactionsZpubs.get(txInput.getOutpoint().getHash()); - } - } - - tx.out = new WalletResponse.TxOutput[blockTransaction.getTransaction().getOutputs().size()]; - for(int i = 0; i < blockTransaction.getTransaction().getOutputs().size(); i++) { - TransactionOutput txOutput = blockTransaction.getTransaction().getOutputs().get(i); - tx.out[i] = new WalletResponse.TxOutput(); - tx.out[i].n = txOutput.getIndex(); - tx.out[i].value = txOutput.getValue(); - tx.out[i].xpub = new UnspentOutput.Xpub(); - tx.out[i].xpub.m = allTransactionsZpubs.get(blockTransaction.getHash()); - } - - txes.add(tx); - } - - walletResponse.addresses = addresses.toArray(new WalletResponse.Address[0]); - walletResponse.txs = txes.toArray(new WalletResponse.Tx[0]); - walletResponse.unspent_outputs = unspentOutputs.toArray(new UnspentOutput[0]); - - walletResponse.info = new WalletResponse.Info(); - walletResponse.info.latest_block = new WalletResponse.InfoBlock(); - walletResponse.info.latest_block.height = AppServices.getCurrentBlockHeight() == null ? storedBlockHeight : AppServices.getCurrentBlockHeight(); - walletResponse.info.latest_block.hash = Sha256Hash.ZERO_HASH.toString(); - walletResponse.info.latest_block.time = AppServices.getLatestBlockHeader() == null ? 1 : AppServices.getLatestBlockHeader().getTime(); - - walletResponse.info.fees = new LinkedHashMap<>(); - for(MinerFeeTarget target : MinerFeeTarget.values()) { - walletResponse.info.fees.put(target.getValue(), getMinerFeeSupplier().getFee(target)); - } - - return walletResponse; - } - - @Override - protected BasicUtxoSupplier computeUtxoSupplier(WhirlpoolWallet whirlpoolWallet, WalletSupplier walletSupplier, UtxoConfigSupplier utxoConfigSupplier, ChainSupplier chainSupplier, PoolSupplier poolSupplier, Tx0ParamService tx0ParamService) throws Exception { - return new BasicUtxoSupplier( - walletSupplier, - utxoConfigSupplier, - chainSupplier, - poolSupplier, - tx0ParamService) { + private IPushTx computePushTx() { + return new IPushTx() { @Override - public void refresh() throws Exception { - SparrowDataSource.this.refresh(); + public String pushTx(String hexTx) throws Exception { + Transaction transaction = new Transaction(Utils.hexToBytes(hexTx)); + ElectrumServer electrumServer = new ElectrumServer(); + return electrumServer.broadcastTransactionPrivately(transaction).toString(); } @Override - protected void onUtxoChanges(UtxoData utxoData) { - super.onUtxoChanges(utxoData); - whirlpoolWallet.onUtxoChanges(utxoData); - } - - @Override - protected byte[] _getPrivKeyBytes(WhirlpoolUtxo whirlpoolUtxo) { - UnspentOutput utxo = whirlpoolUtxo.getUtxo(); - Wallet wallet = getWallet(utxo.xpub.m); - Map walletUtxos = wallet.getWalletUtxos(); - WalletNode node = walletUtxos.entrySet().stream() - .filter(entry -> entry.getKey().getHash().equals(Sha256Hash.wrap(utxo.tx_hash)) && entry.getKey().getIndex() == utxo.tx_output_n) - .map(Map.Entry::getValue) - .findFirst() - .orElseThrow(() -> new IllegalStateException("Cannot find UTXO " + utxo)); - - if(node.getWallet().isBip47()) { - try { - Keystore keystore = node.getWallet().getKeystores().get(0); - return keystore.getKey(node).getPrivKeyBytes(); - } catch(Exception e) { - log.error("Error getting private key", e); - } - } - - return null; + public String pushTx(String txHex, Collection strictModeVouts) throws Exception { + return pushTx(txHex); } }; } @Override - public void pushTx(String txHex) throws Exception { - Transaction transaction = new Transaction(Utils.hexToBytes(txHex)); - ElectrumServer electrumServer = new ElectrumServer(); - electrumServer.broadcastTransactionPrivately(transaction); + public void open(CoordinatorSupplier coordinatorSupplier) throws Exception { + super.open(coordinatorSupplier); + EventManager.get().register(this); + ((SparrowChainSupplier)getDataSourceConfig().getChainSupplier()).open(); } @Override - public MinerFeeSupplier getMinerFeeSupplier() { - return SparrowMinerFeeSupplier.getInstance(); + protected void load(boolean initial) throws Exception { + super.load(initial); + utxoSupplier.refresh(); } - static Wallet getWallet(String zpub) { + @Override + public void close() throws Exception { + EventManager.get().unregister(this); + ((SparrowChainSupplier)getDataSourceConfig().getChainSupplier()).close(); + } + + @Override + public IPushTx getPushTx() { + return pushTx; + } + + public static Wallet getWallet(String zpub) { return AppServices.get().getOpenWallets().keySet().stream() .filter(wallet -> { try { @@ -255,17 +131,10 @@ public class SparrowDataSource extends WalletResponseDataSource { refreshWallet(event.getWalletId(), event.getWallet(), 0); } - @Subscribe - public void newBlock(NewBlockEvent event) { - try { - refresh(); - } catch (Exception e) { - log.error("", e); - } - } - private void refreshWallet(String walletId, Wallet wallet, int i) { try { + // prefix matching :master, :Premix, :Postmix + String walletIdentifierPrefix = getWhirlpoolWallet().getWalletIdentifier().replace(":master", ""); // match :master, :Premix, :Postmix if(walletId.startsWith(walletIdentifierPrefix) && (wallet.isWhirlpoolMasterWallet() || wallet.isWhirlpoolChildWallet())) { //Workaround to avoid refreshing the wallet after it has been opened, but before it has been started @@ -273,11 +142,26 @@ public class SparrowDataSource extends WalletResponseDataSource { if(whirlpool != null && whirlpool.isStarting() && i < 1000) { Platform.runLater(() -> refreshWallet(walletId, wallet, i+1)); } else { - refresh(); + utxoSupplier.refresh(); } } } catch (Exception e) { log.error("Error refreshing wallet", e); } } + + @Override + public ISweepBackend getSweepBackend() { + return null; // not necessary + } + + @Override + public ISeenBackend getSeenBackend() { + return seenBackend; + } + + @Override + public UtxoSupplier getUtxoSupplier() { + return utxoSupplier; + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/whirlpool/dataSource/SparrowPostmixHandler.java b/src/main/java/com/sparrowwallet/sparrow/whirlpool/dataSource/SparrowPostmixHandler.java index 4ae8f021..9f186397 100644 --- a/src/main/java/com/sparrowwallet/sparrow/whirlpool/dataSource/SparrowPostmixHandler.java +++ b/src/main/java/com/sparrowwallet/sparrow/whirlpool/dataSource/SparrowPostmixHandler.java @@ -1,11 +1,12 @@ package com.sparrowwallet.sparrow.whirlpool.dataSource; import com.samourai.wallet.client.indexHandler.IIndexHandler; -import com.samourai.wallet.util.XPubUtil; import com.samourai.whirlpool.client.mix.handler.DestinationType; import com.samourai.whirlpool.client.mix.handler.IPostmixHandler; import com.samourai.whirlpool.client.mix.handler.MixDestination; +import com.samourai.whirlpool.client.utils.ClientUtils; import com.samourai.whirlpool.client.wallet.WhirlpoolWalletService; +import com.samourai.whirlpool.client.wallet.beans.IndexRange; import com.sparrowwallet.drongo.KeyPurpose; import com.sparrowwallet.drongo.address.Address; import com.sparrowwallet.drongo.wallet.Wallet; @@ -19,59 +20,67 @@ public class SparrowPostmixHandler implements IPostmixHandler { private final WhirlpoolWalletService whirlpoolWalletService; private final Wallet wallet; private final KeyPurpose keyPurpose; - private final int startIndex; protected MixDestination destination; - public SparrowPostmixHandler(WhirlpoolWalletService whirlpoolWalletService, Wallet wallet, KeyPurpose keyPurpose, int startIndex) { + public SparrowPostmixHandler(WhirlpoolWalletService whirlpoolWalletService, Wallet wallet, KeyPurpose keyPurpose) { this.whirlpoolWalletService = whirlpoolWalletService; this.wallet = wallet; this.keyPurpose = keyPurpose; - this.startIndex = startIndex; + } + + protected IndexRange getIndexRange() { + return IndexRange.FULL; } public Wallet getWallet() { return wallet; } - protected MixDestination computeNextDestination() throws Exception { - // index - int index = Math.max(getIndexHandler().getAndIncrementUnconfirmed(), startIndex); + @Override + public final MixDestination computeDestinationNext() throws Exception { + // use "unconfirmed" index to avoid huge index gaps on multiple mix failures + int index = ClientUtils.computeNextReceiveAddressIndex(getIndexHandler(), getIndexRange()); + this.destination = computeDestination(index); + if (log.isDebugEnabled()) { + log.debug( + "Mixing to " + + destination.getType() + + " -> receiveAddress=" + + destination.getAddress() + + ", path=" + + destination.getPath()); + } + return destination; + } + @Override + public MixDestination computeDestination(int index) throws Exception { // address WalletNode node = new WalletNode(wallet, keyPurpose, index); Address address = node.getAddress(); - String path = XPubUtil.getInstance().getPath(index, keyPurpose.getPathIndex().num()); + String path = "xpub/" + keyPurpose.getPathIndex().num() + "/" + index; log.info("Mixing to external xPub -> receiveAddress=" + address + ", path=" + path); return new MixDestination(DestinationType.XPUB, index, address.toString(), path); } - @Override - public MixDestination getDestination() { - return destination; // may be NULL - } - - public final MixDestination computeDestination() throws Exception { - // use "unconfirmed" index to avoid huge index gaps on multiple mix failures - this.destination = computeNextDestination(); - return destination; - } - @Override public void onMixFail() { if(destination != null) { + // cancel unconfirmed postmix index if output was not registered yet getIndexHandler().cancelUnconfirmed(destination.getIndex()); } } @Override public void onRegisterOutput() { - // confirm receive address even when REGISTER_OUTPUT fails, to avoid 'ouput already registered' + // confirm postmix index on REGISTER_OUTPUT success getIndexHandler().confirmUnconfirmed(destination.getIndex()); } - private IIndexHandler getIndexHandler() { + @Override + public IIndexHandler getIndexHandler() { return whirlpoolWalletService.whirlpoolWallet().getWalletStateSupplier().getIndexHandlerExternal(); } } diff --git a/src/main/java/com/sparrowwallet/sparrow/whirlpool/dataSource/SparrowSeenBackend.java b/src/main/java/com/sparrowwallet/sparrow/whirlpool/dataSource/SparrowSeenBackend.java new file mode 100644 index 00000000..03c65721 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/whirlpool/dataSource/SparrowSeenBackend.java @@ -0,0 +1,57 @@ +package com.sparrowwallet.sparrow.whirlpool.dataSource; + +import com.samourai.wallet.api.backend.seenBackend.ISeenBackend; +import com.samourai.wallet.api.backend.seenBackend.SeenResponse; +import com.samourai.wallet.httpClient.IHttpClient; +import com.sparrowwallet.drongo.address.Address; +import com.sparrowwallet.drongo.wallet.Wallet; +import com.sparrowwallet.drongo.wallet.WalletNode; +import com.sparrowwallet.sparrow.AppServices; + +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class SparrowSeenBackend implements ISeenBackend { + private final String walletId; + private final IHttpClient httpClient; + + public SparrowSeenBackend(String walletId, IHttpClient httpClient) { + this.walletId = walletId; + this.httpClient = httpClient; + } + + @Override + public SeenResponse seen(Collection addresses) throws Exception { + Wallet wallet = AppServices.get().getWallet(walletId); + Map addressMap = wallet.getWalletAddresses(); + for(Wallet childWallet : wallet.getChildWallets()) { + if(!childWallet.isNested()) { + addressMap.putAll(childWallet.getWalletAddresses()); + } + } + + Map map = new LinkedHashMap<>(); + for(String address : addresses) { + WalletNode walletNode = addressMap.get(Address.fromString(address)); + if(walletNode != null) { + int highestUsedIndex = walletNode.getWallet().getNode(walletNode.getKeyPurpose()).getHighestUsedIndex(); + map.put(address, walletNode.getIndex() <= highestUsedIndex); + } + } + + return new SeenResponse(map); + } + + @Override + public boolean seen(String address) throws Exception { + SeenResponse seenResponse = seen(List.of(address)); + return seenResponse.isSeen(address); + } + + @Override + public IHttpClient getHttpClient() { + return httpClient; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/whirlpool/dataSource/SparrowUtxoSupplier.java b/src/main/java/com/sparrowwallet/sparrow/whirlpool/dataSource/SparrowUtxoSupplier.java new file mode 100644 index 00000000..f4e1aa86 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/whirlpool/dataSource/SparrowUtxoSupplier.java @@ -0,0 +1,151 @@ +package com.sparrowwallet.sparrow.whirlpool.dataSource; + +import com.samourai.wallet.api.backend.beans.UnspentOutput; +import com.samourai.wallet.api.backend.beans.WalletResponse; +import com.samourai.wallet.bipWallet.BipWallet; +import com.samourai.wallet.bipWallet.WalletSupplier; +import com.samourai.whirlpool.client.wallet.WhirlpoolWallet; +import com.samourai.whirlpool.client.wallet.beans.WhirlpoolUtxo; +import com.samourai.whirlpool.client.wallet.data.dataSource.DataSourceConfig; +import com.samourai.whirlpool.client.wallet.data.utxo.BasicUtxoSupplier; +import com.samourai.whirlpool.client.wallet.data.utxo.UtxoData; +import com.samourai.whirlpool.client.wallet.data.utxoConfig.UtxoConfigSupplier; +import com.sparrowwallet.drongo.KeyPurpose; +import com.sparrowwallet.drongo.protocol.Sha256Hash; +import com.sparrowwallet.drongo.protocol.TransactionInput; +import com.sparrowwallet.drongo.protocol.TransactionOutput; +import com.sparrowwallet.drongo.wallet.*; +import com.sparrowwallet.sparrow.whirlpool.Whirlpool; +import org.bitcoinj.core.NetworkParameters; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; + +// manages utxos & wallet indexes +public class SparrowUtxoSupplier extends BasicUtxoSupplier { + private static final Logger log = LoggerFactory.getLogger(SparrowUtxoSupplier.class); + + public SparrowUtxoSupplier( + WalletSupplier walletSupplier, + UtxoConfigSupplier utxoConfigSupplier, + DataSourceConfig dataSourceConfig, + NetworkParameters params) { + super(walletSupplier, utxoConfigSupplier, dataSourceConfig, params); + } + + @Override + public void refresh() throws Exception { + Map allTransactions = new HashMap<>(); + Map allTransactionsXpubs = new HashMap<>(); + List txes = new ArrayList<>(); + List unspentOutputs = new ArrayList<>(); + int storedBlockHeight = 0; + + Collection bipWallets = getWalletSupplier().getWallets(); + for(BipWallet bipWallet : bipWallets) { + String zpub = bipWallet.getBipPub(); + Wallet wallet = SparrowDataSource.getWallet(zpub); + if(wallet == null) { + log.debug("No wallet for " + zpub + " found"); + continue; + } + + Map walletTransactions = wallet.getWalletTransactions(); + allTransactions.putAll(walletTransactions); + String xpub = bipWallet.getXPub(); + walletTransactions.keySet().forEach(txid -> allTransactionsXpubs.put(txid, xpub)); + if(wallet.getStoredBlockHeight() != null) { + storedBlockHeight = Math.max(storedBlockHeight, wallet.getStoredBlockHeight()); + } + + // update wallet index: receive + int receiveIndex = wallet.getNode(KeyPurpose.RECEIVE).getHighestUsedIndex() == null ? 0 : wallet.getNode(KeyPurpose.RECEIVE).getHighestUsedIndex() + 1; + int account_index = wallet.getMixConfig() != null ? Math.max(receiveIndex, wallet.getMixConfig().getReceiveIndex()) : receiveIndex; + bipWallet.getIndexHandlerReceive().set(account_index, false); + + // update wallet index: change + int changeIndex = wallet.getNode(KeyPurpose.CHANGE).getHighestUsedIndex() == null ? 0 : wallet.getNode(KeyPurpose.CHANGE).getHighestUsedIndex() + 1; + int change_index = wallet.getMixConfig() != null ? Math.max(changeIndex, wallet.getMixConfig().getChangeIndex()) : changeIndex; + bipWallet.getIndexHandlerChange().set(change_index, false); + + for(Map.Entry utxo : wallet.getSpendableUtxos().entrySet()) { + BlockTransaction blockTransaction = wallet.getWalletTransaction(utxo.getKey().getHash()); + if(blockTransaction != null) { + unspentOutputs.add(Whirlpool.getUnspentOutput(utxo.getValue(), blockTransaction, (int)utxo.getKey().getIndex())); + } + } + } + + for(BlockTransaction blockTransaction : allTransactions.values()) { + WalletResponse.Tx tx = new WalletResponse.Tx(); + tx.block_height = blockTransaction.getHeight(); + tx.hash = blockTransaction.getHashAsString(); + tx.locktime = blockTransaction.getTransaction().getLocktime(); + tx.version = (int)blockTransaction.getTransaction().getVersion(); + + tx.inputs = new WalletResponse.TxInput[blockTransaction.getTransaction().getInputs().size()]; + for(int i = 0; i < blockTransaction.getTransaction().getInputs().size(); i++) { + TransactionInput txInput = blockTransaction.getTransaction().getInputs().get(i); + tx.inputs[i] = new WalletResponse.TxInput(); + tx.inputs[i].vin = txInput.getIndex(); + tx.inputs[i].sequence = txInput.getSequenceNumber(); + if(allTransactionsXpubs.containsKey(txInput.getOutpoint().getHash())) { + tx.inputs[i].prev_out = new WalletResponse.TxOut(); + tx.inputs[i].prev_out.txid = txInput.getOutpoint().getHash().toString(); + tx.inputs[i].prev_out.vout = (int)txInput.getOutpoint().getIndex(); + + BlockTransaction spentTransaction = allTransactions.get(txInput.getOutpoint().getHash()); + if(spentTransaction != null) { + TransactionOutput spentOutput = spentTransaction.getTransaction().getOutputs().get((int)txInput.getOutpoint().getIndex()); + tx.inputs[i].prev_out.value = spentOutput.getValue(); + } + + tx.inputs[i].prev_out.xpub = new UnspentOutput.Xpub(); + tx.inputs[i].prev_out.xpub.m = allTransactionsXpubs.get(txInput.getOutpoint().getHash()); + } + } + + tx.out = new WalletResponse.TxOutput[blockTransaction.getTransaction().getOutputs().size()]; + for(int i = 0; i < blockTransaction.getTransaction().getOutputs().size(); i++) { + TransactionOutput txOutput = blockTransaction.getTransaction().getOutputs().get(i); + tx.out[i] = new WalletResponse.TxOutput(); + tx.out[i].n = txOutput.getIndex(); + tx.out[i].value = txOutput.getValue(); + tx.out[i].xpub = new UnspentOutput.Xpub(); + tx.out[i].xpub.m = allTransactionsXpubs.get(blockTransaction.getHash()); + } + + txes.add(tx); + } + + // update utxos + UnspentOutput[] uos = unspentOutputs.toArray(new UnspentOutput[0]); + WalletResponse.Tx[] txs = txes.toArray(new WalletResponse.Tx[0]); + UtxoData utxoData = new UtxoData(uos, txs, storedBlockHeight); + setValue(utxoData); + } + + @Override + public byte[] _getPrivKeyBip47(UnspentOutput utxo) throws Exception { + BipWallet bipWallet = getWalletSupplier().getWalletByXPub(utxo.xpub.m); + Wallet wallet = SparrowDataSource.getWallet(bipWallet.getBipPub()); + Map walletUtxos = wallet.getWalletUtxos(); + WalletNode node = walletUtxos.entrySet().stream() + .filter(entry -> entry.getKey().getHash().equals(Sha256Hash.wrap(utxo.tx_hash)) && entry.getKey().getIndex() == utxo.tx_output_n) + .map(Map.Entry::getValue) + .findFirst() + .orElseThrow(() -> new IllegalStateException("Cannot find UTXO " + utxo)); + + if(node.getWallet().isBip47()) { + try { + Keystore keystore = node.getWallet().getKeystores().get(0); + return keystore.getKey(node).getPrivKeyBytes(); + } catch(Exception e) { + log.error("Error getting private key", e); + } + } + + return null; + } +} 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 13778a49..878739e8 100644 --- a/src/main/java/com/sparrowwallet/sparrow/whirlpool/dataSource/SparrowWalletStateSupplier.java +++ b/src/main/java/com/sparrowwallet/sparrow/whirlpool/dataSource/SparrowWalletStateSupplier.java @@ -1,10 +1,11 @@ package com.sparrowwallet.sparrow.whirlpool.dataSource; +import com.samourai.wallet.bipWallet.BipDerivation; +import com.samourai.wallet.bipWallet.BipWallet; import com.samourai.wallet.client.indexHandler.IIndexHandler; -import com.samourai.wallet.hd.AddressType; +import com.samourai.wallet.constants.SamouraiAccount; 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; @@ -30,11 +31,12 @@ public class SparrowWalletStateSupplier implements WalletStateSupplier { } @Override - public IIndexHandler getIndexHandlerWallet(WhirlpoolAccount whirlpoolAccount, AddressType addressType, Chain chain) { - String key = mapKey(whirlpoolAccount, addressType, chain); + public IIndexHandler getIndexHandlerWallet(BipWallet bipWallet, Chain chain) { + SamouraiAccount samouraiAccount = bipWallet.getAccount(); + String key = mapKey(bipWallet, chain); IIndexHandler indexHandler = indexHandlerWallets.get(key); - if (indexHandler == null) { - Wallet wallet = findWallet(whirlpoolAccount); + if(indexHandler == null) { + Wallet wallet = findWallet(samouraiAccount); KeyPurpose keyPurpose = (chain == Chain.RECEIVE ? KeyPurpose.RECEIVE : KeyPurpose.CHANGE); WalletNode walletNode = wallet.getNode(keyPurpose); @@ -61,7 +63,8 @@ public class SparrowWalletStateSupplier implements WalletStateSupplier { if(externalIndexHandler == null) { Wallet externalWallet = null; - if(externalDestination.getPostmixHandler() instanceof SparrowPostmixHandler sparrowPostmixHandler) { + if(externalDestination.getPostmixHandlerCustom() != null + && externalDestination.getPostmixHandlerCustom() instanceof SparrowPostmixHandler sparrowPostmixHandler) { externalWallet = sparrowPostmixHandler.getWallet(); } else if(externalDestination.getXpub() != null) { externalWallet = SparrowDataSource.getWallet(externalDestination.getXpub()); @@ -80,7 +83,7 @@ public class SparrowWalletStateSupplier implements WalletStateSupplier { KeyPurpose keyPurpose = KeyPurpose.fromChildNumber(new ChildNumber(externalDestination.getChain())); WalletNode externalNode = externalWallet.getNode(keyPurpose); - externalIndexHandler = new SparrowIndexHandler(externalWallet, externalNode, externalDestination.getStartIndex()); + externalIndexHandler = new SparrowIndexHandler(externalWallet, externalNode); } return externalIndexHandler; @@ -96,6 +99,16 @@ public class SparrowWalletStateSupplier implements WalletStateSupplier { // nothing required } + @Override + public boolean isNymClaimed() { + return false; // nothing required + } + + @Override + public void setNymClaimed(boolean value) { + // nothing required + } + @Override public void load() throws Exception { // nothing required @@ -107,17 +120,19 @@ public class SparrowWalletStateSupplier implements WalletStateSupplier { return false; } - private String mapKey(WhirlpoolAccount whirlpoolAccount, AddressType addressType, Chain chain) { - return whirlpoolAccount.name()+"_"+addressType.getPurpose()+"_"+chain.getIndex(); + private String mapKey(BipWallet bipWallet, Chain chain) { + SamouraiAccount samouraiAccount = bipWallet.getAccount(); + BipDerivation derivation = bipWallet.getDerivation(); + return samouraiAccount.name() + "_" + derivation.getPurpose() + "_" + chain.getIndex(); } - private Wallet findWallet(WhirlpoolAccount whirlpoolAccount) { + private Wallet findWallet(SamouraiAccount samouraiAccount) { Wallet wallet = getWallet(); if(wallet == null) { throw new IllegalStateException("Can't find wallet with walletId " + walletId); } - return Whirlpool.getStandardAccountWallet(whirlpoolAccount, wallet); + return Whirlpool.getStandardAccountWallet(samouraiAccount, wallet); } private Wallet getWallet() { diff --git a/src/main/java/com/sparrowwallet/sparrow/whirlpool/tor/SparrowTorClientService.java b/src/main/java/com/sparrowwallet/sparrow/whirlpool/tor/SparrowTorClientService.java deleted file mode 100644 index 5cfd57a3..00000000 --- a/src/main/java/com/sparrowwallet/sparrow/whirlpool/tor/SparrowTorClientService.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.sparrowwallet.sparrow.whirlpool.tor; - -import com.google.common.net.HostAndPort; -import com.samourai.tor.client.TorClientService; -import com.sparrowwallet.sparrow.net.TorUtils; -import com.sparrowwallet.sparrow.whirlpool.Whirlpool; - -public class SparrowTorClientService extends TorClientService { - private final Whirlpool whirlpool; - - public SparrowTorClientService(Whirlpool whirlpool) { - this.whirlpool = whirlpool; - } - - @Override - public void changeIdentity() { - HostAndPort proxy = whirlpool.getTorProxy(); - if(proxy != null) { - TorUtils.changeIdentity(proxy); - } - } -} diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 5b667b9c..79e1caaf 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -50,7 +50,6 @@ open module com.sparrowwallet.sparrow { requires com.nativelibs4java.bridj; requires org.reactfx.reactfx; requires dev.bwt.jni; - requires com.sparrowwallet.nightjar; requires io.reactivex.rxjava2; requires io.reactivex.rxjava2fx; requires org.apache.commons.lang3; @@ -65,4 +64,10 @@ open module com.sparrowwallet.sparrow { requires com.sparrowwallet.bokmakierie; requires java.smartcardio; requires com.jcraft.jzlib; + requires com.samourai.whirlpool.client; + requires com.samourai.whirlpool.protocol; + requires com.samourai.extlibj; + requires com.samourai.soroban.client; + requires com.samourai.http.client; + requires com.samourai.bitcoinj; } \ No newline at end of file diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index 93746fb8..33a066e9 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -39,7 +39,6 @@ -