diff --git a/drongo b/drongo index a5668424..dcd4218b 160000 --- a/drongo +++ b/drongo @@ -1 +1 @@ -Subproject commit a56684240a1a9a44f96b2a57fd10799bb9f8a18a +Subproject commit dcd4218ba14ecea9113925b80af02fdc3287079a diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java index 0e6fc24d..f98e1abc 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppController.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppController.java @@ -28,6 +28,7 @@ import javafx.animation.*; import javafx.application.Platform; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; +import javafx.collections.ListChangeListener; import javafx.concurrent.Worker; import javafx.event.ActionEvent; import javafx.fxml.FXML; @@ -158,6 +159,13 @@ public class AppController implements Initializable { } }); + tabs.setTabDragPolicy(TabPane.TabDragPolicy.REORDER); + tabs.getTabs().addListener((ListChangeListener) c -> { + if(c.next() && (c.wasAdded() || c.wasRemoved())) { + EventManager.get().post(new OpenWalletsEvent(getOpenWallets())); + } + }); + BitcoinUnit unit = Config.get().getBitcoinUnit(); if(unit == null) { unit = BitcoinUnit.AUTO; @@ -401,6 +409,20 @@ public class AppController implements Initializable { return fiatCurrencyExchangeRate; } + public List getOpenWallets() { + List openWallets = new ArrayList<>(); + + for(Tab tab : tabs.getTabs()) { + TabData tabData = (TabData)tab.getUserData(); + if(tabData.getType() == TabData.TabType.WALLET) { + WalletTabData walletTabData = (WalletTabData) tabData; + openWallets.add(walletTabData.getWallet()); + } + } + + return openWallets; + } + public static void showErrorDialog(String title, String content) { Alert alert = new Alert(Alert.AlertType.ERROR); alert.setTitle(title); @@ -887,4 +909,14 @@ public class AppController implements Initializable { public void exchangeRatesUpdated(ExchangeRatesUpdatedEvent event) { fiatCurrencyExchangeRate = event.getCurrencyRate(); } + + @Subscribe + public void requestOpenWallets(RequestOpenWalletsEvent event) { + EventManager.get().post(new OpenWalletsEvent(getOpenWallets())); + } + + @Subscribe + public void requestWalletOpen(RequestWalletOpenEvent event) { + openWallet(null); + } } \ No newline at end of file diff --git a/src/main/java/com/sparrowwallet/sparrow/event/FinalizePSBTEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/FinalizePSBTEvent.java new file mode 100644 index 00000000..e63a40b0 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/event/FinalizePSBTEvent.java @@ -0,0 +1,22 @@ +package com.sparrowwallet.sparrow.event; + +import com.sparrowwallet.drongo.psbt.PSBT; +import com.sparrowwallet.drongo.wallet.Wallet; + +public class FinalizePSBTEvent { + private final PSBT psbt; + private final Wallet signingWallet; + + public FinalizePSBTEvent(PSBT psbt, Wallet signingWallet) { + this.psbt = psbt; + this.signingWallet = signingWallet; + } + + public PSBT getPsbt() { + return psbt; + } + + public Wallet getSigningWallet() { + return signingWallet; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/event/OpenWalletsEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/OpenWalletsEvent.java new file mode 100644 index 00000000..b096d349 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/event/OpenWalletsEvent.java @@ -0,0 +1,17 @@ +package com.sparrowwallet.sparrow.event; + +import com.sparrowwallet.drongo.wallet.Wallet; + +import java.util.List; + +public class OpenWalletsEvent { + private final List wallets; + + public OpenWalletsEvent(List wallets) { + this.wallets = wallets; + } + + public List getWallets() { + return wallets; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/event/RequestOpenWalletsEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/RequestOpenWalletsEvent.java new file mode 100644 index 00000000..cadb739b --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/event/RequestOpenWalletsEvent.java @@ -0,0 +1,5 @@ +package com.sparrowwallet.sparrow.event; + +public class RequestOpenWalletsEvent { + //Empty event class used to request a List of opened wallets +} diff --git a/src/main/java/com/sparrowwallet/sparrow/event/RequestWalletOpenEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/RequestWalletOpenEvent.java new file mode 100644 index 00000000..b6796f2f --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/event/RequestWalletOpenEvent.java @@ -0,0 +1,5 @@ +package com.sparrowwallet.sparrow.event; + +public class RequestWalletOpenEvent { + //Empty event class used to request the wallet open dialog +} diff --git a/src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5.java b/src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5.java index 6ab15b3d..ecc5a0ac 100644 --- a/src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5.java +++ b/src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5.java @@ -26,6 +26,7 @@ public class FontAwesome5 extends GlyphFont { LAPTOP('\uf109'), LOCK('\uf023'), LOCK_OPEN('\uf3c1'), + PEN_FANCY('\uf5ac'), QUESTION_CIRCLE('\uf059'), SD_CARD('\uf7c2'), SEARCH('\uf002'), diff --git a/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java b/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java index 16e7d1ca..a6af5f70 100644 --- a/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java +++ b/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java @@ -1,19 +1,17 @@ package com.sparrowwallet.sparrow.transaction; -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.psbt.PSBT; +import com.sparrowwallet.drongo.psbt.PSBTInput; import com.sparrowwallet.drongo.wallet.BlockTransaction; +import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.sparrow.AppController; import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.control.CoinLabel; import com.sparrowwallet.sparrow.control.IdLabel; import com.sparrowwallet.sparrow.control.CopyableLabel; -import com.sparrowwallet.sparrow.event.BitcoinUnitChangedEvent; -import com.sparrowwallet.sparrow.event.BlockTransactionFetchedEvent; -import com.sparrowwallet.sparrow.event.TransactionChangedEvent; -import com.sparrowwallet.sparrow.event.TransactionLocktimeChangedEvent; +import com.sparrowwallet.sparrow.event.*; +import javafx.collections.FXCollections; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.fxml.Initializable; @@ -29,8 +27,10 @@ import tornadofx.control.Form; import java.net.URL; import java.text.SimpleDateFormat; import java.time.*; +import java.util.List; import java.util.Map; import java.util.ResourceBundle; +import java.util.stream.Collectors; public class HeadersController extends TransactionFormController implements Initializable { public static final String LOCKTIME_DATE_FORMAT = "yyyy-MM-dd HH:mm:ss"; @@ -107,6 +107,30 @@ public class HeadersController extends TransactionFormController implements Init @FXML private IdLabel blockHash; + @FXML + private Form finalizeForm; + + @FXML + private ComboBox signingWallet; + + @FXML + private Label noWalletsWarning; + + @FXML + private Hyperlink noWalletsWarningLink; + + @FXML + private ComboBox sigHash; + + @FXML + private Button finalizeTransaction; + + @FXML + private Form signaturesForm; + + @FXML + private Form broadcastForm; + @Override public void initialize(URL location, ResourceBundle resources) { EventManager.get().register(this); @@ -237,10 +261,49 @@ public class HeadersController extends TransactionFormController implements Init } blockchainForm.managedProperty().bind(blockchainForm.visibleProperty()); + finalizeForm.managedProperty().bind(finalizeForm.visibleProperty()); + signaturesForm.managedProperty().bind(signaturesForm.visibleProperty()); + broadcastForm.managedProperty().bind(broadcastForm.visibleProperty()); + + blockchainForm.setVisible(false); + finalizeForm.setVisible(false); + signaturesForm.setVisible(false); + broadcastForm.setVisible(false); + if(headersForm.getBlockTransaction() != null) { + blockchainForm.setVisible(true); updateBlockchainForm(headersForm.getBlockTransaction()); - } else { - blockchainForm.setVisible(false); + } else if(headersForm.getPsbt() != null) { + PSBT psbt = headersForm.getPsbt(); + + if(headersForm.isEditable()) { + finalizeForm.setVisible(true); + } else if(headersForm.getPsbt().isSigned()) { + broadcastForm.setVisible(true); + } else { + signaturesForm.setVisible(true); + } + + signingWallet.valueProperty().addListener((observable, oldValue, newValue) -> headersForm.setSigningWallet(newValue)); + EventManager.get().post(new RequestOpenWalletsEvent()); + + signingWallet.managedProperty().bind(signingWallet.visibleProperty()); + noWalletsWarning.managedProperty().bind(noWalletsWarning.visibleProperty()); + noWalletsWarningLink.managedProperty().bind(noWalletsWarningLink.visibleProperty()); + noWalletsWarningLink.visibleProperty().bind(noWalletsWarning.visibleProperty()); + + SigHash psbtSigHash = SigHash.ALL; + for(PSBTInput psbtInput : psbt.getPsbtInputs()) { + if(psbtInput.getSigHash() != null) { + psbtSigHash = psbtInput.getSigHash(); + } + } + sigHash.setValue(psbtSigHash); + sigHash.valueProperty().addListener((observable, oldValue, newValue) -> { + for(PSBTInput psbtInput : psbt.getPsbtInputs()) { + psbtInput.setSigHash(newValue); + } + }); } } @@ -331,6 +394,14 @@ public class HeadersController extends TransactionFormController implements Init Clipboard.getSystemClipboard().setContent(content); } + public void openWallet(ActionEvent event) { + EventManager.get().post(new RequestWalletOpenEvent()); + } + + public void finalizeTransaction(ActionEvent event) { + EventManager.get().post(new FinalizePSBTEvent(headersForm.getPsbt(), headersForm.getSigningWallet())); + } + @Subscribe public void transactionChanged(TransactionChangedEvent event) { if(headersForm.getTransaction().equals(event.getTransaction())) { @@ -362,4 +433,41 @@ public class HeadersController extends TransactionFormController implements Init public void bitcoinUnitChanged(BitcoinUnitChangedEvent event) { fee.refresh(event.getBitcoinUnit()); } + + @Subscribe + public void openWallets(OpenWalletsEvent event) { + if(headersForm.getPsbt() != null && headersForm.isEditable()) { + List availableWallets = event.getWallets().stream().filter(wallet -> wallet.canSign(headersForm.getPsbt())).collect(Collectors.toList()); + signingWallet.setItems(FXCollections.observableList(availableWallets)); + if(!availableWallets.isEmpty()) { + if(availableWallets.contains(headersForm.getSigningWallet())) { + signingWallet.setValue(headersForm.getSigningWallet()); + } else { + signingWallet.setValue(availableWallets.get(0)); + } + noWalletsWarning.setVisible(false); + signingWallet.setVisible(true); + finalizeTransaction.setDisable(false); + } else { + noWalletsWarning.setVisible(true); + signingWallet.setVisible(false); + finalizeTransaction.setDisable(true); + } + } + } + + @Subscribe + public void finalizePSBT(FinalizePSBTEvent event) { + if(headersForm.getPsbt() == event.getPsbt()) { + version.setDisable(true); + locktimeNoneType.setDisable(true); + locktimeBlockType.setDisable(true); + locktimeDateType.setDisable(true); + locktimeBlock.setDisable(true); + locktimeDate.setDisable(true); + + finalizeForm.setVisible(false); + signaturesForm.setVisible(true); + } + } } \ No newline at end of file diff --git a/src/main/java/com/sparrowwallet/sparrow/transaction/InputController.java b/src/main/java/com/sparrowwallet/sparrow/transaction/InputController.java index 76a7fb10..0920efb7 100644 --- a/src/main/java/com/sparrowwallet/sparrow/transaction/InputController.java +++ b/src/main/java/com/sparrowwallet/sparrow/transaction/InputController.java @@ -500,4 +500,17 @@ public class InputController extends TransactionFormController implements Initia public void bitcoinUnitChanged(BitcoinUnitChangedEvent event) { spends.refresh(event.getBitcoinUnit()); } + + @Subscribe + public void finalizePSBT(FinalizePSBTEvent event) { + if(inputForm.getPsbt() == event.getPsbt()) { + rbf.setDisable(true); + locktimeNoneType.setDisable(true); + locktimeAbsoluteType.setDisable(true); + locktimeRelativeType.setDisable(true); + locktimeRelativeBlocks.setDisable(true); + locktimeRelativeSeconds.setDisable(true); + locktimeRelativeCombo.setDisable(true); + } + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/transaction/TransactionData.java b/src/main/java/com/sparrowwallet/sparrow/transaction/TransactionData.java index c9d22e3b..6d34a515 100644 --- a/src/main/java/com/sparrowwallet/sparrow/transaction/TransactionData.java +++ b/src/main/java/com/sparrowwallet/sparrow/transaction/TransactionData.java @@ -4,6 +4,7 @@ import com.sparrowwallet.drongo.protocol.Sha256Hash; import com.sparrowwallet.drongo.protocol.Transaction; import com.sparrowwallet.drongo.psbt.PSBT; import com.sparrowwallet.drongo.wallet.BlockTransaction; +import com.sparrowwallet.drongo.wallet.Wallet; import java.util.List; import java.util.Map; @@ -20,6 +21,8 @@ public class TransactionData { private int minOutputFetched; private int maxOutputFetched; + private Wallet signingWallet; + public TransactionData(PSBT psbt) { this.transaction = psbt.getTransaction(); this.psbt = psbt; @@ -109,4 +112,12 @@ public class TransactionData { public boolean allOutputsFetched() { return minOutputFetched == 0 && maxOutputFetched == transaction.getOutputs().size(); } + + public Wallet getSigningWallet() { + return signingWallet; + } + + public void setSigningWallet(Wallet signingWallet) { + this.signingWallet = signingWallet; + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/transaction/TransactionForm.java b/src/main/java/com/sparrowwallet/sparrow/transaction/TransactionForm.java index f99b5d96..449ab83a 100644 --- a/src/main/java/com/sparrowwallet/sparrow/transaction/TransactionForm.java +++ b/src/main/java/com/sparrowwallet/sparrow/transaction/TransactionForm.java @@ -4,6 +4,7 @@ import com.sparrowwallet.drongo.protocol.Sha256Hash; import com.sparrowwallet.drongo.protocol.Transaction; import com.sparrowwallet.drongo.psbt.PSBT; import com.sparrowwallet.drongo.wallet.BlockTransaction; +import com.sparrowwallet.drongo.wallet.Wallet; import javafx.scene.Node; import java.io.IOException; @@ -53,8 +54,27 @@ public abstract class TransactionForm { return txdata.allOutputsFetched(); } + public Wallet getSigningWallet() { + return txdata.getSigningWallet(); + } + + public void setSigningWallet(Wallet signingWallet) { + txdata.setSigningWallet(signingWallet); + } + public boolean isEditable() { - return txdata.getBlockTransaction() == null; + if(getBlockTransaction() != null) { + return false; + } + + if(getPsbt() != null) { + if(getPsbt().hasSignatures() || getPsbt().isSigned()) { + return false; + } + return txdata.getSigningWallet() == null; + } + + return true; } public abstract Node getContents() throws IOException; diff --git a/src/main/resources/com/sparrowwallet/sparrow/preferences/server.fxml b/src/main/resources/com/sparrowwallet/sparrow/preferences/server.fxml index b750bdcc..e6d47df2 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/preferences/server.fxml +++ b/src/main/resources/com/sparrowwallet/sparrow/preferences/server.fxml @@ -54,7 +54,7 @@ - + + + +
+
+
+
+ +
+
+
+
+ diff --git a/src/main/resources/com/sparrowwallet/sparrow/wallet/receive.fxml b/src/main/resources/com/sparrowwallet/sparrow/wallet/receive.fxml index 5e25b7a5..df3f41bf 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/wallet/receive.fxml +++ b/src/main/resources/com/sparrowwallet/sparrow/wallet/receive.fxml @@ -85,7 +85,7 @@ -