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'
}
implementation('com.sparrowwallet.bokmakierie:bokmakierie:1.0')
implementation('io.samourai.code.whirlpool:whirlpool-client:1.0.0-beta10')
implementation('io.samourai.code.wallet:java-http-client:2.0.0-beta3')
implementation('io.samourai.code.whirlpool:whirlpool-client:1.0.0-beta13')
implementation('io.samourai.code.wallet:java-http-client:2.0.0-beta4')
implementation('io.reactivex.rxjava2:rxjava:2.2.15')
implementation('io.reactivex.rxjava2:rxjavafx:2.2.2')
implementation('org.apache.commons:commons-lang3:3.7')
@ -490,7 +490,6 @@ extraJavaModuleInfo {
exports('co.nstant.in.cbor.model')
exports('co.nstant.in.cbor.builder')
}
// begin samourai dependencies
module('commons-codec-1.10.jar', 'commons.codec', '1.10') {
exports('org.apache.commons.codec')
}
@ -507,7 +506,6 @@ extraJavaModuleInfo {
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')
}

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) {
if(mixFailReason != MixFailReason.CANCEL) {
if(mixFailReason != MixFailReason.STOP_UTXO && !mixFailReason.isSilent()) {
long elapsed = mixErrorTimestamp == null ? 0L : System.currentTimeMillis() - mixErrorTimestamp;
if(elapsed >= ERROR_DISPLAY_MILLIS) {
//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);
setGraphic(progressIndicator);
Tooltip tt = new Tooltip();
String status = mixProgress.getMixStep().getMessage().substring(0, 1).toUpperCase(Locale.ROOT) + mixProgress.getMixStep().getMessage().substring(1);
String status = mixProgress.getMixStep().getMessage().replaceAll("_", " ");
status = status.substring(0, 1).toUpperCase(Locale.ROOT) + status.substring(1).toLowerCase(Locale.ROOT);
tt.setText(status);
setTooltip(tt);
// 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 {
setGraphic(null);
setTooltip(null);

View file

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

View file

@ -1,7 +1,7 @@
package com.sparrowwallet.sparrow.net;
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.Utils;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
@ -158,7 +158,7 @@ public enum BroadcastSource {
} catch(Exception e) {
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());
} catch(Exception e) {
log.error("Could not post transaction via " + getName(), e);

View file

@ -1,6 +1,6 @@
package com.sparrowwallet.sparrow.net;
import com.samourai.wallet.api.backend.beans.HttpException;
import com.samourai.wallet.httpClient.HttpResponseException;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.event.ExchangeRatesUpdatedEvent;
import javafx.concurrent.ScheduledService;
@ -107,7 +107,7 @@ public enum ExchangeSource {
if(log.isDebugEnabled()) {
log.warn("Error retrieving historical currency rates", e);
} else {
if(e instanceof 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);
} else {
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.IHttpClient;
import com.samourai.wallet.util.AsyncUtil;
import com.samourai.wallet.util.ThreadUtil;
import com.samourai.whirlpool.client.utils.ClientUtils;
import io.reactivex.Observable;
import javafx.concurrent.Service;
@ -17,25 +18,20 @@ public class HttpClientService extends JettyHttpClientService {
private static final int REQUEST_TIMEOUT = 120000;
public HttpClientService(HostAndPort torProxy) {
super(REQUEST_TIMEOUT,
ClientUtils.USER_AGENT,
new HttpProxySupplier(torProxy));
super(REQUEST_TIMEOUT, ClientUtils.USER_AGENT, new HttpProxySupplier(torProxy));
}
public <T> T requestJson(String url, Class<T> responseType, Map<String, String> headers) throws Exception {
return getHttpClient(HttpUsage.BACKEND)
.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) {
return getHttpClient(HttpUsage.BACKEND)
.postJson(url, responseType, headers, body).toObservable();
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 = getHttpClient(HttpUsage.BACKEND);
return AsyncUtil.getInstance().blockingGet(
httpClient.postString(url, headers, contentType, content)).get();
return AsyncUtil.getInstance().blockingGet(httpClient.postString(url, headers, contentType, content)).get();
}
public HostAndPort getTorProxy() {
@ -64,6 +60,7 @@ public class HttpClientService extends JettyHttpClientService {
protected Task<Boolean> createTask() {
return new Task<>() {
protected Boolean call() throws Exception {
ThreadUtil.getInstance().getExecutorService().shutdown();
httpClientService.stop();
return true;
}

View file

@ -21,7 +21,7 @@ public class HttpProxySupplier implements IHttpProxySupplier {
if (hostAndPort == null) {
return null;
}
// TODO verify
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.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.Transaction;
import com.sparrowwallet.drongo.protocol.TransactionInput;
@ -87,7 +87,7 @@ public class Payjoin {
checkProposal(psbt, proposalPsbt, changeOutputIndex, maxAdditionalFeeContribution, allowOutputSubstitution);
return proposalPsbt;
} catch(HttpException e) {
} catch(HttpResponseException e) {
Gson gson = new Gson();
PayjoinReceiverError payjoinReceiverError = gson.fromJson(e.getResponseBody(), PayjoinReceiverError.class);
log.warn("Payjoin receiver returned an error of " + payjoinReceiverError.getErrorCode() + " (" + payjoinReceiverError.getMessage() + ")");

View file

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

View file

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

View file

@ -79,26 +79,4 @@ public class Soroban {
public SorobanWalletService getSorobanWalletService() {
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) {
for(WalletTabData walletTabData : event.getClosedWalletTabData()) {
String walletId = walletTabData.getStorage().getWalletId(walletTabData.getWallet());
Soroban soroban = sorobanMap.remove(walletId);
if(soroban != null) {
Soroban.ShutdownService shutdownService = new Soroban.ShutdownService(soroban);
shutdownService.setOnFailed(failedEvent -> {
log.error("Failed to shutdown soroban", failedEvent.getSource().getException());
});
shutdownService.start();
}
sorobanMap.remove(walletId);
}
}
}

View file

@ -26,13 +26,12 @@ import java.util.List;
public class SparrowCahootsWallet extends AbstractCahootsWallet {
private final Wallet wallet;
private HD_Wallet bip84w;
private final HD_Wallet bip84w;
private final int account;
private List<CahootsUtxo> utxos;
private final List<CahootsUtxo> utxos;
public SparrowCahootsWallet(ChainSupplier chainSupplier, Wallet wallet, HD_Wallet bip84w, int bip47Account) {
super(chainSupplier, bip84w.getFingerprint(),
new BIP47Wallet(bip84w).getAccount(bip47Account));
super(chainSupplier, bip84w.getFingerprint(), new BIP47Wallet(bip84w).getAccount(bip47Account));
this.wallet = wallet;
this.bip84w = bip84w;
this.account = wallet.getAccountIndex();
@ -107,7 +106,7 @@ public class SparrowCahootsWallet extends AbstractCahootsWallet {
throw new IllegalStateException("Cannot add BIP47 UTXO", e);
}
} 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());
}

View file

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

View file

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

View file

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

View file

@ -33,10 +33,7 @@ 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.protocol.*;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.AppServices;
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_RANDOM_FACTOR = 4;
private final WhirlpoolWalletService whirlpoolWalletService;
private final WhirlpoolWalletConfig config;
private WhirlpoolInfo whirlpoolInfo;
@ -89,11 +85,10 @@ public class Whirlpool {
private final BooleanProperty stoppingProperty = new SimpleBooleanProperty(false);
private final BooleanProperty mixingProperty = new SimpleBooleanProperty(false);
public Whirlpool() {
public Whirlpool(Integer storedBlockHeight) {
this.whirlpoolWalletService = new WhirlpoolWalletService();
Integer storedBlockHeight = null; // TODO
this.config = computeWhirlpoolWalletConfig(storedBlockHeight);
this.whirlpoolInfo = null; // instanciated by getWhirlpoolInfo()
this.whirlpoolInfo = null; // instantiated by getWhirlpoolInfo()
WhirlpoolEventService.getInstance().register(this);
}
@ -112,17 +107,14 @@ public class Whirlpool {
}
private DataSourceConfig computeDataSourceConfig(Integer storedBlockHeight) {
return new DataSourceConfig(
SparrowMinerFeeSupplier.getInstance(),
new SparrowChainSupplier(storedBlockHeight),
BIP_FORMAT.PROVIDER,
BIP_WALLETS.WHIRLPOOL);
return new DataSourceConfig(SparrowMinerFeeSupplier.getInstance(), new SparrowChainSupplier(storedBlockHeight), BIP_FORMAT.PROVIDER, BIP_WALLETS.WHIRLPOOL);
}
private WhirlpoolInfo getWhirlpoolInfo() {
if (whirlpoolInfo == null) {
if(whirlpoolInfo == null) {
whirlpoolInfo = new WhirlpoolInfo(SparrowMinerFeeSupplier.getInstance(), config);
}
return whirlpoolInfo;
}
@ -144,8 +136,7 @@ public class Whirlpool {
public Tx0 broadcastTx0(Pool pool, Collection<BlockTransactionHashIndex> utxos) throws Exception {
WhirlpoolWallet whirlpoolWallet = getWhirlpoolWallet();
whirlpoolWallet.startAsync().subscribeOn(Schedulers.io())
.observeOn(JavaFxScheduler.platform());
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());
@ -177,10 +168,13 @@ public class Whirlpool {
try {
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();
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) {
throw new IllegalStateException("Could not create Whirlpool HD wallet ", e);
}
@ -264,9 +258,7 @@ public class Whirlpool {
public void refreshUtxos() {
if(whirlpoolWalletService.whirlpoolWallet() != null) {
whirlpoolWalletService.whirlpoolWallet().refreshUtxosAsync()
.subscribeOn(Schedulers.io())
.observeOn(JavaFxScheduler.platform());
whirlpoolWalletService.whirlpoolWallet().refreshUtxosAsync().subscribeOn(Schedulers.io()).observeOn(JavaFxScheduler.platform());
}
}
@ -352,7 +344,6 @@ public class Whirlpool {
public void shutdown() {
whirlpoolWalletService.closeWallet();
AppServices.getHttpClientService().stop();
}
public StartupService createStartupService() {
@ -517,7 +508,12 @@ public class Whirlpool {
int mixes = minMixes == null ? DEFAULT_MIXTO_MIN_MIXES : minMixes;
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);
}
@ -715,10 +711,7 @@ public class Whirlpool {
whirlpool.startingProperty.set(true);
WhirlpoolWallet whirlpoolWallet = whirlpool.getWhirlpoolWallet();
if(AppServices.onlineProperty().get()) {
whirlpoolWallet.startAsync()
.subscribeOn(Schedulers.io())
.observeOn(JavaFxScheduler.platform())
.subscribe();
whirlpoolWallet.startAsync().subscribeOn(Schedulers.io()).observeOn(JavaFxScheduler.platform()).subscribe();
}
return whirlpoolWallet;

View file

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

View file

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

View file

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

View file

@ -1,8 +1,10 @@
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.IPostmixHandler;
import com.samourai.whirlpool.client.mix.handler.MixDestination;
import com.samourai.whirlpool.client.utils.ClientUtils;
import com.samourai.whirlpool.client.wallet.WhirlpoolWalletService;
import com.samourai.whirlpool.client.wallet.beans.IndexRange;
import com.sparrowwallet.drongo.KeyPurpose;
@ -12,21 +14,21 @@ import com.sparrowwallet.drongo.wallet.WalletNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
// TODO maybe replace with XPubPostmixHandler
public class SparrowPostmixHandler extends AbstractPostmixHandler {
public class SparrowPostmixHandler implements IPostmixHandler {
private static final Logger log = LoggerFactory.getLogger(SparrowPostmixHandler.class);
private final WhirlpoolWalletService whirlpoolWalletService;
private final Wallet wallet;
private final KeyPurpose keyPurpose;
protected MixDestination destination;
public SparrowPostmixHandler(WhirlpoolWalletService whirlpoolWalletService, Wallet wallet, KeyPurpose keyPurpose) {
super(whirlpoolWalletService.whirlpoolWallet().getWalletStateSupplier().getIndexHandlerExternal(),
whirlpoolWalletService.whirlpoolWallet().getConfig().getSamouraiNetwork().getParams());
this.whirlpoolWalletService = whirlpoolWalletService;
this.wallet = wallet;
this.keyPurpose = keyPurpose;
}
@Override
protected IndexRange getIndexRange() {
return IndexRange.FULL;
}
@ -35,14 +37,50 @@ public class SparrowPostmixHandler extends AbstractPostmixHandler {
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
public MixDestination computeDestination(int index) throws Exception {
// address
WalletNode node = new WalletNode(wallet, keyPurpose, index);
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);
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.SeenResponse;
import com.samourai.wallet.httpClient.IHttpClient;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletNode;
import com.sparrowwallet.sparrow.AppServices;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public class SparrowSeenBackend implements ISeenBackend {
private 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;
}
@Override
public SeenResponse seen(Collection<String> addresses) throws Exception {
Map<String,Boolean> map = new LinkedHashMap<>();
for (String address : addresses) {
map.put(address, seen(address));
Wallet wallet = AppServices.get().getWallet(walletId);
Map<Address, WalletNode> addressMap = wallet.getWalletAddresses();
for(Wallet childWallet : wallet.getChildWallets()) {
if(!childWallet.isNested()) {
addressMap.putAll(childWallet.getWalletAddresses());
}
}
Map<String,Boolean> map = new LinkedHashMap<>();
for(String address : addresses) {
WalletNode walletNode = addressMap.get(Address.fromString(address));
if(walletNode != null) {
int highestUsedIndex = walletNode.getWallet().getNode(walletNode.getKeyPurpose()).getHighestUsedIndex();
map.put(address, walletNode.getIndex() <= highestUsedIndex);
}
}
return new SeenResponse(map);
}
@Override
public boolean seen(String address) throws Exception {
return false; // TODO implement: return true if address already received funds
SeenResponse seenResponse = seen(List.of(address));
return seenResponse.isSeen(address);
}
@Override

View file

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

View file

@ -64,7 +64,6 @@ 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;