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