merge whirlpool 1.x client using decentralized soroban

This commit is contained in:
Craig Raw 2024-04-11 08:37:28 +02:00
commit 15cb028951
34 changed files with 1010 additions and 880 deletions

View file

@ -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')
}

View file

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

View file

@ -70,7 +70,7 @@ public class MixStatusCell extends TreeTableCell<Entry, UtxoEntry.MixStatus> {
}
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<Entry, UtxoEntry.MixStatus> {
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);

View file

@ -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);

View file

@ -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);

View file

@ -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() + ")");

View file

@ -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> T requestJson(String url, Class<T> responseType, Map<String, String> 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 <T> Observable<Optional<T>> postJson(String url, Class<T> responseType, Map<String, String> 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<String, String> 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<Boolean> {
@ -65,7 +60,7 @@ public class HttpClientService {
protected Task<Boolean> createTask() {
return new Task<>() {
protected Boolean call() throws Exception {
httpClientService.shutdown();
httpClientService.stop();
return true;
}
};

View file

@ -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<HttpProxy> getHttpProxy(HttpUsage httpUsage) {
return Optional.ofNullable(httpProxy);
}
@Override
public void changeIdentity() {
HostAndPort torProxy = getTorProxy();
if(torProxy != null) {
TorUtils.changeIdentity(torProxy);
}
}
}

View file

@ -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?");
}

View file

@ -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() + ")");

View file

@ -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<Map<String, Object>> 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<String, Object> 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<Map<String, Object>> 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<Map<String, Object>> 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<Map<String, Object>> 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<PayNym> getPayNym(String nymIdentifier) {

View file

@ -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<SorobanRequestMessage> receiveMeetingService = new Service<>() {
@Override
protected Task<SorobanRequestMessage> 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<BlockTransactionHashIndex, WalletNode> walletUtxos = wallet.getSpendableUtxos();
for(Map.Entry<BlockTransactionHashIndex, WalletNode> 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<OnlineCahootsMessage> 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<Cahoots> cahootsService = new Service<>() {
@Override
protected Task<Cahoots> 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());

View file

@ -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<ButtonType> 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<BlockTransactionHashIndex, WalletNode> firstSetUtxos = walletTransaction.isCoinControlUsed() ? walletTransaction.getSelectedUtxoSets().get(0) : wallet.getSpendableUtxos();
for(Map.Entry<BlockTransactionHashIndex, WalletNode> 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<Cahoots> cahootsService = new Service<>() {
@Override
protected Task<Cahoots> 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<BlockTransactionHashIndex, WalletNode> firstSetUtxos = walletTransaction.isCoinControlUsed() ? walletTransaction.getSelectedUtxoSets().get(0) : wallet.getSpendableUtxos();
for(Map.Entry<BlockTransactionHashIndex, WalletNode> 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;

View file

@ -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<Network> 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<String> 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<Boolean> {
private final Soroban soroban;
public ShutdownService(Soroban soroban) {
this.soroban = soroban;
}
@Override
protected Task<Boolean> createTask() {
return new Task<>() {
protected Boolean call() throws Exception {
soroban.shutdown();
return true;
}
};
public void close() {
if(chainSupplier != null) {
chainSupplier.close();
}
}
}

View file

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

View file

@ -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<CahootsUtxo> utxos;
private final Map<KeyPurpose, WalletNode> 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<CahootsUtxo> fetchUtxos(int account) {
List<CahootsUtxo> 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<CahootsUtxo> getUtxosWpkhByAccount(int account) {
return utxos;
}
@Override
public Pair<Integer, Integer> 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<Integer, Integer> 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);
}
}

View file

@ -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 -> {

View file

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

View file

@ -129,9 +129,10 @@ public class WalletTransactionsEntry extends Entry {
private static void getWalletTransactions(Wallet wallet, Map<BlockTransaction, WalletTransaction> walletTransactionMap, WalletNode purposeNode) {
KeyPurpose keyPurpose = purposeNode.getKeyPurpose();
List<WalletNode> 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) {

View file

@ -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() {

View file

@ -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<Network> 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<Pool> 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<UnspentOutput> 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<BlockTransactionHashIndex> utxos) throws Exception {
WhirlpoolWallet whirlpoolWallet = getWhirlpoolWallet();
whirlpoolWallet.start();
whirlpoolWallet.startAsync().subscribeOn(Schedulers.io()).observeOn(JavaFxScheduler.platform());
UtxoSupplier utxoSupplier = whirlpoolWallet.getUtxoSupplier();
List<WhirlpoolUtxo> 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<ExtendedKey.Header> 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<Integer> {
private final Whirlpool whirlpool;
private final String poolId;
public RegisteredInputsService(Whirlpool whirlpool, String poolId) {
this.whirlpool = whirlpool;
this.poolId = poolId;
}
@Override
protected Task<Integer> 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;

View file

@ -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 -> {

View file

@ -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<String, Whirlpool> 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<Wallet, Storage> 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;
}
}

View file

@ -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;
}

View file

@ -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;

View file

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

View file

@ -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 <prefix>: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<Sha256Hash, BlockTransaction> allTransactions = new HashMap<>();
Map<Sha256Hash, String> allTransactionsZpubs = new HashMap<>();
List<WalletResponse.Address> addresses = new ArrayList<>();
List<WalletResponse.Tx> txes = new ArrayList<>();
List<UnspentOutput> 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<Sha256Hash, BlockTransaction> 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<ExtendedKey.Header> 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<BlockTransactionHashIndex, WalletNode> 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<BlockTransactionHashIndex, WalletNode> 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<Integer> 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 <prefix>:master, :Premix, :Postmix
String walletIdentifierPrefix = getWhirlpoolWallet().getWalletIdentifier().replace(":master", "");
// match <prefix>: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;
}
}

View file

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

View file

@ -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<String> addresses) throws Exception {
Wallet wallet = AppServices.get().getWallet(walletId);
Map<Address, WalletNode> addressMap = wallet.getWalletAddresses();
for(Wallet childWallet : wallet.getChildWallets()) {
if(!childWallet.isNested()) {
addressMap.putAll(childWallet.getWalletAddresses());
}
}
Map<String,Boolean> 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;
}
}

View file

@ -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<Sha256Hash, BlockTransaction> allTransactions = new HashMap<>();
Map<Sha256Hash, String> allTransactionsXpubs = new HashMap<>();
List<WalletResponse.Tx> txes = new ArrayList<>();
List<UnspentOutput> unspentOutputs = new ArrayList<>();
int storedBlockHeight = 0;
Collection<BipWallet> 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<Sha256Hash, BlockTransaction> 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<BlockTransactionHashIndex, WalletNode> 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<BlockTransactionHashIndex, WalletNode> 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;
}
}

View file

@ -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() {

View file

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

View file

@ -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;
}

View file

@ -39,7 +39,6 @@
<logger name="org.springframework.web.HttpLogging" level="OFF" />
<logger name="org.springframework.web.socket.sockjs.client.SockJsClient" level="OFF" />
<logger name="org.springframework.web.socket.sockjs.client.DefaultTransportRequest" level="OFF" />
<logger name="com.sparrowwallet.nightjar.stomp.JavaStompClient" level="OFF" />
<contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator"/>