various updates and fixes

This commit is contained in:
Craig Raw 2024-03-13 15:58:43 +02:00
parent da74089969
commit c9d1650ed4
25 changed files with 261 additions and 205 deletions

View file

@ -124,8 +124,8 @@ dependencies {
exclude group: 'org.slf4j' exclude group: 'org.slf4j'
} }
implementation('com.sparrowwallet.bokmakierie:bokmakierie:1.0') implementation('com.sparrowwallet.bokmakierie:bokmakierie:1.0')
implementation('io.samourai.code.whirlpool:whirlpool-client:1.0.0-beta10') implementation('io.samourai.code.whirlpool:whirlpool-client:1.0.0-beta13')
implementation('io.samourai.code.wallet:java-http-client:2.0.0-beta3') implementation('io.samourai.code.wallet:java-http-client:2.0.0-beta4')
implementation('io.reactivex.rxjava2:rxjava:2.2.15') implementation('io.reactivex.rxjava2:rxjava:2.2.15')
implementation('io.reactivex.rxjava2:rxjavafx:2.2.2') implementation('io.reactivex.rxjava2:rxjavafx:2.2.2')
implementation('org.apache.commons:commons-lang3:3.7') implementation('org.apache.commons:commons-lang3:3.7')
@ -490,7 +490,6 @@ extraJavaModuleInfo {
exports('co.nstant.in.cbor.model') exports('co.nstant.in.cbor.model')
exports('co.nstant.in.cbor.builder') exports('co.nstant.in.cbor.builder')
} }
// begin samourai dependencies
module('commons-codec-1.10.jar', 'commons.codec', '1.10') { module('commons-codec-1.10.jar', 'commons.codec', '1.10') {
exports('org.apache.commons.codec') exports('org.apache.commons.codec')
} }
@ -507,7 +506,6 @@ extraJavaModuleInfo {
exports('com.lambdaworks.codec') exports('com.lambdaworks.codec')
exports('com.lambdaworks.crypto') exports('com.lambdaworks.crypto')
} }
// end samourai dependencies
module('okio-1.6.0.jar', 'com.squareup.okio', '1.6.0') { module('okio-1.6.0.jar', 'com.squareup.okio', '1.6.0') {
exports('okio') exports('okio')
} }

2
drongo

@ -1 +1 @@
Subproject commit c8165e154a4088262cdf9428f8f8a6ef95db5140 Subproject commit 3b8435ca37d00d370d859fa9dbc1631d3cdcae45

View file

@ -70,7 +70,7 @@ public class MixStatusCell extends TreeTableCell<Entry, UtxoEntry.MixStatus> {
} }
private void setMixFail(MixFailReason mixFailReason, String mixError, Long mixErrorTimestamp) { private void setMixFail(MixFailReason mixFailReason, String mixError, Long mixErrorTimestamp) {
if(mixFailReason != MixFailReason.CANCEL) { if(mixFailReason != MixFailReason.STOP_UTXO && !mixFailReason.isSilent()) {
long elapsed = mixErrorTimestamp == null ? 0L : System.currentTimeMillis() - mixErrorTimestamp; long elapsed = mixErrorTimestamp == null ? 0L : System.currentTimeMillis() - mixErrorTimestamp;
if(elapsed >= ERROR_DISPLAY_MILLIS) { if(elapsed >= ERROR_DISPLAY_MILLIS) {
//Old error, don't set again. //Old error, don't set again.
@ -116,24 +116,10 @@ public class MixStatusCell extends TreeTableCell<Entry, UtxoEntry.MixStatus> {
progressIndicator.setProgress(mixProgress.getMixStep().getProgressPercent() == 100 ? -1 : mixProgress.getMixStep().getProgressPercent() / 100.0); progressIndicator.setProgress(mixProgress.getMixStep().getProgressPercent() == 100 ? -1 : mixProgress.getMixStep().getProgressPercent() / 100.0);
setGraphic(progressIndicator); setGraphic(progressIndicator);
Tooltip tt = new Tooltip(); Tooltip tt = new Tooltip();
String status = mixProgress.getMixStep().getMessage().substring(0, 1).toUpperCase(Locale.ROOT) + mixProgress.getMixStep().getMessage().substring(1); String status = mixProgress.getMixStep().getMessage().replaceAll("_", " ");
status = status.substring(0, 1).toUpperCase(Locale.ROOT) + status.substring(1).toLowerCase(Locale.ROOT);
tt.setText(status); tt.setText(status);
setTooltip(tt); setTooltip(tt);
// TODO nbRegisteredInputs is not available anymore
/*
if(mixProgress.getMixStep() == MixStep.REGISTERED_INPUT) {
tt.setOnShowing(event -> {
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(utxoEntry.getWallet());
Whirlpool.RegisteredInputsService registeredInputsService = new Whirlpool.RegisteredInputsService(whirlpool, mixProgress.getPoolId());
registeredInputsService.setOnSucceeded(eventStateHandler -> {
if(registeredInputsService.getValue() != null) {
tt.setText(status + " (1 of " + registeredInputsService.getValue() + ")");
}
});
registeredInputsService.start();
});
}*/
} else { } else {
setGraphic(null); setGraphic(null);
setTooltip(null); setTooltip(null);

View file

@ -256,7 +256,10 @@ public class DbPersistence implements Persistence {
} }
for(Sha256Hash txid : referencedTxIds) { for(Sha256Hash txid : referencedTxIds) {
BlockTransaction blkTx = wallet.getTransactions().get(txid); BlockTransaction blkTx = wallet.getTransactions().get(txid);
blockTransactionDao.addOrUpdate(wallet, txid, blkTx); //May be null for a nested wallet if still updating
if(blkTx != null) {
blockTransactionDao.addOrUpdate(wallet, txid, blkTx);
}
} }
if(!dirtyPersistables.clearHistory) { if(!dirtyPersistables.clearHistory) {
DetachedLabelDao detachedLabelDao = handle.attach(DetachedLabelDao.class); DetachedLabelDao detachedLabelDao = handle.attach(DetachedLabelDao.class);

View file

@ -1,7 +1,7 @@
package com.sparrowwallet.sparrow.net; package com.sparrowwallet.sparrow.net;
import com.google.common.net.HostAndPort; import com.google.common.net.HostAndPort;
import com.samourai.wallet.api.backend.beans.HttpException; import com.samourai.wallet.httpClient.HttpResponseException;
import com.sparrowwallet.drongo.Network; import com.sparrowwallet.drongo.Network;
import com.sparrowwallet.drongo.Utils; import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.protocol.Sha256Hash; import com.sparrowwallet.drongo.protocol.Sha256Hash;
@ -158,7 +158,7 @@ public enum BroadcastSource {
} catch(Exception e) { } catch(Exception e) {
throw new BroadcastException("Could not retrieve txid from broadcast, server returned: " + response); throw new BroadcastException("Could not retrieve txid from broadcast, server returned: " + response);
} }
} catch(HttpException e) { } catch(HttpResponseException e) {
throw new BroadcastException("Could not broadcast transaction, server returned " + e.getStatusCode() + ": " + e.getResponseBody()); throw new BroadcastException("Could not broadcast transaction, server returned " + e.getStatusCode() + ": " + e.getResponseBody());
} catch(Exception e) { } catch(Exception e) {
log.error("Could not post transaction via " + getName(), e); log.error("Could not post transaction via " + getName(), e);

View file

@ -1,6 +1,6 @@
package com.sparrowwallet.sparrow.net; package com.sparrowwallet.sparrow.net;
import com.samourai.wallet.api.backend.beans.HttpException; import com.samourai.wallet.httpClient.HttpResponseException;
import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.event.ExchangeRatesUpdatedEvent; import com.sparrowwallet.sparrow.event.ExchangeRatesUpdatedEvent;
import javafx.concurrent.ScheduledService; import javafx.concurrent.ScheduledService;
@ -107,7 +107,7 @@ public enum ExchangeSource {
if(log.isDebugEnabled()) { if(log.isDebugEnabled()) {
log.warn("Error retrieving historical currency rates", e); log.warn("Error retrieving historical currency rates", e);
} else { } else {
if(e instanceof HttpException httpException && httpException.getStatusCode() == 404) { if(e instanceof HttpResponseException httpException && httpException.getStatusCode() == 404) {
log.warn("Error retrieving historical currency rates (" + e.getMessage() + "). BTC-" + currency.getCurrencyCode() + " may not be supported by " + this); log.warn("Error retrieving historical currency rates (" + e.getMessage() + "). BTC-" + currency.getCurrencyCode() + " may not be supported by " + this);
} else { } else {
log.warn("Error retrieving historical currency rates (" + e.getMessage() + ")"); log.warn("Error retrieving historical currency rates (" + e.getMessage() + ")");

View file

@ -5,6 +5,7 @@ import com.samourai.http.client.JettyHttpClientService;
import com.samourai.wallet.httpClient.HttpUsage; import com.samourai.wallet.httpClient.HttpUsage;
import com.samourai.wallet.httpClient.IHttpClient; import com.samourai.wallet.httpClient.IHttpClient;
import com.samourai.wallet.util.AsyncUtil; import com.samourai.wallet.util.AsyncUtil;
import com.samourai.wallet.util.ThreadUtil;
import com.samourai.whirlpool.client.utils.ClientUtils; import com.samourai.whirlpool.client.utils.ClientUtils;
import io.reactivex.Observable; import io.reactivex.Observable;
import javafx.concurrent.Service; import javafx.concurrent.Service;
@ -17,25 +18,20 @@ public class HttpClientService extends JettyHttpClientService {
private static final int REQUEST_TIMEOUT = 120000; private static final int REQUEST_TIMEOUT = 120000;
public HttpClientService(HostAndPort torProxy) { public HttpClientService(HostAndPort torProxy) {
super(REQUEST_TIMEOUT, super(REQUEST_TIMEOUT, ClientUtils.USER_AGENT, new HttpProxySupplier(torProxy));
ClientUtils.USER_AGENT,
new HttpProxySupplier(torProxy));
} }
public <T> T requestJson(String url, Class<T> responseType, Map<String, String> headers) throws Exception { public <T> T requestJson(String url, Class<T> responseType, Map<String, String> headers) throws Exception {
return getHttpClient(HttpUsage.BACKEND) return getHttpClient(HttpUsage.BACKEND).getJson(url, responseType, headers);
.getJson(url, responseType, headers);
} }
public <T> Observable<Optional<T>> postJson(String url, Class<T> responseType, Map<String, String> headers, Object body) { public <T> Observable<Optional<T>> postJson(String url, Class<T> responseType, Map<String, String> headers, Object body) {
return getHttpClient(HttpUsage.BACKEND) return getHttpClient(HttpUsage.BACKEND).postJson(url, responseType, headers, body).toObservable();
.postJson(url, responseType, headers, body).toObservable();
} }
public String postString(String url, Map<String, String> headers, String contentType, String content) throws Exception { public String postString(String url, Map<String, String> headers, String contentType, String content) throws Exception {
IHttpClient httpClient = getHttpClient(HttpUsage.BACKEND); IHttpClient httpClient = getHttpClient(HttpUsage.BACKEND);
return AsyncUtil.getInstance().blockingGet( return AsyncUtil.getInstance().blockingGet(httpClient.postString(url, headers, contentType, content)).get();
httpClient.postString(url, headers, contentType, content)).get();
} }
public HostAndPort getTorProxy() { public HostAndPort getTorProxy() {
@ -64,6 +60,7 @@ public class HttpClientService extends JettyHttpClientService {
protected Task<Boolean> createTask() { protected Task<Boolean> createTask() {
return new Task<>() { return new Task<>() {
protected Boolean call() throws Exception { protected Boolean call() throws Exception {
ThreadUtil.getInstance().getExecutorService().shutdown();
httpClientService.stop(); httpClientService.stop();
return true; return true;
} }

View file

@ -21,7 +21,7 @@ public class HttpProxySupplier implements IHttpProxySupplier {
if (hostAndPort == null) { if (hostAndPort == null) {
return null; return null;
} }
// TODO verify
return new HttpProxy(HttpProxyProtocol.SOCKS, hostAndPort.getHost(), hostAndPort.getPort()); return new HttpProxy(HttpProxyProtocol.SOCKS, hostAndPort.getHost(), hostAndPort.getPort());
} }

View file

@ -2,7 +2,7 @@ package com.sparrowwallet.sparrow.payjoin;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.samourai.wallet.api.backend.beans.HttpException; import com.samourai.wallet.httpClient.HttpResponseException;
import com.sparrowwallet.drongo.protocol.Script; import com.sparrowwallet.drongo.protocol.Script;
import com.sparrowwallet.drongo.protocol.Transaction; import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.protocol.TransactionInput; import com.sparrowwallet.drongo.protocol.TransactionInput;
@ -87,7 +87,7 @@ public class Payjoin {
checkProposal(psbt, proposalPsbt, changeOutputIndex, maxAdditionalFeeContribution, allowOutputSubstitution); checkProposal(psbt, proposalPsbt, changeOutputIndex, maxAdditionalFeeContribution, allowOutputSubstitution);
return proposalPsbt; return proposalPsbt;
} catch(HttpException e) { } catch(HttpResponseException e) {
Gson gson = new Gson(); Gson gson = new Gson();
PayjoinReceiverError payjoinReceiverError = gson.fromJson(e.getResponseBody(), PayjoinReceiverError.class); PayjoinReceiverError payjoinReceiverError = gson.fromJson(e.getResponseBody(), PayjoinReceiverError.class);
log.warn("Payjoin receiver returned an error of " + payjoinReceiverError.getErrorCode() + " (" + payjoinReceiverError.getMessage() + ")"); log.warn("Payjoin receiver returned an error of " + payjoinReceiverError.getErrorCode() + " (" + payjoinReceiverError.getMessage() + ")");

View file

@ -25,6 +25,8 @@ import javafx.application.Platform;
import javafx.beans.property.ObjectProperty; import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import javafx.event.ActionEvent; import javafx.event.ActionEvent;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.control.*; import javafx.scene.control.*;
@ -238,10 +240,19 @@ public class CounterpartyController extends SorobanController {
SorobanWalletCounterparty sorobanWalletCounterparty = sorobanWalletService.getSorobanWalletCounterparty(cahootsWallet); SorobanWalletCounterparty sorobanWalletCounterparty = sorobanWalletService.getSorobanWalletCounterparty(cahootsWallet);
sorobanWalletCounterparty.setTimeoutMeetingMs(TIMEOUT_MS); sorobanWalletCounterparty.setTimeoutMeetingMs(TIMEOUT_MS);
try { Service<SorobanRequestMessage> receiveMeetingService = new Service<>() {
// TODO run in background thread? @Override
SorobanRequestMessage requestMessage = sorobanWalletCounterparty.receiveMeetingRequest(); protected Task<SorobanRequestMessage> createTask() {
return new Task<>() {
@Override
protected SorobanRequestMessage call() throws Exception {
return sorobanWalletCounterparty.receiveMeetingRequest();
}
};
}
};
receiveMeetingService.setOnSucceeded(event -> {
SorobanRequestMessage requestMessage = receiveMeetingService.getValue();
PaymentCode paymentCodeInitiator = requestMessage.getSender(); PaymentCode paymentCodeInitiator = requestMessage.getSender();
CahootsType cahootsType = requestMessage.getType(); CahootsType cahootsType = requestMessage.getType();
updateMixPartner(paymentCodeInitiator, cahootsType); updateMixPartner(paymentCodeInitiator, cahootsType);
@ -261,11 +272,14 @@ public class CounterpartyController extends SorobanController {
mixingPartner.setVisible(false); mixingPartner.setVisible(false);
requestUserAttention(); requestUserAttention();
}); });
} catch(Exception e) { });
receiveMeetingService.setOnFailed(event -> {
Throwable e = event.getSource().getException();
log.error("Failed to receive meeting request", e); log.error("Failed to receive meeting request", e);
mixingPartner.setVisible(false); mixingPartner.setVisible(false);
requestUserAttention(); requestUserAttention();
} });
receiveMeetingService.start();
} }
private void updateMixPartner(PaymentCode paymentCodeInitiator, CahootsType cahootsType) { private void updateMixPartner(PaymentCode paymentCodeInitiator, CahootsType cahootsType) {
@ -308,32 +322,44 @@ public class CounterpartyController extends SorobanController {
CahootsContext cahootsContext = CahootsContext.newCounterparty(cahootsWallet, cahootsType, account); CahootsContext cahootsContext = CahootsContext.newCounterparty(cahootsWallet, cahootsType, account);
Consumer<OnlineCahootsMessage> onProgress = cahootsMessage -> { Consumer<OnlineCahootsMessage> onProgress = cahootsMessage -> {
if(cahootsMessage != null) { if(cahootsMessage != null) {
Cahoots cahoots = cahootsMessage.getCahoots(); Platform.runLater(() -> {
sorobanProgressBar.setProgress((double)(cahoots.getStep() + 1) / 5); Cahoots cahoots = cahootsMessage.getCahoots();
sorobanProgressBar.setProgress((double)(cahoots.getStep() + 1) / 5);
if(cahoots.getStep() == 3) { if(cahoots.getStep() == 3) {
sorobanProgressLabel.setText("Your mix partner is reviewing the transaction..."); sorobanProgressLabel.setText("Your mix partner is reviewing the transaction...");
step3Timer.start(); step3Timer.start();
} else if(cahoots.getStep() >= 4) { } else if(cahoots.getStep() >= 4) {
try { try {
Transaction transaction = getTransaction(cahoots); Transaction transaction = getTransaction(cahoots);
if(transaction != null) { if(transaction != null) {
transactionProperty.set(transaction); transactionProperty.set(transaction);
updateTransactionDiagram(transactionDiagram, wallet, null, transaction); updateTransactionDiagram(transactionDiagram, wallet, null, transaction);
next(); next();
}
} catch(PSBTParseException e) {
log.error("Invalid collaborative PSBT created", e);
step3Desc.setText("Invalid transaction created.");
sorobanProgressLabel.setVisible(false);
} }
} catch(PSBTParseException e) {
log.error("Invalid collaborative PSBT created", e);
step3Desc.setText("Invalid transaction created.");
sorobanProgressLabel.setVisible(false);
} }
} });
} }
}; };
try {
// TODO run in background thread? Service<Cahoots> cahootsService = new Service<>() {
Cahoots result = sorobanWalletCounterparty.counterparty(cahootsContext, initiatorPaymentCode, onProgress); @Override
} catch (Exception error) { protected Task<Cahoots> createTask() {
return new Task<>() {
@Override
protected Cahoots call() throws Exception {
return sorobanWalletCounterparty.counterparty(cahootsContext, initiatorPaymentCode, onProgress);
}
};
}
};
cahootsService.setOnFailed(event -> {
Throwable error = event.getSource().getException();
log.error("Error creating mix transaction", error); log.error("Error creating mix transaction", error);
String cutFrom = "Exception: "; String cutFrom = "Exception: ";
int index = error.getMessage().lastIndexOf(cutFrom); int index = error.getMessage().lastIndexOf(cutFrom);
@ -341,7 +367,8 @@ public class CounterpartyController extends SorobanController {
msg = msg.replace("#Cahoots", "mix transaction"); msg = msg.replace("#Cahoots", "mix transaction");
step3Desc.setText(msg); step3Desc.setText(msg);
sorobanProgressLabel.setVisible(false); sorobanProgressLabel.setVisible(false);
} });
cahootsService.start();
} catch(Exception e) { } catch(Exception e) {
log.error("Error creating mix transaction", e); log.error("Error creating mix transaction", e);
sorobanProgressLabel.setText(e.getMessage()); sorobanProgressLabel.setText(e.getMessage());

View file

@ -38,8 +38,6 @@ import com.sparrowwallet.sparrow.paynym.PayNymAddress;
import com.sparrowwallet.sparrow.paynym.PayNymDialog; import com.sparrowwallet.sparrow.paynym.PayNymDialog;
import com.sparrowwallet.sparrow.paynym.PayNymService; import com.sparrowwallet.sparrow.paynym.PayNymService;
import io.reactivex.Observable; import io.reactivex.Observable;
import io.reactivex.rxjavafx.schedulers.JavaFxScheduler;
import io.reactivex.schedulers.Schedulers;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.property.ObjectProperty; import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleObjectProperty;
@ -47,6 +45,8 @@ import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty; import javafx.beans.property.StringProperty;
import javafx.beans.value.ChangeListener; import javafx.beans.value.ChangeListener;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import javafx.event.ActionEvent; import javafx.event.ActionEvent;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.control.*; import javafx.scene.control.*;
@ -431,79 +431,99 @@ public class InitiatorController extends SorobanController {
public void onResponse(SorobanResponseMessage sorobanResponse) throws Exception { public void onResponse(SorobanResponseMessage sorobanResponse) throws Exception {
super.onResponse(sorobanResponse); super.onResponse(sorobanResponse);
requestUserAttention(); Platform.runLater(() -> {
if(sorobanResponse.isAccept()) { requestUserAttention();
sorobanProgressBar.setProgress(0.1); if(sorobanResponse.isAccept()) {
sorobanProgressLabel.setText("Mix partner accepted!"); sorobanProgressBar.setProgress(0.1);
} else { sorobanProgressLabel.setText("Mix partner accepted!");
step2Desc.setText("Mix partner declined."); } else {
sorobanProgressLabel.setVisible(false); step2Desc.setText("Mix partner declined.");
} sorobanProgressLabel.setVisible(false);
}
});
} }
@Override @Override
public void onInteraction(OnlineSorobanInteraction interaction) throws Exception { public void onInteraction(OnlineSorobanInteraction interaction) throws Exception {
SorobanInteraction originInteraction = interaction.getInteraction(); SorobanInteraction originInteraction = interaction.getInteraction();
if (originInteraction instanceof TxBroadcastInteraction) { if(originInteraction instanceof TxBroadcastInteraction) {
Boolean accepted = (Boolean)Platform.enterNestedEventLoop(transactionAccepted); Platform.runLater(() -> {
if(accepted) { try {
interaction.sorobanAccept(); Boolean accepted = (Boolean)Platform.enterNestedEventLoop(transactionAccepted);
} else { if(accepted) {
interaction.sorobanReject("Mix partner declined to broadcast the transaction."); interaction.sorobanAccept();
} } else {
interaction.sorobanReject("Mix partner declined to broadcast the transaction.");
}
} catch(Exception e) {
log.error("Error accepting Soroban interaction", e);
}
});
} else { } else {
throw new Exception("Unknown interaction: "+originInteraction.getTypeInteraction()); throw new Exception("Unknown interaction: "+originInteraction.getTypeInteraction());
} }
} }
@Override @Override
public void progress(OnlineCahootsMessage message) { public void progress(OnlineCahootsMessage cahootsMessage) {
super.progress(message); super.progress(cahootsMessage);
OnlineCahootsMessage cahootsMessage = (OnlineCahootsMessage)message;
if(cahootsMessage != null) { if(cahootsMessage != null) {
Cahoots cahoots = cahootsMessage.getCahoots(); Platform.runLater(() -> {
sorobanProgressBar.setProgress((double)(cahoots.getStep() + 1) / 5); Cahoots cahoots = cahootsMessage.getCahoots();
sorobanProgressBar.setProgress((double)(cahoots.getStep() + 1) / 5);
if(cahoots.getStep() >= 3) { if(cahoots.getStep() >= 3) {
try { try {
Transaction transaction = getTransaction(cahoots); Transaction transaction = getTransaction(cahoots);
if(transaction != null) { if(transaction != null) {
transactionProperty.set(transaction); transactionProperty.set(transaction);
if(cahoots.getStep() == 3) { if(cahoots.getStep() == 3) {
next(); next();
step3Timer.start(e -> { step3Timer.start(e -> {
if(stepProperty.get() != Step.BROADCAST && stepProperty.get() != Step.REBROADCAST) { if(stepProperty.get() != Step.BROADCAST && stepProperty.get() != Step.REBROADCAST) {
step3Desc.setText("Transaction declined due to timeout."); step3Desc.setText("Transaction declined due to timeout.");
transactionAccepted.set(Boolean.FALSE); transactionAccepted.set(Boolean.FALSE);
} }
}); });
} else if(cahoots.getStep() == 4) { } else if(cahoots.getStep() == 4) {
next(); next();
broadcastTransaction(); broadcastTransaction();
}
} }
} catch(PSBTParseException e) {
log.error("Invalid collaborative PSBT created", e);
step2Desc.setText("Invalid transaction created.");
sorobanProgressLabel.setVisible(false);
} }
} catch(PSBTParseException e) {
log.error("Invalid collaborative PSBT created", e);
step2Desc.setText("Invalid transaction created.");
sorobanProgressLabel.setVisible(false);
} }
} });
} }
} }
}; };
SorobanWalletService sorobanWalletService = soroban.getSorobanWalletService(); SorobanWalletService sorobanWalletService = soroban.getSorobanWalletService();
sorobanProgressLabel.setText("Waiting for mix partner..."); sorobanProgressLabel.setText("Waiting for mix partner...");
try {
// TODO run in background thread? Service<Cahoots> cahootsService = new Service<>() {
Cahoots result = sorobanWalletService.getSorobanWalletInitiator(cahootsWallet).meetAndInitiate(cahootsContext, paymentCodeCounterparty, listener); @Override
} catch (Exception error){ protected Task<Cahoots> createTask() {
return new Task<>() {
@Override
protected Cahoots call() throws Exception {
return sorobanWalletService.getSorobanWalletInitiator(cahootsWallet).meetAndInitiate(cahootsContext, paymentCodeCounterparty, listener);
}
};
}
};
cahootsService.setOnFailed(event -> {
Throwable error = event.getSource().getException();
log.error("Error receiving meeting response", error); log.error("Error receiving meeting response", error);
step2Desc.setText(getErrorMessage(error)); step2Desc.setText(getErrorMessage(error));
sorobanProgressLabel.setVisible(false); sorobanProgressLabel.setVisible(false);
meetingFail.setVisible(true); meetingFail.setVisible(true);
requestUserAttention(); requestUserAttention();
} });
cahootsService.start();
}, error -> { }, error -> {
log.error("Could not retrieve payment code", error); log.error("Could not retrieve payment code", error);
if(error.getMessage().endsWith("404")) { if(error.getMessage().endsWith("404")) {

View file

@ -79,26 +79,4 @@ public class Soroban {
public SorobanWalletService getSorobanWalletService() { public SorobanWalletService getSorobanWalletService() {
return sorobanWalletService; return sorobanWalletService;
} }
public void stop() {
AppServices.getHttpClientService().stop();
}
public static class ShutdownService extends Service<Boolean> {
private final Soroban soroban;
public ShutdownService(Soroban soroban) {
this.soroban = soroban;
}
@Override
protected Task<Boolean> createTask() {
return new Task<>() {
protected Boolean call() throws Exception {
soroban.stop();
return true;
}
};
}
}
} }

View file

@ -53,14 +53,7 @@ public class SorobanServices {
public void walletTabsClosed(WalletTabsClosedEvent event) { public void walletTabsClosed(WalletTabsClosedEvent event) {
for(WalletTabData walletTabData : event.getClosedWalletTabData()) { for(WalletTabData walletTabData : event.getClosedWalletTabData()) {
String walletId = walletTabData.getStorage().getWalletId(walletTabData.getWallet()); String walletId = walletTabData.getStorage().getWalletId(walletTabData.getWallet());
Soroban soroban = sorobanMap.remove(walletId); sorobanMap.remove(walletId);
if(soroban != null) {
Soroban.ShutdownService shutdownService = new Soroban.ShutdownService(soroban);
shutdownService.setOnFailed(failedEvent -> {
log.error("Failed to shutdown soroban", failedEvent.getSource().getException());
});
shutdownService.start();
}
} }
} }
} }

View file

@ -26,13 +26,12 @@ import java.util.List;
public class SparrowCahootsWallet extends AbstractCahootsWallet { public class SparrowCahootsWallet extends AbstractCahootsWallet {
private final Wallet wallet; private final Wallet wallet;
private HD_Wallet bip84w; private final HD_Wallet bip84w;
private final int account; private final int account;
private List<CahootsUtxo> utxos; private final List<CahootsUtxo> utxos;
public SparrowCahootsWallet(ChainSupplier chainSupplier, Wallet wallet, HD_Wallet bip84w, int bip47Account) { public SparrowCahootsWallet(ChainSupplier chainSupplier, Wallet wallet, HD_Wallet bip84w, int bip47Account) {
super(chainSupplier, bip84w.getFingerprint(), super(chainSupplier, bip84w.getFingerprint(), new BIP47Wallet(bip84w).getAccount(bip47Account));
new BIP47Wallet(bip84w).getAccount(bip47Account));
this.wallet = wallet; this.wallet = wallet;
this.bip84w = bip84w; this.bip84w = bip84w;
this.account = wallet.getAccountIndex(); this.account = wallet.getAccountIndex();
@ -107,7 +106,7 @@ public class SparrowCahootsWallet extends AbstractCahootsWallet {
throw new IllegalStateException("Cannot add BIP47 UTXO", e); throw new IllegalStateException("Cannot add BIP47 UTXO", e);
} }
} else { } else {
HD_Address hdAddress = bip84w.getAddressAt(index, unspentOutput); HD_Address hdAddress = bip84w.getAddressAt(account, unspentOutput);
cahootsUtxo = new CahootsUtxo(myTransactionOutPoint, node.getDerivationPath(), null, hdAddress.getECKey().getPrivKeyBytes()); cahootsUtxo = new CahootsUtxo(myTransactionOutPoint, node.getDerivationPath(), null, hdAddress.getECKey().getPrivKeyBytes());
} }

View file

@ -43,7 +43,7 @@ public class MixTableCell extends TableCell {
private String getMixFail(UtxoEntry.MixStatus mixStatus) { private String getMixFail(UtxoEntry.MixStatus mixStatus) {
long elapsed = mixStatus.getMixErrorTimestamp() == null ? 0L : System.currentTimeMillis() - mixStatus.getMixErrorTimestamp(); long elapsed = mixStatus.getMixErrorTimestamp() == null ? 0L : System.currentTimeMillis() - mixStatus.getMixErrorTimestamp();
if(mixStatus.getMixFailReason() == MixFailReason.CANCEL || elapsed >= ERROR_DISPLAY_MILLIS) { if(mixStatus.getMixFailReason() == MixFailReason.STOP_UTXO || mixStatus.getMixFailReason().isSilent() || elapsed >= ERROR_DISPLAY_MILLIS) {
return getMixCountOnly(mixStatus); return getMixCountOnly(mixStatus);
} }

View file

@ -129,9 +129,10 @@ public class WalletTransactionsEntry extends Entry {
private static void getWalletTransactions(Wallet wallet, Map<BlockTransaction, WalletTransaction> walletTransactionMap, WalletNode purposeNode) { private static void getWalletTransactions(Wallet wallet, Map<BlockTransaction, WalletTransaction> walletTransactionMap, WalletNode purposeNode) {
KeyPurpose keyPurpose = purposeNode.getKeyPurpose(); KeyPurpose keyPurpose = purposeNode.getKeyPurpose();
List<WalletNode> childNodes = new ArrayList<>(purposeNode.getChildren()); List<WalletNode> childNodes = new ArrayList<>(purposeNode.getChildren());
Wallet transactionsWallet = wallet.isNested() ? wallet.getMasterWallet() : wallet;
for(WalletNode addressNode : childNodes) { for(WalletNode addressNode : childNodes) {
for(BlockTransactionHashIndex hashIndex : addressNode.getTransactionOutputs()) { for(BlockTransactionHashIndex hashIndex : addressNode.getTransactionOutputs()) {
BlockTransaction inputTx = wallet.getWalletTransaction(hashIndex.getHash()); BlockTransaction inputTx = transactionsWallet.getWalletTransaction(hashIndex.getHash());
//A null inputTx here means the wallet is still updating - ignore as the WalletHistoryChangedEvent will run this again //A null inputTx here means the wallet is still updating - ignore as the WalletHistoryChangedEvent will run this again
if(inputTx != null) { if(inputTx != null) {
WalletTransaction inputWalletTx = walletTransactionMap.get(inputTx); WalletTransaction inputWalletTx = walletTransactionMap.get(inputTx);
@ -142,7 +143,7 @@ public class WalletTransactionsEntry extends Entry {
inputWalletTx.incoming.put(hashIndex, keyPurpose); inputWalletTx.incoming.put(hashIndex, keyPurpose);
if(hashIndex.getSpentBy() != null) { if(hashIndex.getSpentBy() != null) {
BlockTransaction outputTx = wallet.getWalletTransaction(hashIndex.getSpentBy().getHash()); BlockTransaction outputTx = transactionsWallet.getWalletTransaction(hashIndex.getSpentBy().getHash());
if(outputTx != null) { if(outputTx != null) {
WalletTransaction outputWalletTx = walletTransactionMap.get(outputTx); WalletTransaction outputWalletTx = walletTransactionMap.get(outputTx);
if(outputWalletTx == null) { if(outputWalletTx == null) {

View file

@ -6,6 +6,7 @@ import com.sparrowwallet.drongo.wallet.WalletNode;
import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.io.Config; import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.whirlpool.Whirlpool; import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
import javafx.application.Platform;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -92,7 +93,8 @@ public class WalletUtxosEntry extends Entry {
calculateDuplicates(); calculateDuplicates();
calculateDust(); calculateDust();
updateMixProgress(); //Update mix status after SparrowUtxoSupplier has refreshed
Platform.runLater(this::updateMixProgress);
} }
public long getBalance() { public long getBalance() {

View file

@ -33,10 +33,7 @@ import com.sparrowwallet.drongo.ExtendedKey;
import com.sparrowwallet.drongo.KeyPurpose; import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.Network; import com.sparrowwallet.drongo.Network;
import com.sparrowwallet.drongo.Utils; import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.protocol.Sha256Hash; import com.sparrowwallet.drongo.protocol.*;
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.*;
import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.EventManager;
@ -71,7 +68,6 @@ public class Whirlpool {
public static final int DEFAULT_MIXTO_MIN_MIXES = 3; public static final int DEFAULT_MIXTO_MIN_MIXES = 3;
public static final int DEFAULT_MIXTO_RANDOM_FACTOR = 4; public static final int DEFAULT_MIXTO_RANDOM_FACTOR = 4;
private final WhirlpoolWalletService whirlpoolWalletService; private final WhirlpoolWalletService whirlpoolWalletService;
private final WhirlpoolWalletConfig config; private final WhirlpoolWalletConfig config;
private WhirlpoolInfo whirlpoolInfo; private WhirlpoolInfo whirlpoolInfo;
@ -89,11 +85,10 @@ public class Whirlpool {
private final BooleanProperty stoppingProperty = new SimpleBooleanProperty(false); private final BooleanProperty stoppingProperty = new SimpleBooleanProperty(false);
private final BooleanProperty mixingProperty = new SimpleBooleanProperty(false); private final BooleanProperty mixingProperty = new SimpleBooleanProperty(false);
public Whirlpool() { public Whirlpool(Integer storedBlockHeight) {
this.whirlpoolWalletService = new WhirlpoolWalletService(); this.whirlpoolWalletService = new WhirlpoolWalletService();
Integer storedBlockHeight = null; // TODO
this.config = computeWhirlpoolWalletConfig(storedBlockHeight); this.config = computeWhirlpoolWalletConfig(storedBlockHeight);
this.whirlpoolInfo = null; // instanciated by getWhirlpoolInfo() this.whirlpoolInfo = null; // instantiated by getWhirlpoolInfo()
WhirlpoolEventService.getInstance().register(this); WhirlpoolEventService.getInstance().register(this);
} }
@ -112,17 +107,14 @@ public class Whirlpool {
} }
private DataSourceConfig computeDataSourceConfig(Integer storedBlockHeight) { private DataSourceConfig computeDataSourceConfig(Integer storedBlockHeight) {
return new DataSourceConfig( return new DataSourceConfig(SparrowMinerFeeSupplier.getInstance(), new SparrowChainSupplier(storedBlockHeight), BIP_FORMAT.PROVIDER, BIP_WALLETS.WHIRLPOOL);
SparrowMinerFeeSupplier.getInstance(),
new SparrowChainSupplier(storedBlockHeight),
BIP_FORMAT.PROVIDER,
BIP_WALLETS.WHIRLPOOL);
} }
private WhirlpoolInfo getWhirlpoolInfo() { private WhirlpoolInfo getWhirlpoolInfo() {
if (whirlpoolInfo == null) { if(whirlpoolInfo == null) {
whirlpoolInfo = new WhirlpoolInfo(SparrowMinerFeeSupplier.getInstance(), config); whirlpoolInfo = new WhirlpoolInfo(SparrowMinerFeeSupplier.getInstance(), config);
} }
return whirlpoolInfo; return whirlpoolInfo;
} }
@ -144,8 +136,7 @@ public class Whirlpool {
public Tx0 broadcastTx0(Pool pool, Collection<BlockTransactionHashIndex> utxos) throws Exception { public Tx0 broadcastTx0(Pool pool, Collection<BlockTransactionHashIndex> utxos) throws Exception {
WhirlpoolWallet whirlpoolWallet = getWhirlpoolWallet(); WhirlpoolWallet whirlpoolWallet = getWhirlpoolWallet();
whirlpoolWallet.startAsync().subscribeOn(Schedulers.io()) whirlpoolWallet.startAsync().subscribeOn(Schedulers.io()).observeOn(JavaFxScheduler.platform());
.observeOn(JavaFxScheduler.platform());
UtxoSupplier utxoSupplier = whirlpoolWallet.getUtxoSupplier(); UtxoSupplier utxoSupplier = whirlpoolWallet.getUtxoSupplier();
List<WhirlpoolUtxo> whirlpoolUtxos = utxos.stream().map(ref -> utxoSupplier.findUtxo(ref.getHashAsString(), (int)ref.getIndex())).filter(Objects::nonNull).collect(Collectors.toList()); List<WhirlpoolUtxo> whirlpoolUtxos = utxos.stream().map(ref -> utxoSupplier.findUtxo(ref.getHashAsString(), (int)ref.getIndex())).filter(Objects::nonNull).collect(Collectors.toList());
@ -177,10 +168,13 @@ public class Whirlpool {
try { try {
Keystore keystore = wallet.getKeystores().get(0); Keystore keystore = wallet.getKeystores().get(0);
String words = keystore.getSeed().getMnemonicString().asString(); 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(); String passphrase = keystore.getSeed().getPassphrase() == null ? "" : keystore.getSeed().getPassphrase().asString();
HD_WalletFactoryGeneric hdWalletFactory = HD_WalletFactoryGeneric.getInstance(); HD_WalletFactoryGeneric hdWalletFactory = HD_WalletFactoryGeneric.getInstance();
return hdWalletFactory.restoreWalletFromWords(words, passphrase, params); byte[] seed = hdWalletFactory.computeSeedFromWords(words);
return hdWalletFactory.getHD(purpose, seed, passphrase, params);
} catch(Exception e) { } catch(Exception e) {
throw new IllegalStateException("Could not create Whirlpool HD wallet ", e); throw new IllegalStateException("Could not create Whirlpool HD wallet ", e);
} }
@ -264,9 +258,7 @@ public class Whirlpool {
public void refreshUtxos() { public void refreshUtxos() {
if(whirlpoolWalletService.whirlpoolWallet() != null) { if(whirlpoolWalletService.whirlpoolWallet() != null) {
whirlpoolWalletService.whirlpoolWallet().refreshUtxosAsync() whirlpoolWalletService.whirlpoolWallet().refreshUtxosAsync().subscribeOn(Schedulers.io()).observeOn(JavaFxScheduler.platform());
.subscribeOn(Schedulers.io())
.observeOn(JavaFxScheduler.platform());
} }
} }
@ -352,7 +344,6 @@ public class Whirlpool {
public void shutdown() { public void shutdown() {
whirlpoolWalletService.closeWallet(); whirlpoolWalletService.closeWallet();
AppServices.getHttpClientService().stop();
} }
public StartupService createStartupService() { public StartupService createStartupService() {
@ -517,7 +508,12 @@ public class Whirlpool {
int mixes = minMixes == null ? DEFAULT_MIXTO_MIN_MIXES : minMixes; int mixes = minMixes == null ? DEFAULT_MIXTO_MIN_MIXES : minMixes;
IPostmixHandler postmixHandler = new SparrowPostmixHandler(whirlpoolWalletService, mixToWallet, KeyPurpose.RECEIVE); IPostmixHandler postmixHandler = new SparrowPostmixHandler(whirlpoolWalletService, mixToWallet, KeyPurpose.RECEIVE);
ExternalDestination externalDestination = new ExternalDestination(postmixHandler, 0, mixes, DEFAULT_MIXTO_RANDOM_FACTOR); ExternalDestination externalDestination = new ExternalDestination(postmixHandler, 0, mixes, DEFAULT_MIXTO_RANDOM_FACTOR) {
@Override
public IPostmixHandler getPostmixHandlerCustomOrDefault(WhirlpoolWallet whirlpoolWallet) {
return postmixHandler;
}
};
config.setExternalDestination(externalDestination); config.setExternalDestination(externalDestination);
} }
@ -715,10 +711,7 @@ public class Whirlpool {
whirlpool.startingProperty.set(true); whirlpool.startingProperty.set(true);
WhirlpoolWallet whirlpoolWallet = whirlpool.getWhirlpoolWallet(); WhirlpoolWallet whirlpoolWallet = whirlpool.getWhirlpoolWallet();
if(AppServices.onlineProperty().get()) { if(AppServices.onlineProperty().get()) {
whirlpoolWallet.startAsync() whirlpoolWallet.startAsync().subscribeOn(Schedulers.io()).observeOn(JavaFxScheduler.platform()).subscribe();
.subscribeOn(Schedulers.io())
.observeOn(JavaFxScheduler.platform())
.subscribe();
} }
return whirlpoolWallet; return whirlpoolWallet;

View file

@ -73,7 +73,8 @@ public class WhirlpoolServices {
public Whirlpool getWhirlpool(String walletId) { public Whirlpool getWhirlpool(String walletId) {
Whirlpool whirlpool = whirlpoolMap.get(walletId); Whirlpool whirlpool = whirlpoolMap.get(walletId);
if(whirlpool == null) { if(whirlpool == null) {
whirlpool = new Whirlpool(); Wallet wallet = AppServices.get().getWallet(walletId);
whirlpool = new Whirlpool(wallet == null ? null : wallet.getStoredBlockHeight());
whirlpoolMap.put(walletId, whirlpool); whirlpoolMap.put(walletId, whirlpool);
} }

View file

@ -3,19 +3,18 @@ package com.sparrowwallet.sparrow.whirlpool.dataSource;
import com.google.common.eventbus.Subscribe; import com.google.common.eventbus.Subscribe;
import com.samourai.wallet.api.backend.beans.WalletResponse; import com.samourai.wallet.api.backend.beans.WalletResponse;
import com.samourai.wallet.chain.ChainSupplier; import com.samourai.wallet.chain.ChainSupplier;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.protocol.Sha256Hash; import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.NewBlockEvent; import com.sparrowwallet.sparrow.event.NewBlockEvent;
public class SparrowChainSupplier implements ChainSupplier { public class SparrowChainSupplier implements ChainSupplier {
private int storedBlockHeight; private final int storedBlockHeight;
private WalletResponse.InfoBlock latestBlock; private WalletResponse.InfoBlock latestBlock;
public SparrowChainSupplier(Integer storedBlockHeight) { public SparrowChainSupplier(Integer storedBlockHeight) {
this.storedBlockHeight = AppServices.getCurrentBlockHeight() == null ? this.storedBlockHeight = AppServices.getCurrentBlockHeight() == null ? (storedBlockHeight != null ? storedBlockHeight : 0) : AppServices.getCurrentBlockHeight();
(storedBlockHeight!=null?storedBlockHeight:0)
: AppServices.getCurrentBlockHeight();
this.latestBlock = computeLatestBlock(); this.latestBlock = computeLatestBlock();
EventManager.get().register(this); EventManager.get().register(this);
} }
@ -27,7 +26,8 @@ public class SparrowChainSupplier implements ChainSupplier {
private WalletResponse.InfoBlock computeLatestBlock() { private WalletResponse.InfoBlock computeLatestBlock() {
WalletResponse.InfoBlock latestBlock = new WalletResponse.InfoBlock(); WalletResponse.InfoBlock latestBlock = new WalletResponse.InfoBlock();
latestBlock.height = AppServices.getCurrentBlockHeight() == null ? storedBlockHeight : AppServices.getCurrentBlockHeight(); latestBlock.height = AppServices.getCurrentBlockHeight() == null ? storedBlockHeight : AppServices.getCurrentBlockHeight();
latestBlock.hash = Sha256Hash.ZERO_HASH.toString(); latestBlock.hash = AppServices.getLatestBlockHeader() == null ? Sha256Hash.ZERO_HASH.toString() :
Utils.bytesToHex(Sha256Hash.twiceOf(AppServices.getLatestBlockHeader().bitcoinSerialize()).getReversedBytes());
latestBlock.time = AppServices.getLatestBlockHeader() == null ? 1 : AppServices.getLatestBlockHeader().getTime(); latestBlock.time = AppServices.getLatestBlockHeader() == null ? 1 : AppServices.getLatestBlockHeader().getTime();
return latestBlock; return latestBlock;
} }

View file

@ -40,7 +40,7 @@ public class SparrowDataSource extends AbstractDataSource {
private final ISeenBackend seenBackend; private final ISeenBackend seenBackend;
private final IPushTx pushTx; private final IPushTx pushTx;
private SparrowUtxoSupplier utxoSupplier; private final SparrowUtxoSupplier utxoSupplier;
public SparrowDataSource( public SparrowDataSource(
WhirlpoolWallet whirlpoolWallet, WhirlpoolWallet whirlpoolWallet,
@ -57,7 +57,7 @@ public class SparrowDataSource extends AbstractDataSource {
private ISeenBackend computeSeenBackend(WhirlpoolWalletConfig whirlpoolWalletConfig) { private ISeenBackend computeSeenBackend(WhirlpoolWalletConfig whirlpoolWalletConfig) {
IHttpClient httpClient = whirlpoolWalletConfig.getHttpClient(HttpUsage.BACKEND); IHttpClient httpClient = whirlpoolWalletConfig.getHttpClient(HttpUsage.BACKEND);
ISeenBackend sparrowSeenBackend = new SparrowSeenBackend(httpClient); ISeenBackend sparrowSeenBackend = new SparrowSeenBackend(getWhirlpoolWallet().getWalletIdentifier(), httpClient);
NetworkParameters params = whirlpoolWalletConfig.getSamouraiNetwork().getParams(); NetworkParameters params = whirlpoolWalletConfig.getSamouraiNetwork().getParams();
return SeenBackendWithFallback.withOxt(sparrowSeenBackend, params); return SeenBackendWithFallback.withOxt(sparrowSeenBackend, params);
} }

View file

@ -1,8 +1,10 @@
package com.sparrowwallet.sparrow.whirlpool.dataSource; package com.sparrowwallet.sparrow.whirlpool.dataSource;
import com.samourai.whirlpool.client.mix.handler.AbstractPostmixHandler; import com.samourai.wallet.client.indexHandler.IIndexHandler;
import com.samourai.whirlpool.client.mix.handler.DestinationType; 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.mix.handler.MixDestination;
import com.samourai.whirlpool.client.utils.ClientUtils;
import com.samourai.whirlpool.client.wallet.WhirlpoolWalletService; import com.samourai.whirlpool.client.wallet.WhirlpoolWalletService;
import com.samourai.whirlpool.client.wallet.beans.IndexRange; import com.samourai.whirlpool.client.wallet.beans.IndexRange;
import com.sparrowwallet.drongo.KeyPurpose; import com.sparrowwallet.drongo.KeyPurpose;
@ -12,21 +14,21 @@ import com.sparrowwallet.drongo.wallet.WalletNode;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
// TODO maybe replace with XPubPostmixHandler public class SparrowPostmixHandler implements IPostmixHandler {
public class SparrowPostmixHandler extends AbstractPostmixHandler {
private static final Logger log = LoggerFactory.getLogger(SparrowPostmixHandler.class); private static final Logger log = LoggerFactory.getLogger(SparrowPostmixHandler.class);
private final WhirlpoolWalletService whirlpoolWalletService;
private final Wallet wallet; private final Wallet wallet;
private final KeyPurpose keyPurpose; private final KeyPurpose keyPurpose;
protected MixDestination destination;
public SparrowPostmixHandler(WhirlpoolWalletService whirlpoolWalletService, Wallet wallet, KeyPurpose keyPurpose) { public SparrowPostmixHandler(WhirlpoolWalletService whirlpoolWalletService, Wallet wallet, KeyPurpose keyPurpose) {
super(whirlpoolWalletService.whirlpoolWallet().getWalletStateSupplier().getIndexHandlerExternal(), this.whirlpoolWalletService = whirlpoolWalletService;
whirlpoolWalletService.whirlpoolWallet().getConfig().getSamouraiNetwork().getParams());
this.wallet = wallet; this.wallet = wallet;
this.keyPurpose = keyPurpose; this.keyPurpose = keyPurpose;
} }
@Override
protected IndexRange getIndexRange() { protected IndexRange getIndexRange() {
return IndexRange.FULL; return IndexRange.FULL;
} }
@ -35,14 +37,50 @@ public class SparrowPostmixHandler extends AbstractPostmixHandler {
return wallet; return wallet;
} }
@Override
public final MixDestination computeDestinationNext() throws Exception {
// use "unconfirmed" index to avoid huge index gaps on multiple mix failures
int index = ClientUtils.computeNextReceiveAddressIndex(getIndexHandler(), getIndexRange());
this.destination = computeDestination(index);
if (log.isDebugEnabled()) {
log.debug(
"Mixing to "
+ destination.getType()
+ " -> receiveAddress="
+ destination.getAddress()
+ ", path="
+ destination.getPath());
}
return destination;
}
@Override @Override
public MixDestination computeDestination(int index) throws Exception { public MixDestination computeDestination(int index) throws Exception {
// address // address
WalletNode node = new WalletNode(wallet, keyPurpose, index); WalletNode node = new WalletNode(wallet, keyPurpose, index);
Address address = node.getAddress(); Address address = node.getAddress();
String path = "xpub/"+keyPurpose.getPathIndex().num()+"/"+index; String path = "xpub/" + keyPurpose.getPathIndex().num() + "/" + index;
log.info("Mixing to external xPub -> receiveAddress=" + address + ", path=" + path); log.info("Mixing to external xPub -> receiveAddress=" + address + ", path=" + path);
return new MixDestination(DestinationType.XPUB, index, address.toString(), path); return new MixDestination(DestinationType.XPUB, index, address.toString(), path);
} }
@Override
public void onMixFail() {
if(destination != null) {
// cancel unconfirmed postmix index if output was not registered yet
getIndexHandler().cancelUnconfirmed(destination.getIndex());
}
}
@Override
public void onRegisterOutput() {
// confirm postmix index on REGISTER_OUTPUT success
getIndexHandler().confirmUnconfirmed(destination.getIndex());
}
@Override
public IIndexHandler getIndexHandler() {
return whirlpoolWalletService.whirlpoolWallet().getWalletStateSupplier().getIndexHandlerExternal();
}
} }

View file

@ -3,30 +3,51 @@ package com.sparrowwallet.sparrow.whirlpool.dataSource;
import com.samourai.wallet.api.backend.seenBackend.ISeenBackend; import com.samourai.wallet.api.backend.seenBackend.ISeenBackend;
import com.samourai.wallet.api.backend.seenBackend.SeenResponse; import com.samourai.wallet.api.backend.seenBackend.SeenResponse;
import com.samourai.wallet.httpClient.IHttpClient; import com.samourai.wallet.httpClient.IHttpClient;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletNode;
import com.sparrowwallet.sparrow.AppServices;
import java.util.Collection; import java.util.Collection;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
public class SparrowSeenBackend implements ISeenBackend { public class SparrowSeenBackend implements ISeenBackend {
private IHttpClient httpClient; private final String walletId;
private final IHttpClient httpClient;
public SparrowSeenBackend(IHttpClient httpClient) { public SparrowSeenBackend(String walletId, IHttpClient httpClient) {
this.walletId = walletId;
this.httpClient = httpClient; this.httpClient = httpClient;
} }
@Override @Override
public SeenResponse seen(Collection<String> addresses) throws Exception { public SeenResponse seen(Collection<String> addresses) throws Exception {
Map<String,Boolean> map = new LinkedHashMap<>(); Wallet wallet = AppServices.get().getWallet(walletId);
for (String address : addresses) { Map<Address, WalletNode> addressMap = wallet.getWalletAddresses();
map.put(address, seen(address)); for(Wallet childWallet : wallet.getChildWallets()) {
if(!childWallet.isNested()) {
addressMap.putAll(childWallet.getWalletAddresses());
}
} }
Map<String,Boolean> map = new LinkedHashMap<>();
for(String address : addresses) {
WalletNode walletNode = addressMap.get(Address.fromString(address));
if(walletNode != null) {
int highestUsedIndex = walletNode.getWallet().getNode(walletNode.getKeyPurpose()).getHighestUsedIndex();
map.put(address, walletNode.getIndex() <= highestUsedIndex);
}
}
return new SeenResponse(map); return new SeenResponse(map);
} }
@Override @Override
public boolean seen(String address) throws Exception { public boolean seen(String address) throws Exception {
return false; // TODO implement: return true if address already received funds SeenResponse seenResponse = seen(List.of(address));
return seenResponse.isSeen(address);
} }
@Override @Override

View file

@ -35,7 +35,7 @@ public class SparrowWalletStateSupplier implements WalletStateSupplier {
SamouraiAccount samouraiAccount = bipWallet.getAccount(); SamouraiAccount samouraiAccount = bipWallet.getAccount();
String key = mapKey(bipWallet, chain); String key = mapKey(bipWallet, chain);
IIndexHandler indexHandler = indexHandlerWallets.get(key); IIndexHandler indexHandler = indexHandlerWallets.get(key);
if (indexHandler == null) { if(indexHandler == null) {
Wallet wallet = findWallet(samouraiAccount); Wallet wallet = findWallet(samouraiAccount);
KeyPurpose keyPurpose = (chain == Chain.RECEIVE ? KeyPurpose.RECEIVE : KeyPurpose.CHANGE); KeyPurpose keyPurpose = (chain == Chain.RECEIVE ? KeyPurpose.RECEIVE : KeyPurpose.CHANGE);
WalletNode walletNode = wallet.getNode(keyPurpose); WalletNode walletNode = wallet.getNode(keyPurpose);
@ -123,7 +123,7 @@ public class SparrowWalletStateSupplier implements WalletStateSupplier {
private String mapKey(BipWallet bipWallet, Chain chain) { private String mapKey(BipWallet bipWallet, Chain chain) {
SamouraiAccount samouraiAccount = bipWallet.getAccount(); SamouraiAccount samouraiAccount = bipWallet.getAccount();
BipDerivation derivation = bipWallet.getDerivation(); BipDerivation derivation = bipWallet.getDerivation();
return samouraiAccount.name()+"_"+derivation.getPurpose()+"_"+chain.getIndex(); return samouraiAccount.name() + "_" + derivation.getPurpose() + "_" + chain.getIndex();
} }
private Wallet findWallet(SamouraiAccount samouraiAccount) { private Wallet findWallet(SamouraiAccount samouraiAccount) {

View file

@ -64,7 +64,6 @@ open module com.sparrowwallet.sparrow {
requires com.sparrowwallet.bokmakierie; requires com.sparrowwallet.bokmakierie;
requires java.smartcardio; requires java.smartcardio;
requires com.jcraft.jzlib; requires com.jcraft.jzlib;
// samourai dependencies
requires com.samourai.whirlpool.client; requires com.samourai.whirlpool.client;
requires com.samourai.whirlpool.protocol; requires com.samourai.whirlpool.protocol;
requires com.samourai.extlibj; requires com.samourai.extlibj;