mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-11-04 13:26:44 +00:00
add broadcasting step to soroban initiator dialog and indicate when transaction has been successfully broadcasted
This commit is contained in:
parent
a76d9dba21
commit
8fb7f544de
4 changed files with 199 additions and 33 deletions
|
@ -1,5 +1,6 @@
|
|||
package com.sparrowwallet.sparrow.soroban;
|
||||
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.samourai.soroban.cahoots.CahootsContext;
|
||||
import com.samourai.soroban.client.cahoots.OnlineCahootsMessage;
|
||||
import com.samourai.soroban.client.cahoots.SorobanCahootsService;
|
||||
|
@ -12,6 +13,7 @@ import com.sparrowwallet.drongo.crypto.EncryptionType;
|
|||
import com.sparrowwallet.drongo.crypto.InvalidPasswordException;
|
||||
import com.sparrowwallet.drongo.crypto.Key;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.protocol.TransactionInput;
|
||||
import com.sparrowwallet.drongo.psbt.PSBTParseException;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
|
@ -22,8 +24,10 @@ import com.sparrowwallet.sparrow.control.TransactionDiagram;
|
|||
import com.sparrowwallet.sparrow.control.WalletPasswordDialog;
|
||||
import com.sparrowwallet.sparrow.event.StorageEvent;
|
||||
import com.sparrowwallet.sparrow.event.TimedEvent;
|
||||
import com.sparrowwallet.sparrow.event.WalletNodeHistoryChangedEvent;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
import com.sparrowwallet.sparrow.net.ElectrumServer;
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.rxjavafx.schedulers.JavaFxScheduler;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
|
@ -37,6 +41,7 @@ import javafx.event.ActionEvent;
|
|||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.util.Duration;
|
||||
import javafx.util.StringConverter;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.controlsfx.validation.ValidationResult;
|
||||
|
@ -69,6 +74,9 @@ public class InitiatorController extends SorobanController {
|
|||
@FXML
|
||||
private VBox step3;
|
||||
|
||||
@FXML
|
||||
private VBox step4;
|
||||
|
||||
@FXML
|
||||
private ComboBox<PayNym> payNymFollowers;
|
||||
|
||||
|
@ -108,6 +116,18 @@ public class InitiatorController extends SorobanController {
|
|||
@FXML
|
||||
private TransactionDiagram transactionDiagram;
|
||||
|
||||
@FXML
|
||||
private Label step4Desc;
|
||||
|
||||
@FXML
|
||||
private ProgressBar broadcastProgressBar;
|
||||
|
||||
@FXML
|
||||
private Label broadcastProgressLabel;
|
||||
|
||||
@FXML
|
||||
private Glyph broadcastSuccessful;
|
||||
|
||||
private final StringProperty counterpartyPayNymName = new SimpleStringProperty();
|
||||
|
||||
private final ObjectProperty<PaymentCode> counterpartyPaymentCode = new SimpleObjectProperty<>(null);
|
||||
|
@ -120,6 +140,10 @@ public class InitiatorController extends SorobanController {
|
|||
|
||||
private CahootsType cahootsType = CahootsType.STONEWALLX2;
|
||||
|
||||
private ElectrumServer.TransactionMempoolService transactionMempoolService;
|
||||
|
||||
private boolean closed;
|
||||
|
||||
public void initializeView(String walletId, Wallet wallet, WalletTransaction walletTransaction) {
|
||||
this.walletId = walletId;
|
||||
this.wallet = wallet;
|
||||
|
@ -128,6 +152,7 @@ public class InitiatorController extends SorobanController {
|
|||
step1.managedProperty().bind(step1.visibleProperty());
|
||||
step2.managedProperty().bind(step2.visibleProperty());
|
||||
step3.managedProperty().bind(step3.visibleProperty());
|
||||
step4.managedProperty().bind(step4.visibleProperty());
|
||||
|
||||
sorobanProgressBar.managedProperty().bind(sorobanProgressBar.visibleProperty());
|
||||
sorobanProgressLabel.managedProperty().bind(sorobanProgressLabel.visibleProperty());
|
||||
|
@ -135,12 +160,17 @@ public class InitiatorController extends SorobanController {
|
|||
sorobanProgressBar.visibleProperty().bind(sorobanProgressLabel.visibleProperty());
|
||||
mixDeclined.visibleProperty().bind(sorobanProgressLabel.visibleProperty().not());
|
||||
step2Timer.visibleProperty().bind(sorobanProgressLabel.visibleProperty());
|
||||
broadcastProgressBar.managedProperty().bind(broadcastProgressBar.visibleProperty());
|
||||
broadcastProgressLabel.managedProperty().bind(broadcastProgressLabel.visibleProperty());
|
||||
broadcastSuccessful.managedProperty().bind(broadcastSuccessful.visibleProperty());
|
||||
broadcastSuccessful.setVisible(false);
|
||||
|
||||
step2.setVisible(false);
|
||||
step3.setVisible(false);
|
||||
step4.setVisible(false);
|
||||
|
||||
transactionAccepted.addListener((observable, oldValue, accepted) -> {
|
||||
if(transactionProperty.get() != null) {
|
||||
if(transactionProperty.get() != null && stepProperty.get() != Step.REBROADCAST) {
|
||||
Platform.exitNestedEventLoop(transactionAccepted, accepted);
|
||||
}
|
||||
});
|
||||
|
@ -240,6 +270,16 @@ public class InitiatorController extends SorobanController {
|
|||
}
|
||||
});
|
||||
|
||||
stepProperty.addListener((observable, oldValue, step) -> {
|
||||
if(step == Step.BROADCAST) {
|
||||
step4Desc.setText("Broadcasting the mix transaction...");
|
||||
broadcastProgressLabel.setVisible(true);
|
||||
} else if(step == Step.REBROADCAST) {
|
||||
step4Desc.setText("Rebroadcast the mix transaction.");
|
||||
broadcastProgressLabel.setVisible(false);
|
||||
}
|
||||
});
|
||||
|
||||
Payment payment = walletTransaction.getPayments().get(0);
|
||||
if(payment.getAddress() instanceof PayNymAddress payNymAddress) {
|
||||
PayNym payNym = payNymAddress.getPayNym();
|
||||
|
@ -429,13 +469,14 @@ public class InitiatorController extends SorobanController {
|
|||
if(cahoots.getStep() == 3) {
|
||||
next();
|
||||
step3Timer.start(e -> {
|
||||
if(stepProperty.get() != Step.BROADCAST) {
|
||||
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) {
|
||||
stepProperty.set(Step.BROADCAST);
|
||||
next();
|
||||
broadcastTransaction();
|
||||
}
|
||||
}
|
||||
} catch(PSBTParseException e) {
|
||||
|
@ -458,6 +499,70 @@ public class InitiatorController extends SorobanController {
|
|||
}
|
||||
}
|
||||
|
||||
public void broadcastTransaction() {
|
||||
stepProperty.set(Step.BROADCAST);
|
||||
|
||||
ElectrumServer.BroadcastTransactionService broadcastTransactionService = new ElectrumServer.BroadcastTransactionService(getTransaction());
|
||||
broadcastTransactionService.setOnRunning(workerStateEvent -> {
|
||||
broadcastProgressBar.setProgress(-1);
|
||||
});
|
||||
broadcastTransactionService.setOnSucceeded(workerStateEvent -> {
|
||||
Map<BlockTransactionHashIndex, WalletNode> selectedUtxos = new HashMap<>();
|
||||
Map<BlockTransactionHashIndex, WalletNode> walletTxos = wallet.getWalletTxos();
|
||||
for(TransactionInput txInput : getTransaction().getInputs()) {
|
||||
Optional<BlockTransactionHashIndex> optSelectedUtxo = walletTxos.keySet().stream().filter(txo -> txInput.getOutpoint().getHash().equals(txo.getHash()) && txInput.getOutpoint().getIndex() == txo.getIndex())
|
||||
.findFirst();
|
||||
optSelectedUtxo.ifPresent(blockTransactionHashIndex -> selectedUtxos.put(blockTransactionHashIndex, walletTxos.get(blockTransactionHashIndex)));
|
||||
}
|
||||
|
||||
if(transactionMempoolService != null) {
|
||||
transactionMempoolService.cancel();
|
||||
}
|
||||
|
||||
transactionMempoolService = new ElectrumServer.TransactionMempoolService(wallet, getTransaction().getTxId(), new HashSet<>(selectedUtxos.values()));
|
||||
transactionMempoolService.setDelay(Duration.seconds(3));
|
||||
transactionMempoolService.setPeriod(Duration.seconds(10));
|
||||
transactionMempoolService.setRestartOnFailure(false);
|
||||
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.isRunning()) {
|
||||
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.");
|
||||
stepProperty.set(Step.REBROADCAST);
|
||||
}
|
||||
});
|
||||
transactionMempoolService.setOnFailed(mempoolWorkerStateEvent -> {
|
||||
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 indicate it had entered the mempool. It is safe to try broadcasting again.");
|
||||
stepProperty.set(Step.REBROADCAST);
|
||||
});
|
||||
|
||||
if(!closed) {
|
||||
transactionMempoolService.start();
|
||||
}
|
||||
});
|
||||
broadcastTransactionService.setOnFailed(workerStateEvent -> {
|
||||
broadcastProgressBar.setProgress(0);
|
||||
Throwable exception = workerStateEvent.getSource().getException();
|
||||
while(exception.getCause() != null) {
|
||||
exception = exception.getCause();
|
||||
}
|
||||
|
||||
log.error("Error broadcasting transaction", exception);
|
||||
AppServices.showErrorDialog("Error broadcasting transaction", exception.getMessage());
|
||||
stepProperty.set(Step.REBROADCAST);
|
||||
});
|
||||
broadcastTransactionService.start();
|
||||
}
|
||||
|
||||
public void next() {
|
||||
if(step1.isVisible()) {
|
||||
step1.setVisible(false);
|
||||
|
@ -470,6 +575,13 @@ public class InitiatorController extends SorobanController {
|
|||
step2.setVisible(false);
|
||||
step3.setVisible(true);
|
||||
stepProperty.set(Step.REVIEW);
|
||||
return;
|
||||
}
|
||||
|
||||
if(step3.isVisible()) {
|
||||
step3.setVisible(false);
|
||||
step4.setVisible(true);
|
||||
stepProperty.set(Step.BROADCAST);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -525,11 +637,36 @@ public class InitiatorController extends SorobanController {
|
|||
return transactionProperty.get();
|
||||
}
|
||||
|
||||
public void close() {
|
||||
closed = true;
|
||||
if(transactionMempoolService != null) {
|
||||
transactionMempoolService.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isTransactionAccepted() {
|
||||
return transactionAccepted.get() == Boolean.TRUE;
|
||||
}
|
||||
|
||||
public ObjectProperty<Boolean> transactionAcceptedProperty() {
|
||||
return transactionAccepted;
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletNodeHistoryChanged(WalletNodeHistoryChangedEvent event) {
|
||||
if(event.getWalletNode(wallet) != null) {
|
||||
if(transactionMempoolService != null) {
|
||||
transactionMempoolService.cancel();
|
||||
}
|
||||
|
||||
broadcastProgressBar.setVisible(false);
|
||||
broadcastProgressLabel.setVisible(false);
|
||||
step4Desc.setText("Transaction broadcasted.");
|
||||
broadcastSuccessful.setVisible(true);
|
||||
}
|
||||
}
|
||||
|
||||
public enum Step {
|
||||
SETUP, COMMUNICATE, REVIEW, BROADCAST
|
||||
SETUP, COMMUNICATE, REVIEW, BROADCAST, REBROADCAST
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,6 +41,8 @@ public class InitiatorDialog extends Dialog<Transaction> {
|
|||
InitiatorController initiatorController = initiatorLoader.getController();
|
||||
initiatorController.initializeView(walletId, wallet, walletTransaction);
|
||||
|
||||
EventManager.get().register(initiatorController);
|
||||
|
||||
dialogPane.setPrefWidth(730);
|
||||
dialogPane.setPrefHeight(530);
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
|
@ -51,18 +53,23 @@ public class InitiatorDialog extends Dialog<Transaction> {
|
|||
final ButtonType nextButtonType = new javafx.scene.control.ButtonType("Next", ButtonBar.ButtonData.OK_DONE);
|
||||
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||
final ButtonType broadcastButtonType = new javafx.scene.control.ButtonType("Sign & Broadcast", ButtonBar.ButtonData.APPLY);
|
||||
dialogPane.getButtonTypes().addAll(nextButtonType, cancelButtonType, broadcastButtonType);
|
||||
final ButtonType doneButtonType = new javafx.scene.control.ButtonType("Done", ButtonBar.ButtonData.APPLY);
|
||||
dialogPane.getButtonTypes().addAll(nextButtonType, cancelButtonType, broadcastButtonType, doneButtonType);
|
||||
|
||||
Button nextButton = (Button)dialogPane.lookupButton(nextButtonType);
|
||||
Button cancelButton = (Button)dialogPane.lookupButton(cancelButtonType);
|
||||
Button broadcastButton = (Button)dialogPane.lookupButton(broadcastButtonType);
|
||||
Button doneButton = (Button)dialogPane.lookupButton(doneButtonType);
|
||||
nextButton.setDisable(initiatorController.counterpartyPaymentCodeProperty().get() == null);
|
||||
broadcastButton.setDisable(true);
|
||||
|
||||
nextButton.managedProperty().bind(nextButton.visibleProperty());
|
||||
cancelButton.managedProperty().bind(cancelButton.visibleProperty());
|
||||
broadcastButton.managedProperty().bind(broadcastButton.visibleProperty());
|
||||
doneButton.managedProperty().bind(doneButton.visibleProperty());
|
||||
|
||||
broadcastButton.visibleProperty().bind(nextButton.visibleProperty().not());
|
||||
broadcastButton.setVisible(false);
|
||||
doneButton.setVisible(false);
|
||||
|
||||
initiatorController.counterpartyPaymentCodeProperty().addListener((observable, oldValue, paymentCode) -> {
|
||||
nextButton.setDisable(paymentCode == null || !AppServices.isConnected());
|
||||
|
@ -77,10 +84,19 @@ public class InitiatorDialog extends Dialog<Transaction> {
|
|||
nextButton.setVisible(true);
|
||||
} else if(step == InitiatorController.Step.REVIEW) {
|
||||
nextButton.setVisible(false);
|
||||
broadcastButton.setVisible(true);
|
||||
broadcastButton.setDefaultButton(true);
|
||||
broadcastButton.setDisable(false);
|
||||
} else if(step == InitiatorController.Step.BROADCAST) {
|
||||
setResult(initiatorController.getTransaction());
|
||||
cancelButton.setVisible(false);
|
||||
broadcastButton.setVisible(false);
|
||||
doneButton.setVisible(true);
|
||||
doneButton.setDefaultButton(true);
|
||||
} else if(step == InitiatorController.Step.REBROADCAST) {
|
||||
cancelButton.setVisible(true);
|
||||
broadcastButton.setVisible(true);
|
||||
broadcastButton.setDisable(false);
|
||||
doneButton.setVisible(false);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -98,10 +114,19 @@ public class InitiatorDialog extends Dialog<Transaction> {
|
|||
});
|
||||
|
||||
broadcastButton.addEventFilter(ActionEvent.ACTION, event -> {
|
||||
acceptAndBroadcast(initiatorController, walletId, wallet);
|
||||
if(initiatorController.isTransactionAccepted()) {
|
||||
initiatorController.broadcastTransaction();
|
||||
} else {
|
||||
acceptAndBroadcast(initiatorController, walletId, wallet);
|
||||
}
|
||||
event.consume();
|
||||
});
|
||||
|
||||
setOnCloseRequest(event -> {
|
||||
initiatorController.close();
|
||||
EventManager.get().unregister(initiatorController);
|
||||
});
|
||||
|
||||
setResultConverter(dialogButton -> dialogButton.getButtonData().equals(ButtonBar.ButtonData.APPLY) ? initiatorController.getTransaction() : null);
|
||||
} catch(IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
|
|
|
@ -1406,32 +1406,16 @@ public class SendController extends WalletFormController implements Initializabl
|
|||
return;
|
||||
}
|
||||
|
||||
InitiatorDialog initiatorDialog = new InitiatorDialog(getWalletForm().getWalletId(), getWalletForm().getWallet(), walletTransactionProperty.get());
|
||||
if(Config.get().isSameAppMixing()) {
|
||||
initiatorDialog.initModality(Modality.NONE);
|
||||
}
|
||||
Optional<Transaction> optTransaction = initiatorDialog.showAndWait();
|
||||
if(optTransaction.isPresent()) {
|
||||
ElectrumServer.BroadcastTransactionService broadcastTransactionService = new ElectrumServer.BroadcastTransactionService(optTransaction.get());
|
||||
broadcastTransactionService.setOnRunning(workerStateEvent -> {
|
||||
createButton.setDisable(true);
|
||||
addWalletTransactionNodes();
|
||||
});
|
||||
broadcastTransactionService.setOnSucceeded(workerStateEvent -> {
|
||||
createButton.setDisable(false);
|
||||
Platform.runLater(() -> {
|
||||
InitiatorDialog initiatorDialog = new InitiatorDialog(getWalletForm().getWalletId(), getWalletForm().getWallet(), walletTransactionProperty.get());
|
||||
if(Config.get().isSameAppMixing()) {
|
||||
initiatorDialog.initModality(Modality.NONE);
|
||||
}
|
||||
Optional<Transaction> optTransaction = initiatorDialog.showAndWait();
|
||||
if(optTransaction.isPresent()) {
|
||||
clear(null);
|
||||
});
|
||||
broadcastTransactionService.setOnFailed(workerStateEvent -> {
|
||||
createButton.setDisable(false);
|
||||
Throwable exception = workerStateEvent.getSource().getException();
|
||||
while(exception.getCause() != null) {
|
||||
exception = exception.getCause();
|
||||
}
|
||||
|
||||
AppServices.showErrorDialog("Error broadcasting mix transaction", exception.getMessage());
|
||||
});
|
||||
broadcastTransactionService.start();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -106,6 +106,26 @@
|
|||
<TransactionDiagram fx:id="transactionDiagram" maxWidth="700" final="true"/>
|
||||
</HBox>
|
||||
</VBox>
|
||||
<VBox fx:id="step4" spacing="15">
|
||||
<HBox>
|
||||
<Label text="Broadcast Transaction" styleClass="title-text">
|
||||
<graphic>
|
||||
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="20" icon="RANDOM" styleClass="title-icon" />
|
||||
</graphic>
|
||||
</Label>
|
||||
</HBox>
|
||||
<Label fx:id="step4Desc" text="Broadcasting the mix transaction..." wrapText="true" styleClass="content-text" />
|
||||
<HBox>
|
||||
<padding>
|
||||
<Insets top="20" />
|
||||
</padding>
|
||||
<ProgressBar fx:id="broadcastProgressBar" prefWidth="680" />
|
||||
</HBox>
|
||||
<VBox alignment="CENTER" spacing="20">
|
||||
<Label fx:id="broadcastProgressLabel" text="Broadcasting..." styleClass="content-text" alignment="CENTER"/>
|
||||
<Glyph fx:id="broadcastSuccessful" fontFamily="Font Awesome 5 Free Solid" fontSize="80" icon="CHECK_CIRCLE" styleClass="success" />
|
||||
</VBox>
|
||||
</VBox>
|
||||
</VBox>
|
||||
</VBox>
|
||||
</StackPane>
|
||||
|
|
Loading…
Reference in a new issue