From 038069f6e63b4508d83c4e79bcd1cd8102c98054 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Tue, 15 Dec 2020 10:29:13 +0200 Subject: [PATCH] add scheduled service to check mempool after broadcast --- .../sparrow/net/ElectrumServer.java | 52 +++++++++++++++++++ .../transaction/HeadersController.java | 33 +++++++++++- .../sparrow/transaction/TransactionData.java | 37 ++++++++++--- .../sparrow/transaction/TransactionForm.java | 6 +++ 4 files changed, 120 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java index 081b4c91..d333db3b 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java @@ -14,6 +14,8 @@ import com.sparrowwallet.sparrow.event.FeeRatesUpdatedEvent; import com.sparrowwallet.sparrow.event.TorStatusEvent; import com.sparrowwallet.sparrow.io.Config; 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.StringProperty; import javafx.concurrent.ScheduledService; @@ -663,6 +665,24 @@ public class ElectrumServer { } } + public Set getMempoolScriptHashes(Wallet wallet, Sha256Hash txId, Set transactionNodes) throws ServerException { + Map pathScriptHashes = new LinkedHashMap<>(transactionNodes.size()); + for(WalletNode node : transactionNodes) { + pathScriptHashes.put(node.getDerivationPath(), getScriptHash(wallet, node)); + } + + Set mempoolScriptHashes = new LinkedHashSet<>(); + Map 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 getAllScriptHashes(Wallet wallet) { Map scriptHashes = new HashMap<>(); List purposes = List.of(KeyPurpose.RECEIVE, KeyPurpose.CHANGE); @@ -902,6 +922,38 @@ public class ElectrumServer { } } + public static class TransactionMempoolService extends ScheduledService> { + private final Wallet wallet; + private final Sha256Hash txId; + private final Set nodes; + private final IntegerProperty iterationCount = new SimpleIntegerProperty(0); + + public TransactionMempoolService(Wallet wallet, Sha256Hash txId, Set nodes) { + this.wallet = wallet; + this.txId = txId; + this.nodes = nodes; + } + + public int getIterationCount() { + return iterationCount.get(); + } + + public IntegerProperty iterationCountProperty() { + return iterationCount; + } + + @Override + protected Task> createTask() { + return new Task<>() { + protected Set call() throws ServerException { + iterationCount.set(iterationCount.get() + 1); + ElectrumServer electrumServer = new ElectrumServer(); + return electrumServer.getMempoolScriptHashes(wallet, txId, nodes); + } + }; + } + } + public static class TransactionReferenceService extends Service> { private final Set references; private String scriptHash; diff --git a/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java b/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java index 97e4cc91..3ce990df 100644 --- a/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java +++ b/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java @@ -36,6 +36,7 @@ import javafx.scene.layout.VBox; import javafx.stage.FileChooser; import javafx.stage.Modality; import javafx.stage.Stage; +import javafx.util.Duration; import org.controlsfx.glyphfont.Glyph; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -202,6 +203,8 @@ public class HeadersController extends TransactionFormController implements Init @FXML private Button payjoinButton; + private ElectrumServer.TransactionMempoolService transactionMempoolService; + @Override public void initialize(URL location, ResourceBundle resources) { EventManager.get().register(this); @@ -791,7 +794,31 @@ public class HeadersController extends TransactionFormController implements Init ElectrumServer.BroadcastTransactionService broadcastTransactionService = new ElectrumServer.BroadcastTransactionService(headersForm.getTransaction()); 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 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 -> { broadcastProgressBar.setProgress(0); @@ -1022,6 +1049,10 @@ public class HeadersController extends TransactionFormController implements Init @Subscribe public void walletNodeHistoryChanged(WalletNodeHistoryChangedEvent event) { if(headersForm.getSigningWallet() != null && event.getWalletNode(headersForm.getSigningWallet()) != null && headersForm.isTransactionFinalized()) { + if(transactionMempoolService != null) { + transactionMempoolService.cancel(); + } + Sha256Hash txid = headersForm.getTransaction().getTxId(); ElectrumServer.TransactionReferenceService transactionReferenceService = new ElectrumServer.TransactionReferenceService(Set.of(txid), event.getScriptHash()); transactionReferenceService.setOnSucceeded(successEvent -> { diff --git a/src/main/java/com/sparrowwallet/sparrow/transaction/TransactionData.java b/src/main/java/com/sparrowwallet/sparrow/transaction/TransactionData.java index 559b6f97..11a2b393 100644 --- a/src/main/java/com/sparrowwallet/sparrow/transaction/TransactionData.java +++ b/src/main/java/com/sparrowwallet/sparrow/transaction/TransactionData.java @@ -1,21 +1,18 @@ package com.sparrowwallet.sparrow.transaction; -import com.sparrowwallet.drongo.protocol.Sha256Hash; -import com.sparrowwallet.drongo.protocol.Transaction; -import com.sparrowwallet.drongo.protocol.TransactionSignature; +import com.sparrowwallet.drongo.KeyPurpose; +import com.sparrowwallet.drongo.protocol.*; import com.sparrowwallet.drongo.psbt.PSBT; import com.sparrowwallet.drongo.wallet.BlockTransaction; import com.sparrowwallet.drongo.wallet.Keystore; import com.sparrowwallet.drongo.wallet.Wallet; +import com.sparrowwallet.drongo.wallet.WalletNode; import com.sparrowwallet.sparrow.io.Storage; import javafx.beans.property.SimpleObjectProperty; import javafx.collections.FXCollections; import javafx.collections.ObservableMap; -import java.util.Collection; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; +import java.util.*; public class TransactionData { private Transaction transaction; @@ -156,4 +153,30 @@ public class TransactionData { public Collection getSignedKeystores() { return signatureKeystoreMap.values(); } + + public Set getSigningWalletNodes() { + if(getSigningWallet() == null) { + throw new IllegalStateException("Signing wallet cannot be null"); + } + + Set signingWalletNodes = new LinkedHashSet<>(); + for(TransactionInput txInput : transaction.getInputs()) { + Optional 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; + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/transaction/TransactionForm.java b/src/main/java/com/sparrowwallet/sparrow/transaction/TransactionForm.java index 76426b0e..385fbb13 100644 --- a/src/main/java/com/sparrowwallet/sparrow/transaction/TransactionForm.java +++ b/src/main/java/com/sparrowwallet/sparrow/transaction/TransactionForm.java @@ -7,6 +7,7 @@ import com.sparrowwallet.drongo.psbt.PSBT; import com.sparrowwallet.drongo.wallet.BlockTransaction; import com.sparrowwallet.drongo.wallet.Keystore; import com.sparrowwallet.drongo.wallet.Wallet; +import com.sparrowwallet.drongo.wallet.WalletNode; import com.sparrowwallet.sparrow.io.Storage; import javafx.beans.property.SimpleObjectProperty; import javafx.collections.ObservableMap; @@ -16,6 +17,7 @@ import java.io.IOException; import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.Set; public abstract class TransactionForm { protected final TransactionData txdata; @@ -92,6 +94,10 @@ public abstract class TransactionForm { return txdata.getSignedKeystores(); } + public Set getSigningWalletNodes() { + return txdata.getSigningWalletNodes(); + } + public boolean isEditable() { if(getBlockTransaction() != null) { return false;