mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-11-02 20:36:44 +00:00
upgrade to whirlpool 1.0.0-beta4
This commit is contained in:
parent
c34a423f95
commit
249a01c208
28 changed files with 840 additions and 845 deletions
87
build.gradle
87
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.0-beta4')
|
||||
implementation('io.samourai.code.wallet:java-http-client:2.0.0-beta3')
|
||||
implementation('io.reactivex.rxjava2:rxjava:2.2.15')
|
||||
implementation('io.reactivex.rxjava2:rxjavafx:2.2.2')
|
||||
implementation('org.apache.commons:commons-lang3:3.7')
|
||||
|
@ -212,8 +213,6 @@ jlink {
|
|||
uses 'org.flywaydb.core.extensibility.FlywayExtension'
|
||||
uses 'org.flywaydb.core.internal.database.DatabaseType'
|
||||
uses 'org.eclipse.jetty.http.HttpFieldPreEncoder'
|
||||
uses 'org.eclipse.jetty.websocket.api.extensions.Extension'
|
||||
uses 'org.eclipse.jetty.websocket.common.RemoteEndpointFactory'
|
||||
}
|
||||
|
||||
options = ['--strip-native-commands', '--strip-java-debug-attributes', '--compress', '2', '--no-header-files', '--no-man-pages', '--ignore-signing-information', '--exclude-files', '**.png', '--exclude-resources', 'glob:/com.sparrowwallet.merged.module/META-INF/*']
|
||||
|
@ -491,88 +490,36 @@ extraJavaModuleInfo {
|
|||
exports('co.nstant.in.cbor.model')
|
||||
exports('co.nstant.in.cbor.builder')
|
||||
}
|
||||
module('nightjar-0.2.40.jar', 'com.sparrowwallet.nightjar', '0.2.40') {
|
||||
requires('com.google.common')
|
||||
requires('net.sourceforge.streamsupport')
|
||||
requires('org.slf4j')
|
||||
requires('org.bouncycastle.provider')
|
||||
requires('com.fasterxml.jackson.databind')
|
||||
requires('com.fasterxml.jackson.annotation')
|
||||
requires('com.fasterxml.jackson.core')
|
||||
requires('ch.qos.logback.classic')
|
||||
requires('org.json')
|
||||
requires('io.reactivex.rxjava2')
|
||||
exports('com.samourai.http.client')
|
||||
exports('com.samourai.tor.client')
|
||||
exports('com.samourai.wallet.api.backend')
|
||||
exports('com.samourai.wallet.api.backend.beans')
|
||||
exports('com.samourai.wallet.client.indexHandler')
|
||||
exports('com.samourai.wallet.hd')
|
||||
exports('com.samourai.wallet.util')
|
||||
exports('com.samourai.wallet.bip47.rpc')
|
||||
exports('com.samourai.wallet.bip47.rpc.java')
|
||||
exports('com.samourai.wallet.cahoots')
|
||||
exports('com.samourai.wallet.cahoots.psbt')
|
||||
exports('com.samourai.wallet.cahoots.stonewallx2')
|
||||
exports('com.samourai.soroban.cahoots')
|
||||
exports('com.samourai.soroban.client')
|
||||
exports('com.samourai.soroban.client.cahoots')
|
||||
exports('com.samourai.soroban.client.meeting')
|
||||
exports('com.samourai.soroban.client.rpc')
|
||||
exports('com.samourai.wallet.send')
|
||||
exports('com.samourai.whirlpool.client.event')
|
||||
exports('com.samourai.whirlpool.client.wallet')
|
||||
exports('com.samourai.whirlpool.client.wallet.beans')
|
||||
exports('com.samourai.whirlpool.client.wallet.data.dataSource')
|
||||
exports('com.samourai.whirlpool.client.wallet.data.dataPersister')
|
||||
exports('com.samourai.whirlpool.client.whirlpool')
|
||||
exports('com.samourai.whirlpool.client.whirlpool.beans')
|
||||
exports('com.samourai.whirlpool.client.wallet.data.pool')
|
||||
exports('com.samourai.whirlpool.client.wallet.data.utxo')
|
||||
exports('com.samourai.whirlpool.client.wallet.data.utxoConfig')
|
||||
exports('com.samourai.whirlpool.client.wallet.data.supplier')
|
||||
exports('com.samourai.whirlpool.client.mix.handler')
|
||||
exports('com.samourai.whirlpool.client.mix.listener')
|
||||
exports('com.samourai.whirlpool.protocol.beans')
|
||||
exports('com.samourai.whirlpool.protocol.rest')
|
||||
exports('com.samourai.whirlpool.client.tx0')
|
||||
exports('com.samourai.wallet.segwit.bech32')
|
||||
exports('com.samourai.whirlpool.client.wallet.data.chain')
|
||||
exports('com.samourai.whirlpool.client.wallet.data.wallet')
|
||||
exports('com.samourai.whirlpool.client.wallet.data.minerFee')
|
||||
exports('com.samourai.whirlpool.client.wallet.data.walletState')
|
||||
exports('com.sparrowwallet.nightjar.http')
|
||||
exports('com.sparrowwallet.nightjar.stomp')
|
||||
exports('com.sparrowwallet.nightjar.tor')
|
||||
// begin samourai dependencies
|
||||
module('commons-codec-1.10.jar', 'commons.codec', '1.10') {
|
||||
exports('org.apache.commons.codec')
|
||||
}
|
||||
module('throwing-supplier-1.0.3.jar', 'zeroleak.throwingsupplier', '1.0.3') {
|
||||
exports('com.zeroleak.throwingsupplier')
|
||||
module('logback-core-1.2.13.jar', 'ch.qos.logback.core', '1.2.13') {
|
||||
exports('ch.qos.logback.core')
|
||||
}
|
||||
module('okhttp-2.7.5.jar', 'com.squareup.okhttp', '2.7.5') {
|
||||
exports('com.squareup.okhttp')
|
||||
module('jackson-datatype-jsr310-2.13.2.jar', 'jackson-datatype-jsr310', '2.13.2') {
|
||||
exports('com.fasterxml.jackson.datatype.jsr310')
|
||||
}
|
||||
module('json-20240205.jar', 'org.json', '20240205') {
|
||||
exports('org.json')
|
||||
}
|
||||
module('scrypt-1.4.0.jar', 'scrypt', '1.4.0') {
|
||||
exports('com.lambdaworks.codec')
|
||||
exports('com.lambdaworks.crypto')
|
||||
}
|
||||
// end samourai dependencies
|
||||
module('okio-1.6.0.jar', 'com.squareup.okio', '1.6.0') {
|
||||
exports('okio')
|
||||
}
|
||||
module('java-jwt-3.8.1.jar', 'com.auth0.jwt', '3.8.1') {
|
||||
exports('com.auth0.jwt')
|
||||
}
|
||||
module('json-20180130.jar', 'org.json', '1.0') {
|
||||
exports('org.json')
|
||||
}
|
||||
module('scrypt-1.4.0.jar', 'com.lambdaworks.scrypt', '1.4.0') {
|
||||
exports('com.lambdaworks.codec')
|
||||
exports('com.lambdaworks.crypto')
|
||||
}
|
||||
module('streamsupport-1.7.0.jar', 'net.sourceforge.streamsupport', '1.7.0') {
|
||||
requires('jdk.unsupported')
|
||||
exports('java8.util')
|
||||
exports('java8.util.function')
|
||||
exports('java8.util.stream')
|
||||
}
|
||||
module('protobuf-java-2.6.1.jar', 'com.google.protobuf', '2.6.1') {
|
||||
exports('com.google.protobuf')
|
||||
}
|
||||
module('commons-text-1.2.jar', 'org.apache.commons.text', '1.2') {
|
||||
exports('org.apache.commons.text')
|
||||
}
|
||||
|
|
|
@ -521,11 +521,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());
|
||||
}
|
||||
|
|
|
@ -120,6 +120,8 @@ public class MixStatusCell extends TreeTableCell<Entry, UtxoEntry.MixStatus> {
|
|||
tt.setText(status);
|
||||
setTooltip(tt);
|
||||
|
||||
// TODO nbRegisteredInputs is not available anymore
|
||||
/*
|
||||
if(mixProgress.getMixStep() == MixStep.REGISTERED_INPUT) {
|
||||
tt.setOnShowing(event -> {
|
||||
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(utxoEntry.getWallet());
|
||||
|
@ -131,7 +133,7 @@ public class MixStatusCell extends TreeTableCell<Entry, UtxoEntry.MixStatus> {
|
|||
});
|
||||
registeredInputsService.start();
|
||||
});
|
||||
}
|
||||
}*/
|
||||
} else {
|
||||
setGraphic(null);
|
||||
setTooltip(null);
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
package com.sparrowwallet.sparrow.net;
|
||||
|
||||
import com.google.common.net.HostAndPort;
|
||||
import com.samourai.wallet.api.backend.beans.HttpException;
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.nightjar.http.JavaHttpException;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
@ -158,7 +158,7 @@ public enum BroadcastSource {
|
|||
} catch(Exception e) {
|
||||
throw new BroadcastException("Could not retrieve txid from broadcast, server returned: " + response);
|
||||
}
|
||||
} catch(JavaHttpException e) {
|
||||
} catch(HttpException e) {
|
||||
throw new BroadcastException("Could not broadcast transaction, server returned " + e.getStatusCode() + ": " + e.getResponseBody());
|
||||
} catch(Exception e) {
|
||||
log.error("Could not post transaction via " + getName(), e);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package com.sparrowwallet.sparrow.net;
|
||||
|
||||
import com.sparrowwallet.nightjar.http.JavaHttpException;
|
||||
import com.samourai.wallet.api.backend.beans.HttpException;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.event.ExchangeRatesUpdatedEvent;
|
||||
import javafx.concurrent.ScheduledService;
|
||||
|
@ -107,7 +107,7 @@ public enum ExchangeSource {
|
|||
if(log.isDebugEnabled()) {
|
||||
log.warn("Error retrieving historical currency rates", e);
|
||||
} else {
|
||||
if(e instanceof JavaHttpException javaHttpException && javaHttpException.getStatusCode() == 404) {
|
||||
if(e instanceof HttpException httpException && httpException.getStatusCode() == 404) {
|
||||
log.warn("Error retrieving historical currency rates (" + e.getMessage() + "). BTC-" + currency.getCurrencyCode() + " may not be supported by " + this);
|
||||
} else {
|
||||
log.warn("Error retrieving historical currency rates (" + e.getMessage() + ")");
|
||||
|
|
|
@ -1,57 +1,56 @@
|
|||
package com.sparrowwallet.sparrow.net;
|
||||
|
||||
import com.google.common.net.HostAndPort;
|
||||
import com.samourai.http.client.HttpUsage;
|
||||
import com.samourai.http.client.IHttpClient;
|
||||
import com.sparrowwallet.nightjar.http.JavaHttpClientService;
|
||||
import com.samourai.http.client.JettyHttpClientService;
|
||||
import com.samourai.wallet.httpClient.HttpUsage;
|
||||
import com.samourai.wallet.httpClient.IHttpClient;
|
||||
import com.samourai.wallet.util.AsyncUtil;
|
||||
import com.samourai.whirlpool.client.utils.ClientUtils;
|
||||
import io.reactivex.Observable;
|
||||
import java8.util.Optional;
|
||||
import javafx.concurrent.Service;
|
||||
import javafx.concurrent.Task;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
public class HttpClientService {
|
||||
private final JavaHttpClientService httpClientService;
|
||||
public class HttpClientService extends JettyHttpClientService {
|
||||
private static final int REQUEST_TIMEOUT = 120000;
|
||||
|
||||
public HttpClientService(HostAndPort torProxy) {
|
||||
this.httpClientService = new JavaHttpClientService(torProxy, 120000);
|
||||
super(REQUEST_TIMEOUT,
|
||||
ClientUtils.USER_AGENT,
|
||||
new HttpProxySupplier(torProxy));
|
||||
}
|
||||
|
||||
public <T> T requestJson(String url, Class<T> responseType, Map<String, String> headers) throws Exception {
|
||||
IHttpClient httpClient = httpClientService.getHttpClient(HttpUsage.COORDINATOR_REST);
|
||||
return httpClient.getJson(url, responseType, headers);
|
||||
return getHttpClient(HttpUsage.BACKEND)
|
||||
.getJson(url, responseType, headers);
|
||||
}
|
||||
|
||||
public <T> Observable<Optional<T>> postJson(String url, Class<T> responseType, Map<String, String> headers, Object body) {
|
||||
IHttpClient httpClient = httpClientService.getHttpClient(HttpUsage.COORDINATOR_REST);
|
||||
return httpClient.postJson(url, responseType, headers, body);
|
||||
return getHttpClient(HttpUsage.BACKEND)
|
||||
.postJson(url, responseType, headers, body).toObservable();
|
||||
}
|
||||
|
||||
public String postString(String url, Map<String, String> headers, String contentType, String content) throws Exception {
|
||||
IHttpClient httpClient = httpClientService.getHttpClient(HttpUsage.COORDINATOR_REST);
|
||||
return httpClient.postString(url, headers, contentType, content);
|
||||
}
|
||||
|
||||
public void changeIdentity() {
|
||||
HostAndPort torProxy = getTorProxy();
|
||||
if(torProxy != null) {
|
||||
TorUtils.changeIdentity(torProxy);
|
||||
}
|
||||
IHttpClient httpClient = getHttpClient(HttpUsage.BACKEND);
|
||||
return AsyncUtil.getInstance().blockingGet(
|
||||
httpClient.postString(url, headers, contentType, content)).get();
|
||||
}
|
||||
|
||||
public HostAndPort getTorProxy() {
|
||||
return httpClientService.getTorProxy();
|
||||
return getHttpProxySupplier().getTorProxy();
|
||||
}
|
||||
|
||||
public void setTorProxy(HostAndPort torProxy) {
|
||||
//Ensure all http clients are shutdown first
|
||||
httpClientService.shutdown();
|
||||
httpClientService.setTorProxy(torProxy);
|
||||
stop();
|
||||
getHttpProxySupplier()._setTorProxy(torProxy);
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
httpClientService.shutdown();
|
||||
@Override
|
||||
public HttpProxySupplier getHttpProxySupplier() {
|
||||
return (HttpProxySupplier)super.getHttpProxySupplier();
|
||||
}
|
||||
|
||||
public static class ShutdownService extends Service<Boolean> {
|
||||
|
@ -65,7 +64,7 @@ public class HttpClientService {
|
|||
protected Task<Boolean> createTask() {
|
||||
return new Task<>() {
|
||||
protected Boolean call() throws Exception {
|
||||
httpClientService.shutdown();
|
||||
httpClientService.stop();
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
package com.sparrowwallet.sparrow.net;
|
||||
|
||||
import com.google.common.net.HostAndPort;
|
||||
import com.samourai.http.client.IHttpProxySupplier;
|
||||
import com.samourai.wallet.httpClient.HttpProxy;
|
||||
import com.samourai.wallet.httpClient.HttpProxyProtocol;
|
||||
import com.samourai.wallet.httpClient.HttpUsage;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public class HttpProxySupplier implements IHttpProxySupplier {
|
||||
private HostAndPort torProxy;
|
||||
private HttpProxy httpProxy;
|
||||
|
||||
public HttpProxySupplier(HostAndPort torProxy) {
|
||||
this.torProxy = torProxy;
|
||||
this.httpProxy = computeHttpProxy(torProxy);
|
||||
}
|
||||
|
||||
private HttpProxy computeHttpProxy(HostAndPort hostAndPort) {
|
||||
if (hostAndPort == null) {
|
||||
return null;
|
||||
}
|
||||
// TODO verify
|
||||
return new HttpProxy(HttpProxyProtocol.SOCKS, hostAndPort.getHost(), hostAndPort.getPort());
|
||||
}
|
||||
|
||||
public HostAndPort getTorProxy() {
|
||||
return torProxy;
|
||||
}
|
||||
|
||||
// shouldnt call directly but use httpClientService.setTorProxy()
|
||||
public void _setTorProxy(HostAndPort hostAndPort) {
|
||||
// set proxy
|
||||
this.torProxy = hostAndPort;
|
||||
this.httpProxy = computeHttpProxy(hostAndPort);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<HttpProxy> getHttpProxy(HttpUsage httpUsage) {
|
||||
return Optional.ofNullable(httpProxy);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changeIdentity() {
|
||||
HostAndPort torProxy = getTorProxy();
|
||||
if(torProxy != null) {
|
||||
TorUtils.changeIdentity(torProxy);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@ package com.sparrowwallet.sparrow.payjoin;
|
|||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.gson.Gson;
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.samourai.wallet.api.backend.beans.HttpException;
|
||||
import com.sparrowwallet.drongo.protocol.Script;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.protocol.TransactionInput;
|
||||
|
@ -14,7 +14,6 @@ import com.sparrowwallet.drongo.psbt.PSBTParseException;
|
|||
import com.sparrowwallet.drongo.uri.BitcoinURI;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletNode;
|
||||
import com.sparrowwallet.nightjar.http.JavaHttpException;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.net.HttpClientService;
|
||||
import com.sparrowwallet.sparrow.net.Protocol;
|
||||
|
@ -23,7 +22,8 @@ import javafx.concurrent.Task;
|
|||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.*;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.*;
|
||||
|
@ -87,7 +87,7 @@ public class Payjoin {
|
|||
checkProposal(psbt, proposalPsbt, changeOutputIndex, maxAdditionalFeeContribution, allowOutputSubstitution);
|
||||
|
||||
return proposalPsbt;
|
||||
} catch(JavaHttpException e) {
|
||||
} catch(HttpException e) {
|
||||
Gson gson = new Gson();
|
||||
PayjoinReceiverError payjoinReceiverError = gson.fromJson(e.getResponseBody(), PayjoinReceiverError.class);
|
||||
log.warn("Payjoin receiver returned an error of " + payjoinReceiverError.getErrorCode() + " (" + payjoinReceiverError.getMessage() + ")");
|
||||
|
|
|
@ -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,18 +1,24 @@
|
|||
package com.sparrowwallet.sparrow.soroban;
|
||||
|
||||
import com.samourai.soroban.cahoots.CahootsContext;
|
||||
import com.samourai.soroban.client.cahoots.OnlineCahootsMessage;
|
||||
import com.samourai.soroban.client.cahoots.SorobanCahootsService;
|
||||
import com.samourai.soroban.client.meeting.SorobanRequestMessage;
|
||||
import com.samourai.soroban.client.wallet.SorobanWalletService;
|
||||
import com.samourai.soroban.client.wallet.counterparty.SorobanWalletCounterparty;
|
||||
import com.samourai.wallet.bip47.rpc.PaymentCode;
|
||||
import com.samourai.wallet.cahoots.Cahoots;
|
||||
import com.samourai.wallet.cahoots.CahootsContext;
|
||||
import com.samourai.wallet.cahoots.CahootsType;
|
||||
import com.samourai.wallet.cahoots.CahootsWallet;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.psbt.PSBTParseException;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletNode;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.control.*;
|
||||
import com.sparrowwallet.sparrow.paynym.PayNymDialog;
|
||||
import com.sparrowwallet.sparrow.paynym.PayNymService;
|
||||
import io.reactivex.functions.Consumer;
|
||||
import io.reactivex.rxjavafx.schedulers.JavaFxScheduler;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import javafx.application.Platform;
|
||||
|
@ -226,40 +232,39 @@ public class CounterpartyController extends SorobanController {
|
|||
|
||||
private void startCounterpartyMeetingReceive() {
|
||||
Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId);
|
||||
SparrowCahootsWallet counterpartyCahootsWallet = soroban.getCahootsWallet(wallet, 1);
|
||||
SorobanWalletService sorobanWalletService = soroban.getSorobanWalletService();
|
||||
|
||||
SparrowCahootsWallet cahootsWallet = soroban.getCahootsWallet(wallet);
|
||||
SorobanWalletCounterparty sorobanWalletCounterparty = sorobanWalletService.getSorobanWalletCounterparty(cahootsWallet);
|
||||
sorobanWalletCounterparty.setTimeoutMeetingMs(TIMEOUT_MS);
|
||||
|
||||
try {
|
||||
SorobanCahootsService sorobanMeetingService = soroban.getSorobanCahootsService(counterpartyCahootsWallet);
|
||||
sorobanMeetingService.receiveMeetingRequest(TIMEOUT_MS)
|
||||
// TODO run in background thread?
|
||||
SorobanRequestMessage requestMessage = sorobanWalletCounterparty.receiveMeetingRequest();
|
||||
|
||||
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, soroban.getBip47Account());
|
||||
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);
|
||||
log.error("Failed to receive meeting request", e);
|
||||
mixingPartner.setVisible(false);
|
||||
requestUserAttention();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -290,45 +295,46 @@ 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)
|
||||
CahootsContext cahootsContext = CahootsContext.newCounterparty(cahootsWallet, cahootsType, account);
|
||||
Consumer<OnlineCahootsMessage> onProgress = cahootsMessage -> {
|
||||
if(cahootsMessage != null) {
|
||||
Cahoots cahoots = cahootsMessage.getCahoots();
|
||||
sorobanProgressBar.setProgress((double)(cahoots.getStep() + 1) / 5);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
sorobanWalletCounterparty.counterparty(cahootsContext, initiatorPaymentCode, onProgress)
|
||||
.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) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
.subscribe(cahoots -> {
|
||||
// cahoots success
|
||||
}, error -> {
|
||||
log.error("Error creating mix transaction", error);
|
||||
String cutFrom = "Exception: ";
|
||||
|
|
|
@ -1,12 +1,17 @@
|
|||
package com.sparrowwallet.sparrow.soroban;
|
||||
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.samourai.soroban.cahoots.CahootsContext;
|
||||
import com.samourai.soroban.client.OnlineSorobanInteraction;
|
||||
import com.samourai.soroban.client.meeting.SorobanResponseMessage;
|
||||
import com.samourai.soroban.client.wallet.SorobanWalletService;
|
||||
import com.samourai.soroban.client.wallet.sender.CahootsSorobanInitiatorListener;
|
||||
import com.samourai.wallet.cahoots.CahootsContext;
|
||||
import com.samourai.soroban.client.cahoots.OnlineCahootsMessage;
|
||||
import com.samourai.soroban.client.cahoots.SorobanCahootsService;
|
||||
import com.samourai.wallet.bip47.rpc.PaymentCode;
|
||||
import com.samourai.wallet.cahoots.Cahoots;
|
||||
import com.samourai.wallet.cahoots.CahootsType;
|
||||
import com.samourai.wallet.cahoots.TxBroadcastInteraction;
|
||||
import com.samourai.wallet.sorobanClient.SorobanInteraction;
|
||||
import com.sparrowwallet.drongo.SecureString;
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
import com.sparrowwallet.drongo.crypto.EncryptionType;
|
||||
|
@ -62,7 +67,6 @@ import java.util.function.UnaryOperator;
|
|||
|
||||
import static com.sparrowwallet.sparrow.AppServices.showErrorDialog;
|
||||
import static com.sparrowwallet.sparrow.paynym.PayNymController.PAYNYM_REGEX;
|
||||
import static com.sparrowwallet.sparrow.soroban.Soroban.TIMEOUT_MS;
|
||||
|
||||
public class InitiatorController extends SorobanController {
|
||||
private static final Logger log = LoggerFactory.getLogger(InitiatorController.class);
|
||||
|
@ -199,7 +203,7 @@ public class InitiatorController extends SorobanController {
|
|||
meetingFail.setVisible(false);
|
||||
step2Desc.setText("Retrying...");
|
||||
sorobanProgressLabel.setVisible(true);
|
||||
startInitiatorMeetingRequest(AppServices.getSorobanServices().getSoroban(walletId), wallet);
|
||||
startInitiatorMeetAndInitiate(AppServices.getSorobanServices().getSoroban(walletId), wallet);
|
||||
step2Timer.start();
|
||||
});
|
||||
|
||||
|
@ -221,7 +225,7 @@ public class InitiatorController extends SorobanController {
|
|||
|
||||
step2.visibleProperty().addListener((observable, oldValue, visible) -> {
|
||||
if(visible) {
|
||||
startInitiatorMeetingRequest();
|
||||
startInitiatorMeetAndInitiate();
|
||||
step2Timer.start();
|
||||
}
|
||||
});
|
||||
|
@ -358,7 +362,7 @@ public class InitiatorController extends SorobanController {
|
|||
});
|
||||
}
|
||||
|
||||
private void startInitiatorMeetingRequest() {
|
||||
private void startInitiatorMeetAndInitiate() {
|
||||
Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId);
|
||||
if(soroban.getHdWallet() == null) {
|
||||
if(wallet.isEncrypted()) {
|
||||
|
@ -377,7 +381,7 @@ public class InitiatorController extends SorobanController {
|
|||
|
||||
try {
|
||||
soroban.setHDWallet(copy);
|
||||
startInitiatorMeetingRequest(soroban, wallet);
|
||||
startInitiatorMeetAndInitiate(soroban, wallet);
|
||||
} finally {
|
||||
key.clear();
|
||||
encryptionFullKey.clear();
|
||||
|
@ -389,7 +393,7 @@ public class InitiatorController extends SorobanController {
|
|||
if(keyDerivationService.getException() instanceof InvalidPasswordException) {
|
||||
Optional<ButtonType> optResponse = showErrorDialog("Invalid Password", "The wallet password was invalid. Try again?", ButtonType.CANCEL, ButtonType.OK);
|
||||
if(optResponse.isPresent() && optResponse.get().equals(ButtonType.OK)) {
|
||||
Platform.runLater(this::startInitiatorMeetingRequest);
|
||||
Platform.runLater(this::startInitiatorMeetAndInitiate);
|
||||
}
|
||||
} else {
|
||||
log.error("Error deriving wallet key", keyDerivationService.getException());
|
||||
|
@ -403,54 +407,104 @@ 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, soroban.getBip47Account(), feePerB, payment.getAmount(), payment.getAddress().getAddress(), paymentCodeCounterparty.toString());
|
||||
|
||||
CahootsSorobanInitiatorListener listener = new CahootsSorobanInitiatorListener() {
|
||||
@Override
|
||||
public void onResponse(SorobanResponseMessage sorobanResponse) throws Exception {
|
||||
super.onResponse(sorobanResponse);
|
||||
|
||||
requestUserAttention();
|
||||
if(sorobanResponse.isAccept()) {
|
||||
sorobanProgressBar.setProgress(0.1);
|
||||
sorobanProgressLabel.setText("Mix partner accepted!");
|
||||
} 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) {
|
||||
Boolean accepted = (Boolean)Platform.enterNestedEventLoop(transactionAccepted);
|
||||
if(accepted) {
|
||||
interaction.sorobanAccept();
|
||||
} else {
|
||||
interaction.sorobanReject("Mix partner declined to broadcast the transaction.");
|
||||
}
|
||||
} else {
|
||||
throw new Exception("Unknown interaction: "+originInteraction.getTypeInteraction());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void progress(OnlineCahootsMessage message) {
|
||||
super.progress(message);
|
||||
|
||||
OnlineCahootsMessage cahootsMessage = (OnlineCahootsMessage)message;
|
||||
if(cahootsMessage != null) {
|
||||
Cahoots cahoots = cahootsMessage.getCahoots();
|
||||
sorobanProgressBar.setProgress((double)(cahoots.getStep() + 1) / 5);
|
||||
|
||||
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...");
|
||||
sorobanWalletService.getSorobanWalletInitiator(cahootsWallet).meetAndInitiate(cahootsContext, paymentCodeCounterparty, listener)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(JavaFxScheduler.platform())
|
||||
.subscribe(sorobanResponse -> {
|
||||
}, error -> {
|
||||
log.error("Error receiving meeting response", error);
|
||||
step2Desc.setText(getErrorMessage(error));
|
||||
sorobanProgressLabel.setVisible(false);
|
||||
meetingFail.setVisible(true);
|
||||
requestUserAttention();
|
||||
});
|
||||
}, error -> {
|
||||
log.error("Could not retrieve payment code", error);
|
||||
if(error.getMessage().endsWith("404")) {
|
||||
|
@ -464,77 +518,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);
|
||||
|
||||
|
|
|
@ -1,48 +1,37 @@
|
|||
package com.sparrowwallet.sparrow.soroban;
|
||||
|
||||
import com.google.common.net.HostAndPort;
|
||||
import com.samourai.http.client.HttpUsage;
|
||||
import com.samourai.http.client.IHttpClient;
|
||||
import com.samourai.soroban.client.SorobanServer;
|
||||
import com.samourai.soroban.client.cahoots.SorobanCahootsService;
|
||||
import com.samourai.soroban.client.rpc.RpcClient;
|
||||
import com.samourai.wallet.bip47.rpc.java.Bip47UtilJava;
|
||||
import com.samourai.wallet.cahoots.CahootsWallet;
|
||||
import com.samourai.soroban.client.SorobanConfig;
|
||||
import com.samourai.soroban.client.wallet.SorobanWalletService;
|
||||
import com.samourai.wallet.chain.ChainSupplier;
|
||||
import com.samourai.wallet.hd.HD_Wallet;
|
||||
import com.samourai.wallet.hd.HD_WalletFactoryGeneric;
|
||||
import com.sparrowwallet.drongo.Drongo;
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.nightjar.http.JavaHttpClientService;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
|
||||
import com.sparrowwallet.sparrow.whirlpool.dataSource.SparrowChainSupplier;
|
||||
import javafx.concurrent.Service;
|
||||
import javafx.concurrent.Task;
|
||||
import org.bitcoinj.core.NetworkParameters;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.security.Provider;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
|
||||
public class Soroban {
|
||||
private static final Logger log = LoggerFactory.getLogger(Soroban.class);
|
||||
|
||||
protected static final HD_WalletFactoryGeneric hdWalletFactory = HD_WalletFactoryGeneric.getInstance();
|
||||
protected static final Bip47UtilJava bip47Util = Bip47UtilJava.getInstance();
|
||||
protected static final Provider PROVIDER_JAVA = Drongo.getProvider();
|
||||
protected static final int TIMEOUT_MS = 60000;
|
||||
public static final List<Network> SOROBAN_NETWORKS = List.of(Network.MAINNET, Network.TESTNET);
|
||||
|
||||
private final SorobanServer sorobanServer;
|
||||
private final JavaHttpClientService httpClientService;
|
||||
private final SorobanWalletService sorobanWalletService;
|
||||
|
||||
private HD_Wallet hdWallet;
|
||||
private int bip47Account;
|
||||
|
||||
public Soroban(Network network, HostAndPort torProxy) {
|
||||
this.sorobanServer = SorobanServer.valueOf(network.getName().toUpperCase(Locale.ROOT));
|
||||
this.httpClientService = new JavaHttpClientService(torProxy);
|
||||
public Soroban() {
|
||||
SorobanConfig sorobanConfig = AppServices.getWhirlpoolServices().getSorobanConfig();
|
||||
this.sorobanWalletService = sorobanConfig.getSorobanWalletService();
|
||||
}
|
||||
|
||||
public HD_Wallet getHdWallet() {
|
||||
|
@ -50,25 +39,12 @@ public class Soroban {
|
|||
}
|
||||
|
||||
public void setHDWallet(Wallet wallet) {
|
||||
if(wallet.isEncrypted()) {
|
||||
throw new IllegalStateException("Wallet cannot be encrypted");
|
||||
}
|
||||
|
||||
try {
|
||||
Keystore keystore = wallet.getKeystores().get(0);
|
||||
ScriptType scriptType = wallet.getScriptType();
|
||||
int purpose = scriptType.getDefaultDerivation().get(0).num();
|
||||
List<String> words = keystore.getSeed().getMnemonicCode();
|
||||
String passphrase = keystore.getSeed().getPassphrase() == null ? "" : keystore.getSeed().getPassphrase().asString();
|
||||
byte[] seed = hdWalletFactory.computeSeedFromWords(words);
|
||||
hdWallet = new HD_Wallet(purpose, new ArrayList<>(words), sorobanServer.getParams(), seed, passphrase);
|
||||
bip47Account = wallet.isMasterWallet() ? wallet.getAccountIndex() : wallet.getMasterWallet().getAccountIndex();
|
||||
} catch(Exception e) {
|
||||
throw new IllegalStateException("Could not create Soroban HD wallet ", e);
|
||||
}
|
||||
NetworkParameters params = sorobanWalletService.getSorobanService().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,8 @@ public class Soroban {
|
|||
}
|
||||
|
||||
try {
|
||||
return new SparrowCahootsWallet(wallet, hdWallet, bip47Account, sorobanServer, (long)feeRate);
|
||||
ChainSupplier chainSupplier = new SparrowChainSupplier(wallet.getStoredBlockHeight());
|
||||
return new SparrowCahootsWallet(chainSupplier, wallet, hdWallet, bip47Account);
|
||||
} catch(Exception e) {
|
||||
log.error("Could not create cahoots wallet", e);
|
||||
}
|
||||
|
@ -95,24 +72,16 @@ public class Soroban {
|
|||
return null;
|
||||
}
|
||||
|
||||
public SorobanCahootsService getSorobanCahootsService(CahootsWallet cahootsWallet) {
|
||||
IHttpClient httpClient = httpClientService.getHttpClient(HttpUsage.COORDINATOR_REST);
|
||||
RpcClient rpcClient = new RpcClient(httpClient, httpClientService.getTorProxy() != null, sorobanServer.getParams());
|
||||
return new SorobanCahootsService(bip47Util, PROVIDER_JAVA, cahootsWallet, rpcClient);
|
||||
public int getBip47Account() {
|
||||
return bip47Account;
|
||||
}
|
||||
|
||||
public HostAndPort getTorProxy() {
|
||||
return httpClientService.getTorProxy();
|
||||
public SorobanWalletService getSorobanWalletService() {
|
||||
return sorobanWalletService;
|
||||
}
|
||||
|
||||
public void setTorProxy(HostAndPort torProxy) {
|
||||
//Ensure all http clients are shutdown first
|
||||
httpClientService.shutdown();
|
||||
httpClientService.setTorProxy(torProxy);
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
httpClientService.shutdown();
|
||||
public void stop() {
|
||||
AppServices.getHttpClientService().stop();
|
||||
}
|
||||
|
||||
public static class ShutdownService extends Service<Boolean> {
|
||||
|
@ -126,7 +95,7 @@ public class Soroban {
|
|||
protected Task<Boolean> createTask() {
|
||||
return new Task<>() {
|
||||
protected Boolean call() throws Exception {
|
||||
soroban.shutdown();
|
||||
soroban.stop();
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
package com.sparrowwallet.sparrow.soroban;
|
||||
|
||||
import com.samourai.soroban.client.SorobanServer;
|
||||
import com.samourai.wallet.api.backend.beans.UnspentOutput;
|
||||
import com.samourai.wallet.bip47.rpc.BIP47Wallet;
|
||||
import com.samourai.wallet.bip47.rpc.PaymentAddress;
|
||||
import com.samourai.wallet.bip47.rpc.PaymentCode;
|
||||
import com.samourai.wallet.bip47.rpc.java.Bip47UtilJava;
|
||||
import com.samourai.wallet.bipFormat.BipFormat;
|
||||
import com.samourai.wallet.cahoots.AbstractCahootsWallet;
|
||||
import com.samourai.wallet.cahoots.CahootsUtxo;
|
||||
import com.samourai.wallet.cahoots.SimpleCahootsWallet;
|
||||
import com.samourai.wallet.chain.ChainSupplier;
|
||||
import com.samourai.wallet.hd.HD_Address;
|
||||
import com.samourai.wallet.hd.HD_Wallet;
|
||||
import com.samourai.wallet.send.MyTransactionOutPoint;
|
||||
|
@ -17,21 +19,25 @@ import com.sparrowwallet.drongo.wallet.StandardAccount;
|
|||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletNode;
|
||||
import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
import org.bitcoinj.core.NetworkParameters;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class SparrowCahootsWallet extends SimpleCahootsWallet {
|
||||
public class SparrowCahootsWallet extends AbstractCahootsWallet {
|
||||
private final Wallet wallet;
|
||||
private HD_Wallet bip84w;
|
||||
private final int account;
|
||||
private final int bip47Account;
|
||||
private List<CahootsUtxo> utxos;
|
||||
|
||||
public SparrowCahootsWallet(Wallet wallet, HD_Wallet bip84w, int bip47Account, SorobanServer sorobanServer, long feePerB) throws Exception {
|
||||
super(bip84w, sorobanServer.getParams(), wallet.getFreshNode(KeyPurpose.CHANGE).getIndex(), feePerB);
|
||||
public SparrowCahootsWallet(ChainSupplier chainSupplier, Wallet wallet, HD_Wallet bip84w, int bip47Account) {
|
||||
super(chainSupplier, bip84w.getFingerprint(),
|
||||
new BIP47Wallet(bip84w).getAccount(bip47Account));
|
||||
this.wallet = wallet;
|
||||
this.bip84w = bip84w;
|
||||
this.account = wallet.getAccountIndex();
|
||||
this.bip47Account = bip47Account;
|
||||
this.utxos = new LinkedList<>();
|
||||
|
||||
bip84w.getAccount(account).getReceive().setAddrIdx(wallet.getFreshNode(KeyPurpose.RECEIVE).getIndex());
|
||||
bip84w.getAccount(account).getChange().setAddrIdx(wallet.getFreshNode(KeyPurpose.CHANGE).getIndex());
|
||||
|
||||
|
@ -42,32 +48,6 @@ public class SparrowCahootsWallet extends SimpleCahootsWallet {
|
|||
}
|
||||
}
|
||||
|
||||
public void addUtxo(WalletNode node, BlockTransaction blockTransaction, int index) {
|
||||
if(node.getWallet().getScriptType() != ScriptType.P2WPKH) {
|
||||
return;
|
||||
}
|
||||
|
||||
UnspentOutput unspentOutput = Whirlpool.getUnspentOutput(node, blockTransaction, index);
|
||||
MyTransactionOutPoint myTransactionOutPoint = unspentOutput.computeOutpoint(getParams());
|
||||
|
||||
CahootsUtxo cahootsUtxo;
|
||||
if(node.getWallet().isBip47()) {
|
||||
try {
|
||||
String strPaymentCode = node.getWallet().getKeystores().get(0).getExternalPaymentCode().toString();
|
||||
HD_Address hdAddress = getBip47Wallet().getAccount(getBip47Account()).addressAt(node.getIndex());
|
||||
PaymentAddress paymentAddress = Bip47UtilJava.getInstance().getPaymentAddress(new PaymentCode(strPaymentCode), 0, hdAddress, getParams());
|
||||
cahootsUtxo = new CahootsUtxo(myTransactionOutPoint, node.getDerivationPath(), paymentAddress.getReceiveECKey());
|
||||
} catch(Exception e) {
|
||||
throw new IllegalStateException("Cannot add BIP47 UTXO", e);
|
||||
}
|
||||
} else {
|
||||
HD_Address hdAddress = getBip84Wallet().getAddressAt(account, unspentOutput);
|
||||
cahootsUtxo = new CahootsUtxo(myTransactionOutPoint, node.getDerivationPath(), hdAddress.getECKey());
|
||||
}
|
||||
|
||||
addUtxo(account, cahootsUtxo);
|
||||
}
|
||||
|
||||
public Wallet getWallet() {
|
||||
return wallet;
|
||||
}
|
||||
|
@ -77,28 +57,27 @@ public class SparrowCahootsWallet extends SimpleCahootsWallet {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected List<CahootsUtxo> fetchUtxos(int account) {
|
||||
List<CahootsUtxo> utxos = super.fetchUtxos(account);
|
||||
if(utxos == null) {
|
||||
utxos = new LinkedList<>();
|
||||
protected String doFetchAddressReceive(int account, boolean increment, BipFormat bipFormat) throws Exception {
|
||||
if(account == StandardAccount.WHIRLPOOL_POSTMIX.getAccountNumber()) {
|
||||
// force change chain
|
||||
return getAddress(account, KeyPurpose.CHANGE);
|
||||
}
|
||||
|
||||
return getAddress(account, KeyPurpose.RECEIVE);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String doFetchAddressChange(int account, boolean increment, BipFormat bipFormat) throws Exception {
|
||||
return getAddress(account, KeyPurpose.CHANGE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CahootsUtxo> getUtxosWpkhByAccount(int account) {
|
||||
return utxos;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Pair<Integer, Integer> fetchReceiveIndex(int account) throws Exception {
|
||||
if(account == StandardAccount.WHIRLPOOL_POSTMIX.getAccountNumber()) {
|
||||
// force change chain
|
||||
return Pair.of(getWallet(account).getFreshNode(KeyPurpose.CHANGE).getIndex(), 1);
|
||||
}
|
||||
|
||||
return Pair.of(getWallet(account).getFreshNode(KeyPurpose.RECEIVE).getIndex(), 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Pair<Integer, Integer> fetchChangeIndex(int account) throws Exception {
|
||||
return Pair.of(getWallet(account).getFreshNode(KeyPurpose.CHANGE).getIndex(), 1);
|
||||
private String getAddress(int account, KeyPurpose keyPurpose) {
|
||||
return getWallet(account).getFreshNode(keyPurpose).getAddress().getAddress();
|
||||
}
|
||||
|
||||
private Wallet getWallet(int account) {
|
||||
|
@ -108,9 +87,30 @@ public class SparrowCahootsWallet extends SimpleCahootsWallet {
|
|||
|
||||
return wallet;
|
||||
}
|
||||
public void addUtxo(WalletNode node, BlockTransaction blockTransaction, int index) {
|
||||
if(node.getWallet().getScriptType() != ScriptType.P2WPKH) {
|
||||
return;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBip47Account() {
|
||||
return bip47Account;
|
||||
NetworkParameters params = getBip47Account().getParams();
|
||||
UnspentOutput unspentOutput = Whirlpool.getUnspentOutput(node, blockTransaction, index);
|
||||
MyTransactionOutPoint myTransactionOutPoint = unspentOutput.computeOutpoint(params);
|
||||
|
||||
CahootsUtxo cahootsUtxo;
|
||||
if(node.getWallet().isBip47()) {
|
||||
try {
|
||||
String strPaymentCode = node.getWallet().getKeystores().get(0).getExternalPaymentCode().toString();
|
||||
HD_Address hdAddress = getBip47Account().addressAt(node.getIndex());
|
||||
PaymentAddress paymentAddress = Bip47UtilJava.getInstance().getPaymentAddress(new PaymentCode(strPaymentCode), 0, hdAddress, params);
|
||||
cahootsUtxo = new CahootsUtxo(myTransactionOutPoint, node.getDerivationPath(), null, paymentAddress.getReceiveECKey().getPrivKeyBytes());
|
||||
} catch(Exception e) {
|
||||
throw new IllegalStateException("Cannot add BIP47 UTXO", e);
|
||||
}
|
||||
} else {
|
||||
HD_Address hdAddress = bip84w.getAddressAt(index, unspentOutput);
|
||||
cahootsUtxo = new CahootsUtxo(myTransactionOutPoint, node.getDerivationPath(), null, hdAddress.getECKey().getPrivKeyBytes());
|
||||
}
|
||||
|
||||
utxos.add(cahootsUtxo);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,44 +1,55 @@
|
|||
package com.sparrowwallet.sparrow.whirlpool;
|
||||
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.google.common.net.HostAndPort;
|
||||
import com.samourai.tor.client.TorClientService;
|
||||
import com.samourai.soroban.client.SorobanConfig;
|
||||
import com.samourai.wallet.api.backend.beans.UnspentOutput;
|
||||
import com.samourai.wallet.bipFormat.BIP_FORMAT;
|
||||
import com.samourai.wallet.constants.BIP_WALLETS;
|
||||
import com.samourai.wallet.constants.SamouraiAccount;
|
||||
import com.samourai.wallet.constants.SamouraiNetwork;
|
||||
import com.samourai.wallet.hd.HD_Wallet;
|
||||
import com.samourai.wallet.hd.HD_WalletFactoryGeneric;
|
||||
import com.samourai.wallet.util.AsyncUtil;
|
||||
import com.samourai.wallet.util.FormatsUtilGeneric;
|
||||
import com.samourai.whirlpool.client.event.*;
|
||||
import com.samourai.whirlpool.client.mix.handler.IPostmixHandler;
|
||||
import com.samourai.whirlpool.client.tx0.*;
|
||||
import com.samourai.whirlpool.client.tx0.Tx0;
|
||||
import com.samourai.whirlpool.client.tx0.Tx0Config;
|
||||
import com.samourai.whirlpool.client.tx0.Tx0PreviewService;
|
||||
import com.samourai.whirlpool.client.tx0.Tx0Previews;
|
||||
import com.samourai.whirlpool.client.wallet.WhirlpoolEventService;
|
||||
import com.samourai.whirlpool.client.wallet.WhirlpoolWallet;
|
||||
import com.samourai.whirlpool.client.wallet.WhirlpoolWalletConfig;
|
||||
import com.samourai.whirlpool.client.wallet.WhirlpoolWalletService;
|
||||
import com.samourai.whirlpool.client.wallet.beans.*;
|
||||
import com.samourai.whirlpool.client.wallet.data.WhirlpoolInfo;
|
||||
import com.samourai.whirlpool.client.wallet.data.coordinator.CoordinatorSupplier;
|
||||
import com.samourai.whirlpool.client.wallet.data.dataPersister.DataPersisterFactory;
|
||||
import com.samourai.whirlpool.client.wallet.data.dataSource.DataSourceConfig;
|
||||
import com.samourai.whirlpool.client.wallet.data.dataSource.DataSourceFactory;
|
||||
import com.samourai.whirlpool.client.wallet.data.pool.ExpirablePoolSupplier;
|
||||
import com.samourai.whirlpool.client.wallet.data.utxo.UtxoSupplier;
|
||||
import com.samourai.whirlpool.client.wallet.data.utxoConfig.UtxoConfig;
|
||||
import com.samourai.whirlpool.client.whirlpool.ServerApi;
|
||||
import com.samourai.whirlpool.client.whirlpool.beans.Pool;
|
||||
import com.sparrowwallet.drongo.ExtendedKey;
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.protocol.*;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.protocol.TransactionInput;
|
||||
import com.sparrowwallet.drongo.protocol.TransactionOutput;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.nightjar.http.JavaHttpClientService;
|
||||
import com.sparrowwallet.nightjar.stomp.JavaStompClientService;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.WhirlpoolMixEvent;
|
||||
import com.sparrowwallet.sparrow.event.WhirlpoolMixSuccessEvent;
|
||||
import com.sparrowwallet.sparrow.wallet.UtxoEntry;
|
||||
import com.sparrowwallet.sparrow.whirlpool.dataPersister.SparrowDataPersister;
|
||||
import com.sparrowwallet.sparrow.whirlpool.dataSource.SparrowChainSupplier;
|
||||
import com.sparrowwallet.sparrow.whirlpool.dataSource.SparrowDataSource;
|
||||
import com.sparrowwallet.sparrow.whirlpool.dataSource.SparrowMinerFeeSupplier;
|
||||
import com.sparrowwallet.sparrow.whirlpool.dataSource.SparrowPostmixHandler;
|
||||
import com.sparrowwallet.sparrow.whirlpool.tor.SparrowTorClientService;
|
||||
import io.reactivex.rxjavafx.schedulers.JavaFxScheduler;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
|
@ -46,6 +57,7 @@ import javafx.concurrent.ScheduledService;
|
|||
import javafx.concurrent.Service;
|
||||
import javafx.concurrent.Task;
|
||||
import javafx.util.Duration;
|
||||
import org.bitcoinj.core.NetworkParameters;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
|
@ -58,17 +70,11 @@ public class Whirlpool {
|
|||
public static final List<Network> WHIRLPOOL_NETWORKS = List.of(Network.MAINNET, Network.TESTNET);
|
||||
public static final int DEFAULT_MIXTO_MIN_MIXES = 3;
|
||||
public static final int DEFAULT_MIXTO_RANDOM_FACTOR = 4;
|
||||
protected static final int TIMEOUT_MS = 60000;
|
||||
|
||||
private final WhirlpoolServer whirlpoolServer;
|
||||
private final JavaHttpClientService httpClientService;
|
||||
private final JavaStompClientService stompClientService;
|
||||
private final TorClientService torClientService;
|
||||
|
||||
private final WhirlpoolWalletService whirlpoolWalletService;
|
||||
private final WhirlpoolWalletConfig config;
|
||||
private final Tx0ParamService tx0ParamService;
|
||||
private final ExpirablePoolSupplier poolSupplier;
|
||||
private final Tx0Service tx0Service;
|
||||
private WhirlpoolInfo whirlpoolInfo;
|
||||
private Tx0FeeTarget tx0FeeTarget = Tx0FeeTarget.BLOCKS_4;
|
||||
private Tx0FeeTarget mixFeeTarget = Tx0FeeTarget.BLOCKS_4;
|
||||
private HD_Wallet hdWallet;
|
||||
|
@ -83,64 +89,63 @@ public class Whirlpool {
|
|||
private final BooleanProperty stoppingProperty = new SimpleBooleanProperty(false);
|
||||
private final BooleanProperty mixingProperty = new SimpleBooleanProperty(false);
|
||||
|
||||
public Whirlpool(Network network, HostAndPort torProxy) {
|
||||
this.whirlpoolServer = WhirlpoolServer.valueOf(network.getName().toUpperCase(Locale.ROOT));
|
||||
this.httpClientService = new JavaHttpClientService(torProxy, TIMEOUT_MS);
|
||||
this.stompClientService = new JavaStompClientService(httpClientService);
|
||||
this.torClientService = new SparrowTorClientService(this);
|
||||
|
||||
public Whirlpool() {
|
||||
this.whirlpoolWalletService = new WhirlpoolWalletService();
|
||||
this.config = computeWhirlpoolWalletConfig(torProxy);
|
||||
this.tx0ParamService = new Tx0ParamService(SparrowMinerFeeSupplier.getInstance(), config);
|
||||
this.poolSupplier = new ExpirablePoolSupplier(config.getRefreshPoolsDelay(), config.getServerApi(), tx0ParamService);
|
||||
this.tx0Service = new Tx0Service(config);
|
||||
Integer storedBlockHeight = null; // TODO
|
||||
this.config = computeWhirlpoolWalletConfig(storedBlockHeight);
|
||||
this.whirlpoolInfo = null; // instanciated by getWhirlpoolInfo()
|
||||
|
||||
WhirlpoolEventService.getInstance().register(this);
|
||||
}
|
||||
|
||||
private WhirlpoolWalletConfig computeWhirlpoolWalletConfig(HostAndPort torProxy) {
|
||||
DataPersisterFactory dataPersisterFactory = (whirlpoolWallet, bip44w) -> new SparrowDataPersister(whirlpoolWallet, config.getPersistDelaySeconds());
|
||||
DataSourceFactory dataSourceFactory = (whirlpoolWallet, bip44w, dataPersister) -> new SparrowDataSource(whirlpoolWallet, bip44w, dataPersister);
|
||||
private WhirlpoolWalletConfig computeWhirlpoolWalletConfig(Integer storedBlockHeight) {
|
||||
SorobanConfig sorobanConfig = AppServices.getWhirlpoolServices().getSorobanConfig();
|
||||
DataSourceConfig dataSourceConfig = computeDataSourceConfig(storedBlockHeight);
|
||||
DataSourceFactory dataSourceFactory = (whirlpoolWallet, bip44w, passphrase, walletStateSupplier, utxoConfigSupplier) -> new SparrowDataSource(whirlpoolWallet, bip44w, walletStateSupplier, utxoConfigSupplier, dataSourceConfig);
|
||||
|
||||
boolean onion = (torProxy != null);
|
||||
String serverUrl = whirlpoolServer.getServerUrl(onion);
|
||||
ServerApi serverApi = new ServerApi(serverUrl, httpClientService);
|
||||
|
||||
WhirlpoolWalletConfig whirlpoolWalletConfig = new WhirlpoolWalletConfig(dataSourceFactory, httpClientService, stompClientService, torClientService, serverApi, whirlpoolServer.getParams(), false);
|
||||
WhirlpoolWalletConfig whirlpoolWalletConfig = new WhirlpoolWalletConfig(dataSourceFactory, sorobanConfig, false);
|
||||
DataPersisterFactory dataPersisterFactory = (whirlpoolWallet, bip44w) -> new SparrowDataPersister(whirlpoolWallet, whirlpoolWalletConfig.getPersistDelaySeconds());
|
||||
whirlpoolWalletConfig.setDataPersisterFactory(dataPersisterFactory);
|
||||
whirlpoolWalletConfig.setPartner("SPARROW");
|
||||
whirlpoolWalletConfig.setIndexRangePostmix(IndexRange.FULL);
|
||||
return whirlpoolWalletConfig;
|
||||
}
|
||||
|
||||
public Pool getPool(String poolId) {
|
||||
try {
|
||||
return getPools(null).stream().filter(pool -> pool.getPoolId().equals(poolId)).findFirst().orElse(null);
|
||||
} catch(Exception e) {
|
||||
log.error("Error retrieving pools", e);
|
||||
}
|
||||
private DataSourceConfig computeDataSourceConfig(Integer storedBlockHeight) {
|
||||
return new DataSourceConfig(
|
||||
SparrowMinerFeeSupplier.getInstance(),
|
||||
new SparrowChainSupplier(storedBlockHeight),
|
||||
BIP_FORMAT.PROVIDER,
|
||||
BIP_WALLETS.WHIRLPOOL);
|
||||
}
|
||||
|
||||
return null;
|
||||
private WhirlpoolInfo getWhirlpoolInfo() {
|
||||
if (whirlpoolInfo == null) {
|
||||
whirlpoolInfo = new WhirlpoolInfo(SparrowMinerFeeSupplier.getInstance(), config);
|
||||
}
|
||||
return whirlpoolInfo;
|
||||
}
|
||||
|
||||
public Collection<Pool> getPools(Long totalUtxoValue) throws Exception {
|
||||
this.poolSupplier.load();
|
||||
CoordinatorSupplier coordinatorSupplier = getWhirlpoolInfo().getCoordinatorSupplier();
|
||||
coordinatorSupplier.load();
|
||||
if(totalUtxoValue == null) {
|
||||
return poolSupplier.getPools();
|
||||
return coordinatorSupplier.getPools();
|
||||
}
|
||||
|
||||
return tx0ParamService.findPools(poolSupplier.getPools(), totalUtxoValue);
|
||||
return coordinatorSupplier.findPoolsForTx0(totalUtxoValue);
|
||||
}
|
||||
|
||||
public Tx0Previews getTx0Previews(Collection<UnspentOutput> utxos) throws Exception {
|
||||
// preview all pools
|
||||
Tx0Config tx0Config = computeTx0Config();
|
||||
return tx0Service.tx0Previews(utxos, tx0Config);
|
||||
return AsyncUtil.getInstance().blockingGet(getWhirlpoolInfo().tx0Previews(tx0Config, utxos));
|
||||
}
|
||||
|
||||
public Tx0 broadcastTx0(Pool pool, Collection<BlockTransactionHashIndex> utxos) throws Exception {
|
||||
WhirlpoolWallet whirlpoolWallet = getWhirlpoolWallet();
|
||||
whirlpoolWallet.start();
|
||||
whirlpoolWallet.startAsync().subscribeOn(Schedulers.io())
|
||||
.observeOn(JavaFxScheduler.platform());
|
||||
UtxoSupplier utxoSupplier = whirlpoolWallet.getUtxoSupplier();
|
||||
List<WhirlpoolUtxo> whirlpoolUtxos = utxos.stream().map(ref -> utxoSupplier.findUtxo(ref.getHashAsString(), (int)ref.getIndex())).filter(Objects::nonNull).collect(Collectors.toList());
|
||||
|
||||
|
@ -153,24 +158,29 @@ public class Whirlpool {
|
|||
}
|
||||
|
||||
private Tx0Config computeTx0Config() {
|
||||
return new Tx0Config(tx0ParamService, poolSupplier, tx0FeeTarget, mixFeeTarget, WhirlpoolAccount.BADBANK);
|
||||
CoordinatorSupplier coordinatorSupplier = getWhirlpoolInfo().getCoordinatorSupplier();
|
||||
Tx0PreviewService tx0PreviewService = getWhirlpoolInfo().getTx0PreviewService();
|
||||
Collection<Pool> pools = coordinatorSupplier.getPools();
|
||||
return new Tx0Config(tx0PreviewService, pools, tx0FeeTarget, mixFeeTarget, SamouraiAccount.BADBANK);
|
||||
}
|
||||
|
||||
public void setHDWallet(String walletId, Wallet wallet) {
|
||||
NetworkParameters params = config.getSamouraiNetwork().getParams();
|
||||
this.hdWallet = computeHdWallet(wallet, params);
|
||||
this.walletId = walletId;
|
||||
}
|
||||
|
||||
public static HD_Wallet computeHdWallet(Wallet wallet, NetworkParameters params) {
|
||||
if(wallet.isEncrypted()) {
|
||||
throw new IllegalStateException("Wallet cannot be encrypted");
|
||||
}
|
||||
|
||||
try {
|
||||
Keystore keystore = wallet.getKeystores().get(0);
|
||||
ScriptType scriptType = wallet.getScriptType();
|
||||
int purpose = scriptType.getDefaultDerivation().get(0).num();
|
||||
List<String> words = keystore.getSeed().getMnemonicCode();
|
||||
String words = keystore.getSeed().getMnemonicString().asString();
|
||||
String passphrase = keystore.getSeed().getPassphrase() == null ? "" : keystore.getSeed().getPassphrase().asString();
|
||||
HD_WalletFactoryGeneric hdWalletFactory = HD_WalletFactoryGeneric.getInstance();
|
||||
byte[] seed = hdWalletFactory.computeSeedFromWords(words);
|
||||
this.walletId = walletId;
|
||||
hdWallet = new HD_Wallet(purpose, words, config.getNetworkParameters(), seed, passphrase);
|
||||
return hdWalletFactory.restoreWalletFromWords(words, passphrase, params);
|
||||
} catch(Exception e) {
|
||||
throw new IllegalStateException("Could not create Whirlpool HD wallet ", e);
|
||||
}
|
||||
|
@ -187,7 +197,7 @@ public class Whirlpool {
|
|||
|
||||
try {
|
||||
WhirlpoolWallet whirlpoolWallet = new WhirlpoolWallet(config, Utils.hexToBytes(hdWallet.getSeedHex()), hdWallet.getPassphrase(), walletId);
|
||||
return whirlpoolWalletService.openWallet(whirlpoolWallet);
|
||||
return whirlpoolWalletService.openWallet(whirlpoolWallet, hdWallet.getPassphrase());
|
||||
} catch(Exception e) {
|
||||
throw new WhirlpoolException("Could not create whirlpool wallet ", e);
|
||||
}
|
||||
|
@ -199,18 +209,6 @@ public class Whirlpool {
|
|||
}
|
||||
}
|
||||
|
||||
public UtxoMixData getMixData(BlockTransactionHashIndex txo) {
|
||||
if(whirlpoolWalletService.whirlpoolWallet() != null) {
|
||||
WhirlpoolUtxo whirlpoolUtxo = whirlpoolWalletService.whirlpoolWallet().getUtxoSupplier().findUtxo(txo.getHashAsString(), (int)txo.getIndex());
|
||||
if (whirlpoolUtxo != null) {
|
||||
UtxoConfig utxoConfig = whirlpoolUtxo.getUtxoConfigOrDefault();
|
||||
return new UtxoMixData(utxoConfig.getMixsDone(), null);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void mix(BlockTransactionHashIndex utxo) throws WhirlpoolException {
|
||||
if(whirlpoolWalletService.whirlpoolWallet() == null) {
|
||||
throw new WhirlpoolException("Whirlpool wallet not yet created");
|
||||
|
@ -266,7 +264,9 @@ public class Whirlpool {
|
|||
|
||||
public void refreshUtxos() {
|
||||
if(whirlpoolWalletService.whirlpoolWallet() != null) {
|
||||
whirlpoolWalletService.whirlpoolWallet().refreshUtxos();
|
||||
whirlpoolWalletService.whirlpoolWallet().refreshUtxosAsync()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(JavaFxScheduler.platform());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -321,7 +321,7 @@ public class Whirlpool {
|
|||
log.warn("Wallet is not started, but mixingProperty is true");
|
||||
WhirlpoolEventService.getInstance().post(new WalletStopEvent(whirlpoolWalletService.whirlpoolWallet()));
|
||||
} else if(whirlpoolWalletService.whirlpoolWallet().getMixingState().getUtxosMixing().isEmpty() &&
|
||||
!whirlpoolWalletService.whirlpoolWallet().getUtxoSupplier().findUtxos(WhirlpoolAccount.PREMIX, WhirlpoolAccount.POSTMIX).isEmpty()) {
|
||||
!whirlpoolWalletService.whirlpoolWallet().getUtxoSupplier().findUtxos(SamouraiAccount.PREMIX, SamouraiAccount.POSTMIX).isEmpty()) {
|
||||
log.warn("No UTXOs mixing, but mixingProperty is true");
|
||||
//Will automatically restart
|
||||
AppServices.getWhirlpoolServices().stopWhirlpool(this, false);
|
||||
|
@ -352,7 +352,7 @@ public class Whirlpool {
|
|||
|
||||
public void shutdown() {
|
||||
whirlpoolWalletService.closeWallet();
|
||||
httpClientService.shutdown();
|
||||
AppServices.getHttpClientService().stop();
|
||||
}
|
||||
|
||||
public StartupService createStartupService() {
|
||||
|
@ -389,7 +389,7 @@ public class Whirlpool {
|
|||
return AppServices.get().getOpenWallets().entrySet().stream().filter(entry -> entry.getValue().getWalletId(entry.getKey()).equals(walletId)).map(Map.Entry::getKey).findFirst().orElse(null);
|
||||
}
|
||||
|
||||
public static Wallet getStandardAccountWallet(WhirlpoolAccount whirlpoolAccount, Wallet wallet) {
|
||||
public static Wallet getStandardAccountWallet(SamouraiAccount whirlpoolAccount, Wallet wallet) {
|
||||
StandardAccount standardAccount = getStandardAccount(whirlpoolAccount);
|
||||
if(StandardAccount.isWhirlpoolAccount(standardAccount) || wallet.getStandardAccountType() != standardAccount) {
|
||||
Wallet standardWallet = wallet.getChildWallet(standardAccount);
|
||||
|
@ -403,12 +403,12 @@ public class Whirlpool {
|
|||
return wallet;
|
||||
}
|
||||
|
||||
public static StandardAccount getStandardAccount(WhirlpoolAccount whirlpoolAccount) {
|
||||
if(whirlpoolAccount == WhirlpoolAccount.PREMIX) {
|
||||
public static StandardAccount getStandardAccount(SamouraiAccount whirlpoolAccount) {
|
||||
if(whirlpoolAccount == SamouraiAccount.PREMIX) {
|
||||
return StandardAccount.WHIRLPOOL_PREMIX;
|
||||
} else if(whirlpoolAccount == WhirlpoolAccount.POSTMIX) {
|
||||
} else if(whirlpoolAccount == SamouraiAccount.POSTMIX) {
|
||||
return StandardAccount.WHIRLPOOL_POSTMIX;
|
||||
} else if(whirlpoolAccount == WhirlpoolAccount.BADBANK) {
|
||||
} else if(whirlpoolAccount == SamouraiAccount.BADBANK) {
|
||||
return StandardAccount.WHIRLPOOL_BADBANK;
|
||||
}
|
||||
|
||||
|
@ -442,9 +442,11 @@ public class Whirlpool {
|
|||
throw new IllegalStateException("Cannot mix outputs from a wallet with multiple keystores");
|
||||
}
|
||||
|
||||
SamouraiNetwork samouraiNetwork = AppServices.getWhirlpoolServices().getSamouraiNetwork();
|
||||
boolean testnet = FormatsUtilGeneric.getInstance().isTestNet(samouraiNetwork.getParams());
|
||||
|
||||
UnspentOutput.Xpub xpub = new UnspentOutput.Xpub();
|
||||
List<ExtendedKey.Header> headers = ExtendedKey.Header.getHeaders(Network.get());
|
||||
ExtendedKey.Header header = headers.stream().filter(head -> head.getDefaultScriptType().equals(wallet.getScriptType()) && !head.isPrivateKey()).findFirst().orElse(ExtendedKey.Header.xpub);
|
||||
ExtendedKey.Header header = testnet ? ExtendedKey.Header.tpub : ExtendedKey.Header.xpub;
|
||||
xpub.m = wallet.getKeystores().get(0).getExtendedPublicKey().toString(header);
|
||||
xpub.path = node.getDerivationPath().toUpperCase(Locale.ROOT);
|
||||
|
||||
|
@ -453,26 +455,8 @@ public class Whirlpool {
|
|||
return out;
|
||||
}
|
||||
|
||||
public HostAndPort getTorProxy() {
|
||||
return httpClientService.getTorProxy();
|
||||
}
|
||||
|
||||
public void setTorProxy(HostAndPort torProxy) {
|
||||
if(isStarted()) {
|
||||
throw new IllegalStateException("Cannot set tor proxy on a started Whirlpool");
|
||||
}
|
||||
|
||||
//Ensure all http clients are shutdown first
|
||||
httpClientService.shutdown();
|
||||
|
||||
httpClientService.setTorProxy(torProxy);
|
||||
String serverUrl = whirlpoolServer.getServerUrl(torProxy != null);
|
||||
ServerApi serverApi = new ServerApi(serverUrl, httpClientService);
|
||||
config.setServerApi(serverApi);
|
||||
}
|
||||
|
||||
public void refreshTorCircuits() {
|
||||
torClientService.changeIdentity();
|
||||
AppServices.getHttpClientService().changeIdentity();
|
||||
}
|
||||
|
||||
public String getScode() {
|
||||
|
@ -530,16 +514,10 @@ public class Whirlpool {
|
|||
throw new IllegalStateException("Cannot find mix to wallet with id " + mixToWalletId);
|
||||
}
|
||||
|
||||
Integer highestUsedIndex = mixToWallet.getNode(KeyPurpose.RECEIVE).getHighestUsedIndex();
|
||||
int startIndex = highestUsedIndex == null ? 0 : highestUsedIndex + 1;
|
||||
int mixes = minMixes == null ? DEFAULT_MIXTO_MIN_MIXES : minMixes;
|
||||
|
||||
if(mixToWallet.getMixConfig() != null) {
|
||||
startIndex = Math.max(startIndex, mixToWallet.getMixConfig().getReceiveIndex());
|
||||
}
|
||||
|
||||
IPostmixHandler postmixHandler = new SparrowPostmixHandler(whirlpoolWalletService, mixToWallet, KeyPurpose.RECEIVE, startIndex);
|
||||
ExternalDestination externalDestination = new ExternalDestination(postmixHandler, 0, startIndex, mixes, DEFAULT_MIXTO_RANDOM_FACTOR);
|
||||
IPostmixHandler postmixHandler = new SparrowPostmixHandler(whirlpoolWalletService, mixToWallet, KeyPurpose.RECEIVE);
|
||||
ExternalDestination externalDestination = new ExternalDestination(postmixHandler, 0, mixes, DEFAULT_MIXTO_RANDOM_FACTOR);
|
||||
config.setExternalDestination(externalDestination);
|
||||
}
|
||||
|
||||
|
@ -580,7 +558,8 @@ public class Whirlpool {
|
|||
|
||||
@Subscribe
|
||||
public void onMixSuccess(MixSuccessEvent e) {
|
||||
WalletUtxo walletUtxo = getUtxo(e.getWhirlpoolUtxo());
|
||||
WhirlpoolUtxo whirlpoolUtxo = e.getMixParams().getWhirlpoolUtxo();
|
||||
WalletUtxo walletUtxo = getUtxo(whirlpoolUtxo);
|
||||
if(walletUtxo != null) {
|
||||
log.debug("Mix success, new utxo " + e.getReceiveUtxo().getHash() + ":" + e.getReceiveUtxo().getIndex());
|
||||
Platform.runLater(() -> EventManager.get().post(new WhirlpoolMixSuccessEvent(walletUtxo.wallet, walletUtxo.utxo, e.getReceiveUtxo(), getReceiveNode(e, walletUtxo))));
|
||||
|
@ -589,7 +568,7 @@ public class Whirlpool {
|
|||
|
||||
private WalletNode getReceiveNode(MixSuccessEvent e, WalletUtxo walletUtxo) {
|
||||
for(WalletNode walletNode : walletUtxo.wallet.getNode(KeyPurpose.RECEIVE).getChildren()) {
|
||||
if(walletNode.getAddress().toString().equals(e.getMixProgress().getDestination().getAddress())) {
|
||||
if(walletNode.getAddress().toString().equals(e.getReceiveDestination().getAddress())) {
|
||||
return walletNode;
|
||||
}
|
||||
}
|
||||
|
@ -599,19 +578,22 @@ public class Whirlpool {
|
|||
|
||||
@Subscribe
|
||||
public void onMixFail(MixFailEvent e) {
|
||||
WalletUtxo walletUtxo = getUtxo(e.getWhirlpoolUtxo());
|
||||
WhirlpoolUtxo whirlpoolUtxo = e.getMixParams().getWhirlpoolUtxo();
|
||||
WalletUtxo walletUtxo = getUtxo(whirlpoolUtxo);
|
||||
if(walletUtxo != null) {
|
||||
log.debug("Mix failed for utxo " + e.getWhirlpoolUtxo().getUtxo().tx_hash + ":" + e.getWhirlpoolUtxo().getUtxo().tx_output_n + " " + e.getMixFailReason());
|
||||
log.debug("Mix failed for utxo " + whirlpoolUtxo.getUtxo().tx_hash + ":" + whirlpoolUtxo.getUtxo().tx_output_n + " " + e.getMixFailReason());
|
||||
Platform.runLater(() -> EventManager.get().post(new WhirlpoolMixEvent(walletUtxo.wallet, walletUtxo.utxo, e.getMixFailReason(), e.getError())));
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onMixProgress(MixProgressEvent e) {
|
||||
WalletUtxo walletUtxo = getUtxo(e.getWhirlpoolUtxo());
|
||||
WhirlpoolUtxo whirlpoolUtxo = e.getMixParams().getWhirlpoolUtxo();
|
||||
MixProgress mixProgress = whirlpoolUtxo.getUtxoState().getMixProgress();
|
||||
WalletUtxo walletUtxo = getUtxo(whirlpoolUtxo);
|
||||
if(walletUtxo != null && isMixing()) {
|
||||
log.debug("Mix progress for utxo " + e.getWhirlpoolUtxo().getUtxo().tx_hash + ":" + e.getWhirlpoolUtxo().getUtxo().tx_output_n + " " + e.getWhirlpoolUtxo().getMixsDone() + " " + e.getMixProgress().getMixStep() + " " + e.getWhirlpoolUtxo().getUtxoState().getStatus());
|
||||
Platform.runLater(() -> EventManager.get().post(new WhirlpoolMixEvent(walletUtxo.wallet, walletUtxo.utxo, e.getMixProgress())));
|
||||
log.debug("Mix progress for utxo " + whirlpoolUtxo.getUtxo().tx_hash + ":" + whirlpoolUtxo.getUtxo().tx_output_n + " " + whirlpoolUtxo.getMixsDone() + " " + mixProgress.getMixStep() + " " + whirlpoolUtxo.getUtxoState().getStatus());
|
||||
Platform.runLater(() -> EventManager.get().post(new WhirlpoolMixEvent(walletUtxo.wallet, walletUtxo.utxo, mixProgress)));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -624,7 +606,7 @@ public class Whirlpool {
|
|||
if(resyncMixesDone) {
|
||||
Wallet wallet = AppServices.get().getWallet(walletId);
|
||||
if(wallet != null) {
|
||||
Wallet postmixWallet = getStandardAccountWallet(WhirlpoolAccount.POSTMIX, wallet);
|
||||
Wallet postmixWallet = getStandardAccountWallet(SamouraiAccount.POSTMIX, wallet);
|
||||
resyncMixesDone(this, postmixWallet);
|
||||
resyncMixesDone = false;
|
||||
}
|
||||
|
@ -709,7 +691,7 @@ public class Whirlpool {
|
|||
updateMessage("Broadcasting premix transaction...");
|
||||
|
||||
Tx0 tx0 = whirlpool.broadcastTx0(pool, utxos);
|
||||
return Sha256Hash.wrap(tx0.getTxid());
|
||||
return Sha256Hash.wrap(tx0.getTx().getHashAsString());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -733,7 +715,10 @@ public class Whirlpool {
|
|||
whirlpool.startingProperty.set(true);
|
||||
WhirlpoolWallet whirlpoolWallet = whirlpool.getWhirlpoolWallet();
|
||||
if(AppServices.onlineProperty().get()) {
|
||||
whirlpoolWallet.start();
|
||||
whirlpoolWallet.startAsync()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(JavaFxScheduler.platform())
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
return whirlpoolWallet;
|
||||
|
@ -771,30 +756,6 @@ public class Whirlpool {
|
|||
}
|
||||
}
|
||||
|
||||
public static class RegisteredInputsService extends Service<Integer> {
|
||||
private final Whirlpool whirlpool;
|
||||
private final String poolId;
|
||||
|
||||
public RegisteredInputsService(Whirlpool whirlpool, String poolId) {
|
||||
this.whirlpool = whirlpool;
|
||||
this.poolId = poolId;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Task<Integer> createTask() {
|
||||
return new Task<>() {
|
||||
protected Integer call() {
|
||||
Pool pool = whirlpool.getPool(poolId);
|
||||
if(pool != null) {
|
||||
return pool.getNbRegistered();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static class WalletUtxo {
|
||||
public final Wallet wallet;
|
||||
public final BlockTransactionHashIndex utxo;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -2,7 +2,11 @@ package com.sparrowwallet.sparrow.whirlpool;
|
|||
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.google.common.net.HostAndPort;
|
||||
import com.samourai.soroban.client.SorobanConfig;
|
||||
import com.samourai.wallet.constants.SamouraiNetwork;
|
||||
import com.samourai.wallet.util.ExtLibJConfig;
|
||||
import com.samourai.whirlpool.client.wallet.WhirlpoolEventService;
|
||||
import com.sparrowwallet.drongo.Drongo;
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.DeterministicSeed;
|
||||
|
@ -14,6 +18,7 @@ import com.sparrowwallet.sparrow.EventManager;
|
|||
import com.sparrowwallet.sparrow.WalletTabData;
|
||||
import com.sparrowwallet.sparrow.event.*;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
import com.sparrowwallet.sparrow.net.HttpClientService;
|
||||
import com.sparrowwallet.sparrow.soroban.Soroban;
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.input.KeyCode;
|
||||
|
@ -36,6 +41,24 @@ public class WhirlpoolServices {
|
|||
|
||||
private final Map<String, Whirlpool> whirlpoolMap = new HashMap<>();
|
||||
|
||||
private final SorobanConfig sorobanConfig;
|
||||
|
||||
public WhirlpoolServices() {
|
||||
ExtLibJConfig extLibJConfig = computeExtLibJConfig();
|
||||
this.sorobanConfig = new SorobanConfig(extLibJConfig);
|
||||
}
|
||||
|
||||
private ExtLibJConfig computeExtLibJConfig() {
|
||||
HttpClientService httpClientService = AppServices.getHttpClientService();
|
||||
boolean onion = (AppServices.getTorProxy() != null);
|
||||
SamouraiNetwork samouraiNetwork = getSamouraiNetwork();
|
||||
return new ExtLibJConfig(samouraiNetwork, onion, Drongo.getProvider(), httpClientService);
|
||||
}
|
||||
|
||||
public SamouraiNetwork getSamouraiNetwork() {
|
||||
return SamouraiNetwork.valueOf(Network.get().getName().toUpperCase(Locale.ROOT));
|
||||
}
|
||||
|
||||
public Whirlpool getWhirlpool(Wallet wallet) {
|
||||
Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet();
|
||||
for(Map.Entry<Wallet, Storage> entry : AppServices.get().getOpenWallets().entrySet()) {
|
||||
|
@ -50,14 +73,8 @@ public class WhirlpoolServices {
|
|||
public Whirlpool getWhirlpool(String walletId) {
|
||||
Whirlpool whirlpool = whirlpoolMap.get(walletId);
|
||||
if(whirlpool == null) {
|
||||
HostAndPort torProxy = getTorProxy();
|
||||
whirlpool = new Whirlpool(Network.get(), torProxy);
|
||||
whirlpool = new Whirlpool();
|
||||
whirlpoolMap.put(walletId, whirlpool);
|
||||
} else if(!whirlpool.isStarted()) {
|
||||
HostAndPort torProxy = getTorProxy();
|
||||
if(!Objects.equals(whirlpool.getTorProxy(), torProxy)) {
|
||||
whirlpool.setTorProxy(getTorProxy());
|
||||
}
|
||||
}
|
||||
|
||||
return whirlpool;
|
||||
|
@ -87,11 +104,6 @@ public class WhirlpoolServices {
|
|||
|
||||
public void startWhirlpool(Wallet wallet, Whirlpool whirlpool, boolean notifyIfMixToMissing) {
|
||||
if(wallet.getMasterMixConfig().getMixOnStartup() != Boolean.FALSE) {
|
||||
HostAndPort torProxy = getTorProxy();
|
||||
if(!Objects.equals(whirlpool.getTorProxy(), torProxy)) {
|
||||
whirlpool.setTorProxy(getTorProxy());
|
||||
}
|
||||
|
||||
try {
|
||||
String mixToWalletId = getWhirlpoolMixToWalletId(wallet.getMasterMixConfig());
|
||||
whirlpool.setMixToWallet(mixToWalletId, wallet.getMasterMixConfig().getMinMixes());
|
||||
|
@ -122,6 +134,7 @@ public class WhirlpoolServices {
|
|||
}
|
||||
if(exception instanceof TimeoutException || exception instanceof SocketTimeoutException) {
|
||||
EventManager.get().post(new StatusEvent("Error connecting to Whirlpool server, will retry soon..."));
|
||||
HostAndPort torProxy = getTorProxy();
|
||||
if(torProxy != null) {
|
||||
whirlpool.refreshTorCircuits();
|
||||
}
|
||||
|
@ -300,4 +313,8 @@ public class WhirlpoolServices {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
public SorobanConfig getSorobanConfig() {
|
||||
return sorobanConfig;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
@ -37,7 +37,7 @@ public class SparrowUtxoConfigPersister extends UtxoConfigPersister {
|
|||
}
|
||||
|
||||
Map<String, UtxoConfigPersisted> utxoConfigs = wallet.getUtxoMixes().entrySet().stream()
|
||||
.collect(Collectors.toMap(entry -> entry.getKey().toString(), entry -> new UtxoConfigPersisted(entry.getValue().getMixesDone(), entry.getValue().getExpired()),
|
||||
.collect(Collectors.toMap(entry -> entry.getKey().toString(), entry -> new UtxoConfigPersisted(entry.getValue().getMixesDone(), entry.getValue().getExpired(), false, null),
|
||||
(u, v) -> { throw new IllegalStateException("Duplicate utxo config hashes"); },
|
||||
ConcurrentHashMap::new));
|
||||
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
package com.sparrowwallet.sparrow.whirlpool.dataSource;
|
||||
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.samourai.wallet.api.backend.beans.WalletResponse;
|
||||
import com.samourai.wallet.chain.ChainSupplier;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.NewBlockEvent;
|
||||
|
||||
public class SparrowChainSupplier implements ChainSupplier {
|
||||
private int storedBlockHeight;
|
||||
private WalletResponse.InfoBlock latestBlock;
|
||||
|
||||
public SparrowChainSupplier(Integer storedBlockHeight) {
|
||||
this.storedBlockHeight = AppServices.getCurrentBlockHeight() == null ?
|
||||
(storedBlockHeight!=null?storedBlockHeight:0)
|
||||
: AppServices.getCurrentBlockHeight();
|
||||
this.latestBlock = computeLatestBlock();
|
||||
EventManager.get().register(this);
|
||||
}
|
||||
|
||||
public void close() {
|
||||
EventManager.get().unregister(this);
|
||||
}
|
||||
|
||||
private WalletResponse.InfoBlock computeLatestBlock() {
|
||||
WalletResponse.InfoBlock latestBlock = new WalletResponse.InfoBlock();
|
||||
latestBlock.height = AppServices.getCurrentBlockHeight() == null ? storedBlockHeight : AppServices.getCurrentBlockHeight();
|
||||
latestBlock.hash = Sha256Hash.ZERO_HASH.toString();
|
||||
latestBlock.time = AppServices.getLatestBlockHeader() == null ? 1 : AppServices.getLatestBlockHeader().getTime();
|
||||
return latestBlock;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WalletResponse.InfoBlock getLatestBlock() {
|
||||
return latestBlock;
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void newBlock(NewBlockEvent event) {
|
||||
this.latestBlock = computeLatestBlock();
|
||||
}
|
||||
}
|
|
@ -1,235 +1,107 @@
|
|||
package com.sparrowwallet.sparrow.whirlpool.dataSource;
|
||||
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.samourai.wallet.api.backend.MinerFeeTarget;
|
||||
import com.samourai.wallet.api.backend.beans.UnspentOutput;
|
||||
import com.samourai.wallet.api.backend.beans.WalletResponse;
|
||||
import com.samourai.wallet.api.backend.IPushTx;
|
||||
import com.samourai.wallet.api.backend.ISweepBackend;
|
||||
import com.samourai.wallet.api.backend.seenBackend.ISeenBackend;
|
||||
import com.samourai.wallet.api.backend.seenBackend.SeenBackendWithFallback;
|
||||
import com.samourai.wallet.hd.HD_Wallet;
|
||||
import com.samourai.whirlpool.client.tx0.Tx0ParamService;
|
||||
import com.samourai.wallet.httpClient.HttpUsage;
|
||||
import com.samourai.wallet.httpClient.IHttpClient;
|
||||
import com.samourai.whirlpool.client.wallet.WhirlpoolWallet;
|
||||
import com.samourai.whirlpool.client.wallet.beans.WhirlpoolUtxo;
|
||||
import com.samourai.whirlpool.client.wallet.data.chain.ChainSupplier;
|
||||
import com.samourai.whirlpool.client.wallet.data.dataPersister.DataPersister;
|
||||
import com.samourai.whirlpool.client.wallet.data.dataSource.WalletResponseDataSource;
|
||||
import com.samourai.whirlpool.client.wallet.data.minerFee.MinerFeeSupplier;
|
||||
import com.samourai.whirlpool.client.wallet.data.pool.PoolSupplier;
|
||||
import com.samourai.whirlpool.client.wallet.data.utxo.BasicUtxoSupplier;
|
||||
import com.samourai.whirlpool.client.wallet.data.utxo.UtxoData;
|
||||
import com.samourai.whirlpool.client.wallet.WhirlpoolWalletConfig;
|
||||
import com.samourai.whirlpool.client.wallet.data.coordinator.CoordinatorSupplier;
|
||||
import com.samourai.whirlpool.client.wallet.data.dataSource.AbstractDataSource;
|
||||
import com.samourai.whirlpool.client.wallet.data.dataSource.DataSourceConfig;
|
||||
import com.samourai.whirlpool.client.wallet.data.utxo.UtxoSupplier;
|
||||
import com.samourai.whirlpool.client.wallet.data.utxoConfig.UtxoConfigSupplier;
|
||||
import com.samourai.whirlpool.client.wallet.data.wallet.WalletSupplier;
|
||||
import com.samourai.whirlpool.client.wallet.data.walletState.WalletStateSupplier;
|
||||
import com.sparrowwallet.drongo.ExtendedKey;
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.protocol.TransactionInput;
|
||||
import com.sparrowwallet.drongo.protocol.TransactionOutput;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.NewBlockEvent;
|
||||
import com.sparrowwallet.sparrow.event.WalletAddressesChangedEvent;
|
||||
import com.sparrowwallet.sparrow.event.WalletHistoryChangedEvent;
|
||||
import com.sparrowwallet.sparrow.net.ElectrumServer;
|
||||
import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
|
||||
import javafx.application.Platform;
|
||||
import org.bitcoinj.core.NetworkParameters;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
public class SparrowDataSource extends WalletResponseDataSource {
|
||||
public class SparrowDataSource extends AbstractDataSource {
|
||||
private static final Logger log = LoggerFactory.getLogger(SparrowDataSource.class);
|
||||
|
||||
private final String walletIdentifierPrefix;
|
||||
private final ISeenBackend seenBackend;
|
||||
private final IPushTx pushTx;
|
||||
private SparrowUtxoSupplier utxoSupplier;
|
||||
|
||||
public SparrowDataSource(
|
||||
WhirlpoolWallet whirlpoolWallet,
|
||||
HD_Wallet bip44w,
|
||||
DataPersister dataPersister)
|
||||
WalletStateSupplier walletStateSupplier,
|
||||
UtxoConfigSupplier utxoConfigSupplier,
|
||||
DataSourceConfig dataSourceConfig)
|
||||
throws Exception {
|
||||
super(whirlpoolWallet, bip44w, dataPersister);
|
||||
|
||||
// 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();
|
||||
this.utxoSupplier = new SparrowUtxoSupplier(whirlpoolWallet, walletSupplier, utxoConfigSupplier, dataSourceConfig);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void open() throws Exception {
|
||||
super.open();
|
||||
EventManager.get().register(this);
|
||||
private ISeenBackend computeSeenBackend(WhirlpoolWalletConfig whirlpoolWalletConfig) {
|
||||
IHttpClient httpClient = whirlpoolWalletConfig.getHttpClient(HttpUsage.BACKEND);
|
||||
ISeenBackend sparrowSeenBackend = new SparrowSeenBackend(httpClient);
|
||||
NetworkParameters params = whirlpoolWalletConfig.getSamouraiNetwork().getParams();
|
||||
return SeenBackendWithFallback.withOxt(sparrowSeenBackend, params);
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MinerFeeSupplier getMinerFeeSupplier() {
|
||||
return SparrowMinerFeeSupplier.getInstance();
|
||||
protected void load(boolean initial) throws Exception {
|
||||
super.load(initial);
|
||||
utxoSupplier.refresh();
|
||||
}
|
||||
|
||||
static Wallet getWallet(String zpub) {
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
EventManager.get().unregister(this);
|
||||
((SparrowChainSupplier)getDataSourceConfig().getChainSupplier()).close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public IPushTx getPushTx() {
|
||||
return pushTx;
|
||||
}
|
||||
|
||||
public static Wallet getWallet(String zpub) {
|
||||
return AppServices.get().getOpenWallets().keySet().stream()
|
||||
.filter(wallet -> {
|
||||
try {
|
||||
|
@ -255,17 +127,10 @@ public class SparrowDataSource extends WalletResponseDataSource {
|
|||
refreshWallet(event.getWalletId(), event.getWallet(), 0);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void newBlock(NewBlockEvent event) {
|
||||
try {
|
||||
refresh();
|
||||
} catch (Exception e) {
|
||||
log.error("", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshWallet(String walletId, Wallet wallet, int i) {
|
||||
try {
|
||||
// prefix matching <prefix>:master, :Premix, :Postmix
|
||||
String walletIdentifierPrefix = getWhirlpoolWallet().getWalletIdentifier().replace(":master", "");
|
||||
// match <prefix>:master, :Premix, :Postmix
|
||||
if(walletId.startsWith(walletIdentifierPrefix) && (wallet.isWhirlpoolMasterWallet() || wallet.isWhirlpoolChildWallet())) {
|
||||
//Workaround to avoid refreshing the wallet after it has been opened, but before it has been started
|
||||
|
@ -273,11 +138,26 @@ public class SparrowDataSource extends WalletResponseDataSource {
|
|||
if(whirlpool != null && whirlpool.isStarting() && i < 1000) {
|
||||
Platform.runLater(() -> refreshWallet(walletId, wallet, i+1));
|
||||
} else {
|
||||
refresh();
|
||||
utxoSupplier.refresh();
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Error refreshing wallet", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ISweepBackend getSweepBackend() {
|
||||
return null; // not necessary
|
||||
}
|
||||
|
||||
@Override
|
||||
public ISeenBackend getSeenBackend() {
|
||||
return seenBackend;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UtxoSupplier getUtxoSupplier() {
|
||||
return utxoSupplier;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
package com.sparrowwallet.sparrow.whirlpool.dataSource;
|
||||
|
||||
import com.samourai.wallet.client.indexHandler.IIndexHandler;
|
||||
import com.samourai.wallet.util.XPubUtil;
|
||||
import com.samourai.whirlpool.client.mix.handler.AbstractPostmixHandler;
|
||||
import com.samourai.whirlpool.client.mix.handler.DestinationType;
|
||||
import com.samourai.whirlpool.client.mix.handler.IPostmixHandler;
|
||||
import com.samourai.whirlpool.client.mix.handler.MixDestination;
|
||||
import com.samourai.whirlpool.client.wallet.WhirlpoolWalletService;
|
||||
import com.samourai.whirlpool.client.wallet.beans.IndexRange;
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
@ -13,65 +12,37 @@ import com.sparrowwallet.drongo.wallet.WalletNode;
|
|||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class SparrowPostmixHandler implements IPostmixHandler {
|
||||
// TODO maybe replace with XPubPostmixHandler
|
||||
public class SparrowPostmixHandler extends AbstractPostmixHandler {
|
||||
private static final Logger log = LoggerFactory.getLogger(SparrowPostmixHandler.class);
|
||||
|
||||
private final WhirlpoolWalletService whirlpoolWalletService;
|
||||
private final Wallet wallet;
|
||||
private final KeyPurpose keyPurpose;
|
||||
private final int startIndex;
|
||||
|
||||
protected MixDestination destination;
|
||||
|
||||
public SparrowPostmixHandler(WhirlpoolWalletService whirlpoolWalletService, Wallet wallet, KeyPurpose keyPurpose, int startIndex) {
|
||||
this.whirlpoolWalletService = whirlpoolWalletService;
|
||||
public SparrowPostmixHandler(WhirlpoolWalletService whirlpoolWalletService, Wallet wallet, KeyPurpose keyPurpose) {
|
||||
super(whirlpoolWalletService.whirlpoolWallet().getWalletStateSupplier().getIndexHandlerExternal(),
|
||||
whirlpoolWalletService.whirlpoolWallet().getConfig().getSamouraiNetwork().getParams());
|
||||
this.wallet = wallet;
|
||||
this.keyPurpose = keyPurpose;
|
||||
this.startIndex = startIndex;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected IndexRange getIndexRange() {
|
||||
return IndexRange.FULL;
|
||||
}
|
||||
|
||||
public Wallet getWallet() {
|
||||
return wallet;
|
||||
}
|
||||
|
||||
protected MixDestination computeNextDestination() throws Exception {
|
||||
// index
|
||||
int index = Math.max(getIndexHandler().getAndIncrementUnconfirmed(), startIndex);
|
||||
|
||||
@Override
|
||||
public MixDestination computeDestination(int index) throws Exception {
|
||||
// address
|
||||
WalletNode node = new WalletNode(wallet, keyPurpose, index);
|
||||
Address address = node.getAddress();
|
||||
String path = XPubUtil.getInstance().getPath(index, keyPurpose.getPathIndex().num());
|
||||
String path = "xpub/"+keyPurpose.getPathIndex().num()+"/"+index;
|
||||
|
||||
log.info("Mixing to external xPub -> receiveAddress=" + address + ", path=" + path);
|
||||
return new MixDestination(DestinationType.XPUB, index, address.toString(), path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MixDestination getDestination() {
|
||||
return destination; // may be NULL
|
||||
}
|
||||
|
||||
public final MixDestination computeDestination() throws Exception {
|
||||
// use "unconfirmed" index to avoid huge index gaps on multiple mix failures
|
||||
this.destination = computeNextDestination();
|
||||
return destination;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMixFail() {
|
||||
if(destination != null) {
|
||||
getIndexHandler().cancelUnconfirmed(destination.getIndex());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRegisterOutput() {
|
||||
// confirm receive address even when REGISTER_OUTPUT fails, to avoid 'ouput already registered'
|
||||
getIndexHandler().confirmUnconfirmed(destination.getIndex());
|
||||
}
|
||||
|
||||
private IIndexHandler getIndexHandler() {
|
||||
return whirlpoolWalletService.whirlpoolWallet().getWalletStateSupplier().getIndexHandlerExternal();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
package com.sparrowwallet.sparrow.whirlpool.dataSource;
|
||||
|
||||
import com.samourai.wallet.api.backend.seenBackend.ISeenBackend;
|
||||
import com.samourai.wallet.api.backend.seenBackend.SeenResponse;
|
||||
import com.samourai.wallet.httpClient.IHttpClient;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
public class SparrowSeenBackend implements ISeenBackend {
|
||||
private IHttpClient httpClient;
|
||||
|
||||
public SparrowSeenBackend(IHttpClient httpClient) {
|
||||
this.httpClient = httpClient;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SeenResponse seen(Collection<String> addresses) throws Exception {
|
||||
return null; // TODO implement: check if each address already received funds
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean seen(String address) throws Exception {
|
||||
return false; // TODO implement: return true if address already received funds
|
||||
}
|
||||
|
||||
@Override
|
||||
public IHttpClient getHttpClient() {
|
||||
return httpClient;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,150 @@
|
|||
package com.sparrowwallet.sparrow.whirlpool.dataSource;
|
||||
|
||||
import com.samourai.wallet.api.backend.beans.UnspentOutput;
|
||||
import com.samourai.wallet.api.backend.beans.WalletResponse;
|
||||
import com.samourai.wallet.bipWallet.BipWallet;
|
||||
import com.samourai.wallet.bipWallet.WalletSupplier;
|
||||
import com.samourai.whirlpool.client.wallet.WhirlpoolWallet;
|
||||
import com.samourai.whirlpool.client.wallet.beans.WhirlpoolUtxo;
|
||||
import com.samourai.whirlpool.client.wallet.data.dataSource.DataSourceConfig;
|
||||
import com.samourai.whirlpool.client.wallet.data.utxo.BasicUtxoSupplier;
|
||||
import com.samourai.whirlpool.client.wallet.data.utxo.UtxoData;
|
||||
import com.samourai.whirlpool.client.wallet.data.utxoConfig.UtxoConfigSupplier;
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.protocol.TransactionInput;
|
||||
import com.sparrowwallet.drongo.protocol.TransactionOutput;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
// manages utxos & wallet indexes
|
||||
public class SparrowUtxoSupplier extends BasicUtxoSupplier {
|
||||
private static final Logger log = LoggerFactory.getLogger(SparrowUtxoSupplier.class);
|
||||
|
||||
public SparrowUtxoSupplier(
|
||||
WhirlpoolWallet whirlpoolWallet,
|
||||
WalletSupplier walletSupplier,
|
||||
UtxoConfigSupplier utxoConfigSupplier,
|
||||
DataSourceConfig dataSourceConfig) {
|
||||
super(whirlpoolWallet, walletSupplier, utxoConfigSupplier, dataSourceConfig);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void refresh() throws Exception {
|
||||
Map<Sha256Hash, BlockTransaction> allTransactions = new HashMap<>();
|
||||
Map<Sha256Hash, String> allTransactionsXpubs = new HashMap<>();
|
||||
List<WalletResponse.Tx> txes = new ArrayList<>();
|
||||
List<UnspentOutput> unspentOutputs = new ArrayList<>();
|
||||
int storedBlockHeight = 0;
|
||||
|
||||
Collection<BipWallet> bipWallets = getWalletSupplier().getWallets();
|
||||
for(BipWallet bipWallet : bipWallets) {
|
||||
String zpub = bipWallet.getBipPub();
|
||||
Wallet wallet = SparrowDataSource.getWallet(zpub);
|
||||
if(wallet == null) {
|
||||
log.debug("No wallet for " + zpub + " found");
|
||||
continue;
|
||||
}
|
||||
|
||||
Map<Sha256Hash, BlockTransaction> walletTransactions = wallet.getWalletTransactions();
|
||||
allTransactions.putAll(walletTransactions);
|
||||
String xpub = bipWallet.getXPub();
|
||||
walletTransactions.keySet().forEach(txid -> allTransactionsXpubs.put(txid, xpub));
|
||||
if(wallet.getStoredBlockHeight() != null) {
|
||||
storedBlockHeight = Math.max(storedBlockHeight, wallet.getStoredBlockHeight());
|
||||
}
|
||||
|
||||
// update wallet index: receive
|
||||
int receiveIndex = wallet.getNode(KeyPurpose.RECEIVE).getHighestUsedIndex() == null ? 0 : wallet.getNode(KeyPurpose.RECEIVE).getHighestUsedIndex() + 1;
|
||||
int account_index = wallet.getMixConfig() != null ? Math.max(receiveIndex, wallet.getMixConfig().getReceiveIndex()) : receiveIndex;
|
||||
bipWallet.getIndexHandlerReceive().set(account_index, false);
|
||||
|
||||
// update wallet index: change
|
||||
int changeIndex = wallet.getNode(KeyPurpose.CHANGE).getHighestUsedIndex() == null ? 0 : wallet.getNode(KeyPurpose.CHANGE).getHighestUsedIndex() + 1;
|
||||
int change_index = wallet.getMixConfig() != null ? Math.max(changeIndex, wallet.getMixConfig().getChangeIndex()) : changeIndex;
|
||||
bipWallet.getIndexHandlerChange().set(change_index, false);
|
||||
|
||||
for(Map.Entry<BlockTransactionHashIndex, WalletNode> utxo : wallet.getSpendableUtxos().entrySet()) {
|
||||
BlockTransaction blockTransaction = wallet.getWalletTransaction(utxo.getKey().getHash());
|
||||
if(blockTransaction != null) {
|
||||
unspentOutputs.add(Whirlpool.getUnspentOutput(utxo.getValue(), blockTransaction, (int)utxo.getKey().getIndex()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for(BlockTransaction blockTransaction : allTransactions.values()) {
|
||||
WalletResponse.Tx tx = new WalletResponse.Tx();
|
||||
tx.block_height = blockTransaction.getHeight();
|
||||
tx.hash = blockTransaction.getHashAsString();
|
||||
tx.locktime = blockTransaction.getTransaction().getLocktime();
|
||||
tx.version = (int)blockTransaction.getTransaction().getVersion();
|
||||
|
||||
tx.inputs = new WalletResponse.TxInput[blockTransaction.getTransaction().getInputs().size()];
|
||||
for(int i = 0; i < blockTransaction.getTransaction().getInputs().size(); i++) {
|
||||
TransactionInput txInput = blockTransaction.getTransaction().getInputs().get(i);
|
||||
tx.inputs[i] = new WalletResponse.TxInput();
|
||||
tx.inputs[i].vin = txInput.getIndex();
|
||||
tx.inputs[i].sequence = txInput.getSequenceNumber();
|
||||
if(allTransactionsXpubs.containsKey(txInput.getOutpoint().getHash())) {
|
||||
tx.inputs[i].prev_out = new WalletResponse.TxOut();
|
||||
tx.inputs[i].prev_out.txid = txInput.getOutpoint().getHash().toString();
|
||||
tx.inputs[i].prev_out.vout = (int)txInput.getOutpoint().getIndex();
|
||||
|
||||
BlockTransaction spentTransaction = allTransactions.get(txInput.getOutpoint().getHash());
|
||||
if(spentTransaction != null) {
|
||||
TransactionOutput spentOutput = spentTransaction.getTransaction().getOutputs().get((int)txInput.getOutpoint().getIndex());
|
||||
tx.inputs[i].prev_out.value = spentOutput.getValue();
|
||||
}
|
||||
|
||||
tx.inputs[i].prev_out.xpub = new UnspentOutput.Xpub();
|
||||
tx.inputs[i].prev_out.xpub.m = allTransactionsXpubs.get(txInput.getOutpoint().getHash());
|
||||
}
|
||||
}
|
||||
|
||||
tx.out = new WalletResponse.TxOutput[blockTransaction.getTransaction().getOutputs().size()];
|
||||
for(int i = 0; i < blockTransaction.getTransaction().getOutputs().size(); i++) {
|
||||
TransactionOutput txOutput = blockTransaction.getTransaction().getOutputs().get(i);
|
||||
tx.out[i] = new WalletResponse.TxOutput();
|
||||
tx.out[i].n = txOutput.getIndex();
|
||||
tx.out[i].value = txOutput.getValue();
|
||||
tx.out[i].xpub = new UnspentOutput.Xpub();
|
||||
tx.out[i].xpub.m = allTransactionsXpubs.get(blockTransaction.getHash());
|
||||
}
|
||||
|
||||
txes.add(tx);
|
||||
}
|
||||
|
||||
// update utxos
|
||||
UnspentOutput[] uos = unspentOutputs.toArray(new UnspentOutput[0]);
|
||||
WalletResponse.Tx[] txs = txes.toArray(new WalletResponse.Tx[0]);
|
||||
UtxoData utxoData = new UtxoData(uos, txs);
|
||||
setValue(utxoData);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte[] _getPrivKey(WhirlpoolUtxo whirlpoolUtxo) throws Exception {
|
||||
UnspentOutput utxo = whirlpoolUtxo.getUtxo();
|
||||
Wallet wallet = SparrowDataSource.getWallet(utxo.xpub.m);
|
||||
Map<BlockTransactionHashIndex, WalletNode> walletUtxos = wallet.getWalletUtxos();
|
||||
WalletNode node = walletUtxos.entrySet().stream()
|
||||
.filter(entry -> entry.getKey().getHash().equals(Sha256Hash.wrap(utxo.tx_hash)) && entry.getKey().getIndex() == utxo.tx_output_n)
|
||||
.map(Map.Entry::getValue)
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalStateException("Cannot find UTXO " + utxo));
|
||||
|
||||
if(node.getWallet().isBip47()) {
|
||||
try {
|
||||
Keystore keystore = node.getWallet().getKeystores().get(0);
|
||||
return keystore.getKey(node).getPrivKeyBytes();
|
||||
} catch(Exception e) {
|
||||
log.error("Error getting private key", e);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -1,10 +1,11 @@
|
|||
package com.sparrowwallet.sparrow.whirlpool.dataSource;
|
||||
|
||||
import com.samourai.wallet.bipWallet.BipDerivation;
|
||||
import com.samourai.wallet.bipWallet.BipWallet;
|
||||
import com.samourai.wallet.client.indexHandler.IIndexHandler;
|
||||
import com.samourai.wallet.hd.AddressType;
|
||||
import com.samourai.wallet.constants.SamouraiAccount;
|
||||
import com.samourai.wallet.hd.Chain;
|
||||
import com.samourai.whirlpool.client.wallet.beans.ExternalDestination;
|
||||
import com.samourai.whirlpool.client.wallet.beans.WhirlpoolAccount;
|
||||
import com.samourai.whirlpool.client.wallet.data.walletState.WalletStateSupplier;
|
||||
import com.samourai.whirlpool.client.whirlpool.WhirlpoolClientConfig;
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
|
@ -30,11 +31,12 @@ public class SparrowWalletStateSupplier implements WalletStateSupplier {
|
|||
}
|
||||
|
||||
@Override
|
||||
public IIndexHandler getIndexHandlerWallet(WhirlpoolAccount whirlpoolAccount, AddressType addressType, Chain chain) {
|
||||
String key = mapKey(whirlpoolAccount, addressType, chain);
|
||||
public IIndexHandler getIndexHandlerWallet(BipWallet bipWallet, Chain chain) {
|
||||
SamouraiAccount samouraiAccount = bipWallet.getAccount();
|
||||
String key = mapKey(bipWallet, chain);
|
||||
IIndexHandler indexHandler = indexHandlerWallets.get(key);
|
||||
if (indexHandler == null) {
|
||||
Wallet wallet = findWallet(whirlpoolAccount);
|
||||
Wallet wallet = findWallet(samouraiAccount);
|
||||
KeyPurpose keyPurpose = (chain == Chain.RECEIVE ? KeyPurpose.RECEIVE : KeyPurpose.CHANGE);
|
||||
WalletNode walletNode = wallet.getNode(keyPurpose);
|
||||
|
||||
|
@ -61,7 +63,8 @@ public class SparrowWalletStateSupplier implements WalletStateSupplier {
|
|||
|
||||
if(externalIndexHandler == null) {
|
||||
Wallet externalWallet = null;
|
||||
if(externalDestination.getPostmixHandler() instanceof SparrowPostmixHandler sparrowPostmixHandler) {
|
||||
if(externalDestination.getPostmixHandlerCustom() != null
|
||||
&& externalDestination.getPostmixHandlerCustom() instanceof SparrowPostmixHandler sparrowPostmixHandler) {
|
||||
externalWallet = sparrowPostmixHandler.getWallet();
|
||||
} else if(externalDestination.getXpub() != null) {
|
||||
externalWallet = SparrowDataSource.getWallet(externalDestination.getXpub());
|
||||
|
@ -80,7 +83,7 @@ public class SparrowWalletStateSupplier implements WalletStateSupplier {
|
|||
|
||||
KeyPurpose keyPurpose = KeyPurpose.fromChildNumber(new ChildNumber(externalDestination.getChain()));
|
||||
WalletNode externalNode = externalWallet.getNode(keyPurpose);
|
||||
externalIndexHandler = new SparrowIndexHandler(externalWallet, externalNode, externalDestination.getStartIndex());
|
||||
externalIndexHandler = new SparrowIndexHandler(externalWallet, externalNode);
|
||||
}
|
||||
|
||||
return externalIndexHandler;
|
||||
|
@ -96,6 +99,16 @@ public class SparrowWalletStateSupplier implements WalletStateSupplier {
|
|||
// nothing required
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isNymClaimed() {
|
||||
return false; // nothing required
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setNymClaimed(boolean value) {
|
||||
// nothing required
|
||||
}
|
||||
|
||||
@Override
|
||||
public void load() throws Exception {
|
||||
// nothing required
|
||||
|
@ -107,17 +120,19 @@ public class SparrowWalletStateSupplier implements WalletStateSupplier {
|
|||
return false;
|
||||
}
|
||||
|
||||
private String mapKey(WhirlpoolAccount whirlpoolAccount, AddressType addressType, Chain chain) {
|
||||
return whirlpoolAccount.name()+"_"+addressType.getPurpose()+"_"+chain.getIndex();
|
||||
private String mapKey(BipWallet bipWallet, Chain chain) {
|
||||
SamouraiAccount samouraiAccount = bipWallet.getAccount();
|
||||
BipDerivation derivation = bipWallet.getDerivation();
|
||||
return samouraiAccount.name()+"_"+derivation.getPurpose()+"_"+chain.getIndex();
|
||||
}
|
||||
|
||||
private Wallet findWallet(WhirlpoolAccount whirlpoolAccount) {
|
||||
private Wallet findWallet(SamouraiAccount samouraiAccount) {
|
||||
Wallet wallet = getWallet();
|
||||
if(wallet == null) {
|
||||
throw new IllegalStateException("Can't find wallet with walletId " + walletId);
|
||||
}
|
||||
|
||||
return Whirlpool.getStandardAccountWallet(whirlpoolAccount, wallet);
|
||||
return Whirlpool.getStandardAccountWallet(samouraiAccount, wallet);
|
||||
}
|
||||
|
||||
private Wallet getWallet() {
|
||||
|
|
|
@ -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,11 @@ open module com.sparrowwallet.sparrow {
|
|||
requires com.sparrowwallet.bokmakierie;
|
||||
requires java.smartcardio;
|
||||
requires com.jcraft.jzlib;
|
||||
// samourai dependencies
|
||||
requires com.samourai.whirlpool.client;
|
||||
requires com.samourai.whirlpool.protocol;
|
||||
requires com.samourai.extlibj;
|
||||
requires com.samourai.soroban.client;
|
||||
requires com.samourai.http.client;
|
||||
requires com.samourai.bitcoinj;
|
||||
}
|
|
@ -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