upgrade to whirlpool 1.0.0-beta4

This commit is contained in:
zeroleak 2024-03-05 18:32:09 +01:00
parent c34a423f95
commit 249a01c208
28 changed files with 840 additions and 845 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.0-beta4')
implementation('io.samourai.code.wallet:java-http-client:2.0.0-beta3')
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/*']
@ -491,88 +490,36 @@ 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')
// begin samourai dependencies
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')
}
// end samourai dependencies
module('okio-1.6.0.jar', 'com.squareup.okio', '1.6.0') {
exports('okio')
}
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

@ -521,11 +521,10 @@ public class AppServices {
}
public static HttpClientService getHttpClientService() {
if(httpClientService == null) {
HostAndPort torProxy = getTorProxy();
if(httpClientService == null) {
httpClientService = new HttpClientService(torProxy);
} else {
HostAndPort torProxy = getTorProxy();
if(!Objects.equals(httpClientService.getTorProxy(), torProxy)) {
httpClientService.setTorProxy(getTorProxy());
}

View file

@ -120,6 +120,8 @@ public class MixStatusCell extends TreeTableCell<Entry, UtxoEntry.MixStatus> {
tt.setText(status);
setTooltip(tt);
// TODO nbRegisteredInputs is not available anymore
/*
if(mixProgress.getMixStep() == MixStep.REGISTERED_INPUT) {
tt.setOnShowing(event -> {
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(utxoEntry.getWallet());
@ -131,7 +133,7 @@ public class MixStatusCell extends TreeTableCell<Entry, UtxoEntry.MixStatus> {
});
registeredInputsService.start();
});
}
}*/
} else {
setGraphic(null);
setTooltip(null);

View file

@ -1,11 +1,11 @@
package com.sparrowwallet.sparrow.net;
import com.google.common.net.HostAndPort;
import com.samourai.wallet.api.backend.beans.HttpException;
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(HttpException 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.api.backend.beans.HttpException;
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 HttpException 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,56 @@
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.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,
ClientUtils.USER_AGENT,
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 +64,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;
}
// TODO verify
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

@ -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.api.backend.beans.HttpException;
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(HttpException 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,18 +1,24 @@
package com.sparrowwallet.sparrow.soroban;
import com.samourai.soroban.cahoots.CahootsContext;
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.samourai.wallet.cahoots.CahootsWallet;
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;
@ -226,26 +232,28 @@ public class CounterpartyController extends SorobanController {
private void startCounterpartyMeetingReceive() {
Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId);
SparrowCahootsWallet counterpartyCahootsWallet = soroban.getCahootsWallet(wallet, 1);
SorobanWalletService sorobanWalletService = soroban.getSorobanWalletService();
SparrowCahootsWallet cahootsWallet = soroban.getCahootsWallet(wallet);
SorobanWalletCounterparty sorobanWalletCounterparty = sorobanWalletService.getSorobanWalletCounterparty(cahootsWallet);
sorobanWalletCounterparty.setTimeoutMeetingMs(TIMEOUT_MS);
try {
SorobanCahootsService sorobanMeetingService = soroban.getSorobanCahootsService(counterpartyCahootsWallet);
sorobanMeetingService.receiveMeetingRequest(TIMEOUT_MS)
.subscribeOn(Schedulers.io())
.observeOn(JavaFxScheduler.platform())
.subscribe(requestMessage -> {
String code = requestMessage.getSender();
// TODO run in background thread?
SorobanRequestMessage requestMessage = sorobanWalletCounterparty.receiveMeetingRequest();
PaymentCode paymentCodeInitiator = 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)
sorobanWalletCounterparty.sendMeetingResponse(requestMessage, accepted)
.subscribeOn(Schedulers.io())
.observeOn(JavaFxScheduler.platform())
.subscribe(responseMessage -> {
requestUserAttention();
if(accepted) {
startCounterpartyCollaboration(counterpartyCahootsWallet, paymentCodeInitiator, cahootsType);
startCounterpartyCollaboration(sorobanWalletCounterparty, paymentCodeInitiator, cahootsType, soroban.getBip47Account());
followPaymentCode(paymentCodeInitiator);
}
}, error -> {
@ -253,13 +261,10 @@ public class CounterpartyController extends SorobanController {
mixingPartner.setVisible(false);
requestUserAttention();
});
}, error -> {
log.error("Failed to receive meeting request", error);
} catch(Exception e) {
log.error("Failed to receive meeting request", e);
mixingPartner.setVisible(false);
requestUserAttention();
});
} catch(Exception e) {
log.error("Error sending meeting response", e);
}
}
@ -290,23 +295,18 @@ 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;
CahootsContext cahootsContext = CahootsContext.newCounterparty(cahootsWallet, cahootsType, account);
Consumer<OnlineCahootsMessage> onProgress = cahootsMessage -> {
if(cahootsMessage != null) {
Cahoots cahoots = cahootsMessage.getCahoots();
sorobanProgressBar.setProgress((double)(cahoots.getStep() + 1) / 5);
@ -329,6 +329,12 @@ public class CounterpartyController extends SorobanController {
}
}
}
};
sorobanWalletCounterparty.counterparty(cahootsContext, initiatorPaymentCode, onProgress)
.subscribeOn(Schedulers.io())
.observeOn(JavaFxScheduler.platform())
.subscribe(cahoots -> {
// cahoots success
}, error -> {
log.error("Error creating mix transaction", error);
String cutFrom = "Exception: ";

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;
@ -62,7 +67,6 @@ 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 +203,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 +225,7 @@ public class InitiatorController extends SorobanController {
step2.visibleProperty().addListener((observable, oldValue, visible) -> {
if(visible) {
startInitiatorMeetingRequest();
startInitiatorMeetAndInitiate();
step2Timer.start();
}
});
@ -358,7 +362,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 +381,7 @@ public class InitiatorController extends SorobanController {
try {
soroban.setHDWallet(copy);
startInitiatorMeetingRequest(soroban, wallet);
startInitiatorMeetAndInitiate(soroban, wallet);
} finally {
key.clear();
encryptionFullKey.clear();
@ -389,7 +393,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,98 +407,60 @@ 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 -> {
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, soroban.getBip47Account(), feePerB, payment.getAmount(), payment.getAddress().getAddress(), paymentCodeCounterparty.toString());
CahootsSorobanInitiatorListener listener = new CahootsSorobanInitiatorListener() {
@Override
public void onResponse(SorobanResponseMessage sorobanResponse) throws Exception {
super.onResponse(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);
}
}, error -> {
log.error("Could not retrieve payment code", error);
if(error.getMessage().endsWith("404")) {
step2Desc.setText("PayNym not found");
} else if(error.getMessage().endsWith("400")) {
step2Desc.setText("Could not retrieve PayNym");
} else {
step2Desc.setText(error.getMessage());
}
sorobanProgressLabel.setVisible(false);
});
}
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 -> {
@Override
public void onInteraction(OnlineSorobanInteraction interaction) throws Exception {
SorobanInteraction originInteraction = interaction.getInteraction();
if (originInteraction instanceof TxBroadcastInteraction) {
Boolean accepted = (Boolean)Platform.enterNestedEventLoop(transactionAccepted);
if(accepted) {
interaction.sorobanAccept();
} else {
interaction.sorobanReject("Mix partner declined to broadcast the transaction.");
}
});
} else {
throw new Exception("Unknown interaction: "+originInteraction.getTypeInteraction());
}
}
try {
sorobanCahootsService.initiator(initiatorCahootsWallet.getAccount(), cahootsContext, paymentCodeCounterparty, TIMEOUT_MS)
.subscribeOn(Schedulers.io())
.observeOn(JavaFxScheduler.platform())
.subscribe(sorobanMessage -> {
OnlineCahootsMessage cahootsMessage = (OnlineCahootsMessage)sorobanMessage;
@Override
public void progress(OnlineCahootsMessage message) {
super.progress(message);
OnlineCahootsMessage cahootsMessage = (OnlineCahootsMessage)message;
if(cahootsMessage != null) {
Cahoots cahoots = cahootsMessage.getCahoots();
sorobanProgressBar.setProgress((double)(cahoots.getStep() + 1) / 5);
@ -524,15 +490,32 @@ public class InitiatorController extends SorobanController {
}
}
}
},
error -> {
log.error("Error creating mix transaction", error);
}
};
SorobanWalletService sorobanWalletService = soroban.getSorobanWalletService();
sorobanProgressLabel.setText("Waiting for mix partner...");
sorobanWalletService.getSorobanWalletInitiator(cahootsWallet).meetAndInitiate(cahootsContext, paymentCodeCounterparty, listener)
.subscribeOn(Schedulers.io())
.observeOn(JavaFxScheduler.platform())
.subscribe(sorobanResponse -> {
}, error -> {
log.error("Error receiving meeting response", error);
step2Desc.setText(getErrorMessage(error));
sorobanProgressLabel.setVisible(false);
meetingFail.setVisible(true);
requestUserAttention();
});
} catch(Exception e) {
log.error("Soroban communication error", e);
}, error -> {
log.error("Could not retrieve payment code", error);
if(error.getMessage().endsWith("404")) {
step2Desc.setText("PayNym not found");
} else if(error.getMessage().endsWith("400")) {
step2Desc.setText("Could not retrieve PayNym");
} else {
step2Desc.setText(error.getMessage());
}
sorobanProgressLabel.setVisible(false);
});
}
public void broadcastTransaction() {

View file

@ -1,48 +1,37 @@
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.chain.ChainSupplier;
import com.samourai.wallet.hd.HD_Wallet;
import com.samourai.wallet.hd.HD_WalletFactoryGeneric;
import com.sparrowwallet.drongo.Drongo;
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 com.sparrowwallet.sparrow.whirlpool.Whirlpool;
import com.sparrowwallet.sparrow.whirlpool.dataSource.SparrowChainSupplier;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
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;
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 +39,12 @@ 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);
NetworkParameters params = sorobanWalletService.getSorobanService().getParams();
hdWallet = Whirlpool.computeHdWallet(wallet, params);
bip47Account = wallet.isMasterWallet() ? wallet.getAccountIndex() : wallet.getMasterWallet().getAccountIndex();
} catch(Exception e) {
throw new IllegalStateException("Could not create Soroban HD wallet ", e);
}
}
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,8 @@ public class Soroban {
}
try {
return new SparrowCahootsWallet(wallet, hdWallet, bip47Account, sorobanServer, (long)feeRate);
ChainSupplier chainSupplier = new SparrowChainSupplier(wallet.getStoredBlockHeight());
return new SparrowCahootsWallet(chainSupplier, wallet, hdWallet, bip47Account);
} catch(Exception e) {
log.error("Could not create cahoots wallet", e);
}
@ -95,24 +72,16 @@ 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 void stop() {
AppServices.getHttpClientService().stop();
}
public static class ShutdownService extends Service<Boolean> {
@ -126,7 +95,7 @@ public class Soroban {
protected Task<Boolean> createTask() {
return new Task<>() {
protected Boolean call() throws Exception {
soroban.shutdown();
soroban.stop();
return true;
}
};

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;

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,25 @@ 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.LinkedList;
import java.util.List;
public class SparrowCahootsWallet extends SimpleCahootsWallet {
public class SparrowCahootsWallet extends AbstractCahootsWallet {
private final Wallet wallet;
private HD_Wallet bip84w;
private final int account;
private final int bip47Account;
private List<CahootsUtxo> utxos;
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 +48,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 +57,27 @@ 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, KeyPurpose.CHANGE);
}
return getAddress(account, KeyPurpose.RECEIVE);
}
@Override
protected String doFetchAddressChange(int account, boolean increment, BipFormat bipFormat) throws Exception {
return getAddress(account, 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, KeyPurpose keyPurpose) {
return getWallet(account).getFreshNode(keyPurpose).getAddress().getAddress();
}
private Wallet getWallet(int account) {
@ -108,9 +87,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(index, unspentOutput);
cahootsUtxo = new CahootsUtxo(myTransactionOutPoint, node.getDerivationPath(), null, hdAddress.getECKey().getPrivKeyBytes());
}
utxos.add(cahootsUtxo);
}
}

View file

@ -1,44 +1,55 @@
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.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.Tx0PreviewService;
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.beans.Pool;
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.*;
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.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.rxjavafx.schedulers.JavaFxScheduler;
import io.reactivex.schedulers.Schedulers;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
@ -46,6 +57,7 @@ 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;
@ -58,17 +70,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 Tx0FeeTarget tx0FeeTarget = Tx0FeeTarget.BLOCKS_4;
private Tx0FeeTarget mixFeeTarget = Tx0FeeTarget.BLOCKS_4;
private HD_Wallet hdWallet;
@ -83,64 +89,63 @@ 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() {
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);
Integer storedBlockHeight = null; // TODO
this.config = computeWhirlpoolWalletConfig(storedBlockHeight);
this.whirlpoolInfo = null; // instanciated 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);
}
return null;
private WhirlpoolInfo getWhirlpoolInfo() {
if (whirlpoolInfo == null) {
whirlpoolInfo = new WhirlpoolInfo(SparrowMinerFeeSupplier.getInstance(), config);
}
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 {
// preview all pools
Tx0Config tx0Config = computeTx0Config();
return tx0Service.tx0Previews(utxos, tx0Config);
return AsyncUtil.getInstance().blockingGet(getWhirlpoolInfo().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());
@ -153,24 +158,29 @@ public class Whirlpool {
}
private Tx0Config computeTx0Config() {
return new Tx0Config(tx0ParamService, poolSupplier, tx0FeeTarget, mixFeeTarget, WhirlpoolAccount.BADBANK);
CoordinatorSupplier coordinatorSupplier = getWhirlpoolInfo().getCoordinatorSupplier();
Tx0PreviewService tx0PreviewService = getWhirlpoolInfo().getTx0PreviewService();
Collection<Pool> pools = coordinatorSupplier.getPools();
return new Tx0Config(tx0PreviewService, pools, tx0FeeTarget, mixFeeTarget, SamouraiAccount.BADBANK);
}
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");
}
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 words = keystore.getSeed().getMnemonicString().asString();
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.restoreWalletFromWords(words, passphrase, params);
} catch(Exception e) {
throw new IllegalStateException("Could not create Whirlpool HD wallet ", e);
}
@ -187,7 +197,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 +209,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 +264,9 @@ public class Whirlpool {
public void refreshUtxos() {
if(whirlpoolWalletService.whirlpoolWallet() != null) {
whirlpoolWalletService.whirlpoolWallet().refreshUtxos();
whirlpoolWalletService.whirlpoolWallet().refreshUtxosAsync()
.subscribeOn(Schedulers.io())
.observeOn(JavaFxScheduler.platform());
}
}
@ -321,7 +321,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 +352,7 @@ public class Whirlpool {
public void shutdown() {
whirlpoolWalletService.closeWallet();
httpClientService.shutdown();
AppServices.getHttpClientService().stop();
}
public StartupService createStartupService() {
@ -389,7 +389,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 +403,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,9 +442,11 @@ 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);
@ -453,26 +455,8 @@ public class Whirlpool {
return out;
}
public HostAndPort getTorProxy() {
return httpClientService.getTorProxy();
}
public void setTorProxy(HostAndPort torProxy) {
if(isStarted()) {
throw new IllegalStateException("Cannot set tor proxy on a started Whirlpool");
}
//Ensure all http clients are shutdown first
httpClientService.shutdown();
httpClientService.setTorProxy(torProxy);
String serverUrl = whirlpoolServer.getServerUrl(torProxy != null);
ServerApi serverApi = new ServerApi(serverUrl, httpClientService);
config.setServerApi(serverApi);
}
public void refreshTorCircuits() {
torClientService.changeIdentity();
AppServices.getHttpClientService().changeIdentity();
}
public String getScode() {
@ -530,16 +514,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 +558,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 +568,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 +578,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 +606,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 +691,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 +715,10 @@ 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 +756,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);
}

View file

@ -2,7 +2,11 @@ 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.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 +18,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;
@ -36,6 +41,24 @@ public class WhirlpoolServices {
private final Map<String, Whirlpool> whirlpoolMap = new HashMap<>();
private final SorobanConfig sorobanConfig;
public WhirlpoolServices() {
ExtLibJConfig extLibJConfig = computeExtLibJConfig();
this.sorobanConfig = new SorobanConfig(extLibJConfig);
}
private ExtLibJConfig computeExtLibJConfig() {
HttpClientService httpClientService = AppServices.getHttpClientService();
boolean onion = (AppServices.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 +73,8 @@ 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);
whirlpool = new Whirlpool();
whirlpoolMap.put(walletId, whirlpool);
} else if(!whirlpool.isStarted()) {
HostAndPort torProxy = getTorProxy();
if(!Objects.equals(whirlpool.getTorProxy(), torProxy)) {
whirlpool.setTorProxy(getTorProxy());
}
}
return whirlpool;
@ -87,11 +104,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 +134,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();
}
@ -300,4 +313,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;
@ -37,7 +37,7 @@ public class SparrowUtxoConfigPersister extends UtxoConfigPersister {
}
Map<String, UtxoConfigPersisted> utxoConfigs = wallet.getUtxoMixes().entrySet().stream()
.collect(Collectors.toMap(entry -> entry.getKey().toString(), entry -> new UtxoConfigPersisted(entry.getValue().getMixesDone(), entry.getValue().getExpired()),
.collect(Collectors.toMap(entry -> entry.getKey().toString(), entry -> new UtxoConfigPersisted(entry.getValue().getMixesDone(), entry.getValue().getExpired(), false, null),
(u, v) -> { throw new IllegalStateException("Duplicate utxo config hashes"); },
ConcurrentHashMap::new));

View file

@ -0,0 +1,44 @@
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.protocol.Sha256Hash;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.NewBlockEvent;
public class SparrowChainSupplier implements ChainSupplier {
private int storedBlockHeight;
private WalletResponse.InfoBlock latestBlock;
public SparrowChainSupplier(Integer storedBlockHeight) {
this.storedBlockHeight = AppServices.getCurrentBlockHeight() == null ?
(storedBlockHeight!=null?storedBlockHeight:0)
: AppServices.getCurrentBlockHeight();
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 = Sha256Hash.ZERO_HASH.toString();
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,107 @@
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.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 SparrowUtxoSupplier utxoSupplier;
public SparrowDataSource(
WhirlpoolWallet whirlpoolWallet,
HD_Wallet bip44w,
DataPersister dataPersister)
WalletStateSupplier walletStateSupplier,
UtxoConfigSupplier utxoConfigSupplier,
DataSourceConfig dataSourceConfig)
throws Exception {
super(whirlpoolWallet, bip44w, dataPersister);
super(whirlpoolWallet, bip44w, walletStateSupplier, dataSourceConfig);
this.seenBackend = computeSeenBackend(whirlpoolWallet.getConfig());
this.pushTx = computePushTx();
this.utxoSupplier = new SparrowUtxoSupplier(whirlpoolWallet, walletSupplier, utxoConfigSupplier, dataSourceConfig);
}
// prefix matching <prefix>:master, :Premix, :Postmix
this.walletIdentifierPrefix = getWhirlpoolWallet().getWalletIdentifier().replace(":master", "");
private ISeenBackend computeSeenBackend(WhirlpoolWalletConfig whirlpoolWalletConfig) {
IHttpClient httpClient = whirlpoolWalletConfig.getHttpClient(HttpUsage.BACKEND);
ISeenBackend sparrowSeenBackend = new SparrowSeenBackend(httpClient);
NetworkParameters params = whirlpoolWalletConfig.getSamouraiNetwork().getParams();
return SeenBackendWithFallback.withOxt(sparrowSeenBackend, params);
}
private IPushTx computePushTx() {
return new IPushTx() {
@Override
public String pushTx(String hexTx) throws Exception {
Transaction transaction = new Transaction(Utils.hexToBytes(hexTx));
ElectrumServer electrumServer = new ElectrumServer();
return electrumServer.broadcastTransactionPrivately(transaction).toString();
}
@Override
public void open() throws Exception {
super.open();
EventManager.get().register(this);
}
@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) {
@Override
public void refresh() throws Exception {
SparrowDataSource.this.refresh();
}
@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);
}
@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 +127,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 +138,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,10 @@
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.AbstractPostmixHandler;
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.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;
@ -13,65 +12,37 @@ import com.sparrowwallet.drongo.wallet.WalletNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class SparrowPostmixHandler implements IPostmixHandler {
// TODO maybe replace with XPubPostmixHandler
public class SparrowPostmixHandler extends AbstractPostmixHandler {
private static final Logger log = LoggerFactory.getLogger(SparrowPostmixHandler.class);
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) {
this.whirlpoolWalletService = whirlpoolWalletService;
public SparrowPostmixHandler(WhirlpoolWalletService whirlpoolWalletService, Wallet wallet, KeyPurpose keyPurpose) {
super(whirlpoolWalletService.whirlpoolWallet().getWalletStateSupplier().getIndexHandlerExternal(),
whirlpoolWalletService.whirlpoolWallet().getConfig().getSamouraiNetwork().getParams());
this.wallet = wallet;
this.keyPurpose = keyPurpose;
this.startIndex = startIndex;
}
@Override
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 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) {
getIndexHandler().cancelUnconfirmed(destination.getIndex());
}
}
@Override
public void onRegisterOutput() {
// confirm receive address even when REGISTER_OUTPUT fails, to avoid 'ouput already registered'
getIndexHandler().confirmUnconfirmed(destination.getIndex());
}
private IIndexHandler getIndexHandler() {
return whirlpoolWalletService.whirlpoolWallet().getWalletStateSupplier().getIndexHandlerExternal();
}
}

View file

@ -0,0 +1,30 @@
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 java.util.Collection;
public class SparrowSeenBackend implements ISeenBackend {
private IHttpClient httpClient;
public SparrowSeenBackend(IHttpClient httpClient) {
this.httpClient = httpClient;
}
@Override
public SeenResponse seen(Collection<String> addresses) throws Exception {
return null; // TODO implement: check if each address already received funds
}
@Override
public boolean seen(String address) throws Exception {
return false; // TODO implement: return true if address already received funds
}
@Override
public IHttpClient getHttpClient() {
return httpClient;
}
}

View file

@ -0,0 +1,150 @@
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.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(
WhirlpoolWallet whirlpoolWallet,
WalletSupplier walletSupplier,
UtxoConfigSupplier utxoConfigSupplier,
DataSourceConfig dataSourceConfig) {
super(whirlpoolWallet, walletSupplier, utxoConfigSupplier, dataSourceConfig);
}
@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);
setValue(utxoData);
}
@Override
protected byte[] _getPrivKey(WhirlpoolUtxo whirlpoolUtxo) throws Exception {
UnspentOutput utxo = whirlpoolUtxo.getUtxo();
Wallet wallet = SparrowDataSource.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;
}
}

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);
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,11 @@ open module com.sparrowwallet.sparrow {
requires com.sparrowwallet.bokmakierie;
requires java.smartcardio;
requires com.jcraft.jzlib;
// samourai dependencies
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"/>