add scheduled service to check mempool after broadcast

This commit is contained in:
Craig Raw 2020-12-15 10:29:13 +02:00
parent ee6d8028f8
commit 038069f6e6
4 changed files with 120 additions and 8 deletions

View file

@ -14,6 +14,8 @@ import com.sparrowwallet.sparrow.event.FeeRatesUpdatedEvent;
import com.sparrowwallet.sparrow.event.TorStatusEvent; import com.sparrowwallet.sparrow.event.TorStatusEvent;
import com.sparrowwallet.sparrow.io.Config; import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.wallet.SendController; import com.sparrowwallet.sparrow.wallet.SendController;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty; import javafx.beans.property.StringProperty;
import javafx.concurrent.ScheduledService; import javafx.concurrent.ScheduledService;
@ -663,6 +665,24 @@ public class ElectrumServer {
} }
} }
public Set<String> getMempoolScriptHashes(Wallet wallet, Sha256Hash txId, Set<WalletNode> transactionNodes) throws ServerException {
Map<String, String> pathScriptHashes = new LinkedHashMap<>(transactionNodes.size());
for(WalletNode node : transactionNodes) {
pathScriptHashes.put(node.getDerivationPath(), getScriptHash(wallet, node));
}
Set<String> mempoolScriptHashes = new LinkedHashSet<>();
Map<String, ScriptHashTx[]> result = electrumServerRpc.getScriptHashHistory(getTransport(), wallet, pathScriptHashes, true);
for(String path : result.keySet()) {
ScriptHashTx[] txes = result.get(path);
if(Arrays.stream(txes).map(ScriptHashTx::getBlockchainTransactionHash).anyMatch(ref -> txId.equals(ref.getHash()) && ref.getHeight() <= 0)) {
mempoolScriptHashes.add(pathScriptHashes.get(path));
}
}
return mempoolScriptHashes;
}
public static Map<String, WalletNode> getAllScriptHashes(Wallet wallet) { public static Map<String, WalletNode> getAllScriptHashes(Wallet wallet) {
Map<String, WalletNode> scriptHashes = new HashMap<>(); Map<String, WalletNode> scriptHashes = new HashMap<>();
List<KeyPurpose> purposes = List.of(KeyPurpose.RECEIVE, KeyPurpose.CHANGE); List<KeyPurpose> purposes = List.of(KeyPurpose.RECEIVE, KeyPurpose.CHANGE);
@ -902,6 +922,38 @@ public class ElectrumServer {
} }
} }
public static class TransactionMempoolService extends ScheduledService<Set<String>> {
private final Wallet wallet;
private final Sha256Hash txId;
private final Set<WalletNode> nodes;
private final IntegerProperty iterationCount = new SimpleIntegerProperty(0);
public TransactionMempoolService(Wallet wallet, Sha256Hash txId, Set<WalletNode> nodes) {
this.wallet = wallet;
this.txId = txId;
this.nodes = nodes;
}
public int getIterationCount() {
return iterationCount.get();
}
public IntegerProperty iterationCountProperty() {
return iterationCount;
}
@Override
protected Task<Set<String>> createTask() {
return new Task<>() {
protected Set<String> call() throws ServerException {
iterationCount.set(iterationCount.get() + 1);
ElectrumServer electrumServer = new ElectrumServer();
return electrumServer.getMempoolScriptHashes(wallet, txId, nodes);
}
};
}
}
public static class TransactionReferenceService extends Service<Map<Sha256Hash, BlockTransaction>> { public static class TransactionReferenceService extends Service<Map<Sha256Hash, BlockTransaction>> {
private final Set<Sha256Hash> references; private final Set<Sha256Hash> references;
private String scriptHash; private String scriptHash;

View file

@ -36,6 +36,7 @@ import javafx.scene.layout.VBox;
import javafx.stage.FileChooser; import javafx.stage.FileChooser;
import javafx.stage.Modality; import javafx.stage.Modality;
import javafx.stage.Stage; import javafx.stage.Stage;
import javafx.util.Duration;
import org.controlsfx.glyphfont.Glyph; import org.controlsfx.glyphfont.Glyph;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -202,6 +203,8 @@ public class HeadersController extends TransactionFormController implements Init
@FXML @FXML
private Button payjoinButton; private Button payjoinButton;
private ElectrumServer.TransactionMempoolService transactionMempoolService;
@Override @Override
public void initialize(URL location, ResourceBundle resources) { public void initialize(URL location, ResourceBundle resources) {
EventManager.get().register(this); EventManager.get().register(this);
@ -791,7 +794,31 @@ public class HeadersController extends TransactionFormController implements Init
ElectrumServer.BroadcastTransactionService broadcastTransactionService = new ElectrumServer.BroadcastTransactionService(headersForm.getTransaction()); ElectrumServer.BroadcastTransactionService broadcastTransactionService = new ElectrumServer.BroadcastTransactionService(headersForm.getTransaction());
broadcastTransactionService.setOnSucceeded(workerStateEvent -> { broadcastTransactionService.setOnSucceeded(workerStateEvent -> {
//Do nothing and wait for WalletNodeHistoryChangedEvent to indicate tx is in mempool //Although we wait for WalletNodeHistoryChangedEvent to indicate tx is in mempool, start a scheduled service to check the script hashes should notifications fail
if(headersForm.getSigningWallet() != null) {
if(transactionMempoolService != null) {
transactionMempoolService.cancel();
}
transactionMempoolService = new ElectrumServer.TransactionMempoolService(headersForm.getSigningWallet(), headersForm.getTransaction().getTxId(), headersForm.getSigningWalletNodes());
transactionMempoolService.setDelay(Duration.seconds(5));
transactionMempoolService.setPeriod(Duration.seconds(10));
transactionMempoolService.setOnSucceeded(mempoolWorkerStateEvent -> {
Set<String> scriptHashes = transactionMempoolService.getValue();
if(!scriptHashes.isEmpty()) {
Platform.runLater(() -> EventManager.get().post(new WalletNodeHistoryChangedEvent(scriptHashes.iterator().next())));
}
if(transactionMempoolService.getIterationCount() > 3) {
transactionMempoolService.cancel();
broadcastProgressBar.setProgress(0);
log.error("Timeout searching for broadcasted transaction");
AppServices.showErrorDialog("Timeout searching for broadcasted transaction", "The transaction was broadcast but the server did not register it in the mempool. It is safe to try broadcasting again.");
broadcastButton.setDisable(false);
}
});
transactionMempoolService.start();
}
}); });
broadcastTransactionService.setOnFailed(workerStateEvent -> { broadcastTransactionService.setOnFailed(workerStateEvent -> {
broadcastProgressBar.setProgress(0); broadcastProgressBar.setProgress(0);
@ -1022,6 +1049,10 @@ public class HeadersController extends TransactionFormController implements Init
@Subscribe @Subscribe
public void walletNodeHistoryChanged(WalletNodeHistoryChangedEvent event) { public void walletNodeHistoryChanged(WalletNodeHistoryChangedEvent event) {
if(headersForm.getSigningWallet() != null && event.getWalletNode(headersForm.getSigningWallet()) != null && headersForm.isTransactionFinalized()) { if(headersForm.getSigningWallet() != null && event.getWalletNode(headersForm.getSigningWallet()) != null && headersForm.isTransactionFinalized()) {
if(transactionMempoolService != null) {
transactionMempoolService.cancel();
}
Sha256Hash txid = headersForm.getTransaction().getTxId(); Sha256Hash txid = headersForm.getTransaction().getTxId();
ElectrumServer.TransactionReferenceService transactionReferenceService = new ElectrumServer.TransactionReferenceService(Set.of(txid), event.getScriptHash()); ElectrumServer.TransactionReferenceService transactionReferenceService = new ElectrumServer.TransactionReferenceService(Set.of(txid), event.getScriptHash());
transactionReferenceService.setOnSucceeded(successEvent -> { transactionReferenceService.setOnSucceeded(successEvent -> {

View file

@ -1,21 +1,18 @@
package com.sparrowwallet.sparrow.transaction; package com.sparrowwallet.sparrow.transaction;
import com.sparrowwallet.drongo.protocol.Sha256Hash; import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.protocol.Transaction; import com.sparrowwallet.drongo.protocol.*;
import com.sparrowwallet.drongo.protocol.TransactionSignature;
import com.sparrowwallet.drongo.psbt.PSBT; import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.wallet.BlockTransaction; import com.sparrowwallet.drongo.wallet.BlockTransaction;
import com.sparrowwallet.drongo.wallet.Keystore; import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletNode;
import com.sparrowwallet.sparrow.io.Storage; import com.sparrowwallet.sparrow.io.Storage;
import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.collections.ObservableMap; import javafx.collections.ObservableMap;
import java.util.Collection; import java.util.*;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public class TransactionData { public class TransactionData {
private Transaction transaction; private Transaction transaction;
@ -156,4 +153,30 @@ public class TransactionData {
public Collection<Keystore> getSignedKeystores() { public Collection<Keystore> getSignedKeystores() {
return signatureKeystoreMap.values(); return signatureKeystoreMap.values();
} }
public Set<WalletNode> getSigningWalletNodes() {
if(getSigningWallet() == null) {
throw new IllegalStateException("Signing wallet cannot be null");
}
Set<WalletNode> signingWalletNodes = new LinkedHashSet<>();
for(TransactionInput txInput : transaction.getInputs()) {
Optional<WalletNode> optNode = getSigningWallet().getWalletTxos().entrySet().stream().filter(entry -> entry.getKey().getHash().equals(txInput.getOutpoint().getHash()) && entry.getKey().getIndex() == txInput.getOutpoint().getIndex()).map(Map.Entry::getValue).findFirst();
optNode.ifPresent(signingWalletNodes::add);
}
for(TransactionOutput txOutput : transaction.getOutputs()) {
WalletNode changeNode = getSigningWallet().getWalletOutputScripts(KeyPurpose.CHANGE).get(txOutput.getScript());
if(changeNode != null) {
signingWalletNodes.add(changeNode);
} else {
WalletNode receiveNode = getSigningWallet().getWalletOutputScripts(KeyPurpose.RECEIVE).get(txOutput.getScript());
if(receiveNode != null) {
signingWalletNodes.add(receiveNode);
}
}
}
return signingWalletNodes;
}
} }

View file

@ -7,6 +7,7 @@ import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.wallet.BlockTransaction; import com.sparrowwallet.drongo.wallet.BlockTransaction;
import com.sparrowwallet.drongo.wallet.Keystore; import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletNode;
import com.sparrowwallet.sparrow.io.Storage; import com.sparrowwallet.sparrow.io.Storage;
import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.ObservableMap; import javafx.collections.ObservableMap;
@ -16,6 +17,7 @@ import java.io.IOException;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
public abstract class TransactionForm { public abstract class TransactionForm {
protected final TransactionData txdata; protected final TransactionData txdata;
@ -92,6 +94,10 @@ public abstract class TransactionForm {
return txdata.getSignedKeystores(); return txdata.getSignedKeystores();
} }
public Set<WalletNode> getSigningWalletNodes() {
return txdata.getSigningWalletNodes();
}
public boolean isEditable() { public boolean isEditable() {
if(getBlockTransaction() != null) { if(getBlockTransaction() != null) {
return false; return false;