diff --git a/drongo b/drongo index dcd4218b..f6dcdb6d 160000 --- a/drongo +++ b/drongo @@ -1 +1 @@ -Subproject commit dcd4218ba14ecea9113925b80af02fdc3287079a +Subproject commit f6dcdb6d26c3b40ae1c0f7502b3e526aa8959564 diff --git a/src/main/java/com/sparrowwallet/sparrow/control/SignaturesProgressBar.java b/src/main/java/com/sparrowwallet/sparrow/control/SignaturesProgressBar.java new file mode 100644 index 00000000..f1ba187a --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/control/SignaturesProgressBar.java @@ -0,0 +1,150 @@ +package com.sparrowwallet.sparrow.control; + +import com.sparrowwallet.drongo.wallet.Keystore; +import javafx.animation.KeyFrame; +import javafx.animation.KeyValue; +import javafx.animation.Timeline; +import javafx.beans.property.SimpleObjectProperty; +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; +import javafx.geometry.Insets; +import javafx.geometry.Orientation; +import javafx.geometry.Pos; +import javafx.scene.control.Label; +import javafx.scene.control.ProgressBar; +import javafx.scene.layout.StackPane; +import javafx.util.Duration; +import org.controlsfx.control.SegmentedBar; + +public class SignaturesProgressBar extends SegmentedBar { + public SignaturesProgressBar() { + setOrientation(Orientation.HORIZONTAL); + setSegmentViewFactory(SignatureProgressSegmentView::new); + setInfoNodeFactory(segment -> segment.getKeystore() == null ? null : new SignatureProgressSegmentLabel(segment.getKeystore().getLabel())); + } + + public void initialize(ObservableList signedKeystores, int threshold) { + getStyleClass().add("signatures-progress-bar"); + getSegments().clear(); + + int numSegments = Math.max(threshold, signedKeystores.size()); + double segmentSize = 100d / numSegments; + for(int i = 0; i < numSegments; i++) { + if(i < signedKeystores.size()) { + getSegments().add(new SignatureProgressSegment(segmentSize, i, signedKeystores.get(i))); + } else { + getSegments().add(new SignatureProgressSegment(segmentSize, i, null)); + } + } + + signedKeystores.addListener((ListChangeListener) c -> { + int numSegments1 = Math.max(threshold, c.getList().size()); + double newSegmentSize = 100d / numSegments1; + + for(int i = 0; i < numSegments1; i++) { + SignatureProgressSegment segment = null; + if(i < getSegments().size()) { + segment = getSegments().get(i); + } + + Keystore signedKeystore = null; + if(i < signedKeystores.size()) { + signedKeystore = signedKeystores.get(i); + } + + if(segment != null) { + //Animate new signature if changed + segment.setKeystore(signedKeystore); + } else { + //Add extra (unnecessary) signature + for(SignaturesProgressBar.SignatureProgressSegment existingSegment : getSegments()) { + existingSegment.setValue(newSegmentSize); + } + + SignaturesProgressBar.SignatureProgressSegment newSegment = new SignatureProgressSegment(newSegmentSize, i, null); + getSegments().add(newSegment); + newSegment.setKeystore(signedKeystore); + } + } + }); + } + + public static class SignatureProgressSegment extends SegmentedBar.Segment { + private final SimpleObjectProperty keystoreProperty; + private final int index; + + public SignatureProgressSegment(double value, int index, Keystore keystore) { + super(value); + this.index = index; + + this.keystoreProperty = new SimpleObjectProperty<>(this, "keystore", null); + keystoreProperty.addListener((observable, oldValue, newValue) -> { + setText(newValue == null ? "No keystore" : newValue.getLabel()); + }); + + setKeystore(keystore); + } + + public int getIndex() { + return index; + } + + public Keystore getKeystore() { + return keystoreProperty.get(); + } + + public SimpleObjectProperty keystoreProperty() { + return keystoreProperty; + } + + public void setKeystore(Keystore keystore) { + keystoreProperty.set(keystore); + } + } + + public static class SignatureProgressSegmentView extends StackPane { + private final ProgressBar progressBar; + private final Label label; + + public SignatureProgressSegmentView(SignatureProgressSegment segment) { + getStyleClass().add("signature-progress-segment"); + getStyleClass().add("segment" + segment.getIndex()); + + label = new Label(); + label.textProperty().bind(segment.textProperty()); + label.getStyleClass().add("signature-progress-segment-label"); + StackPane.setAlignment(label, Pos.CENTER); + + progressBar = new ProgressBar(segment.getKeystore() == null ? 0.0 : 1.0); + progressBar.setPrefWidth(Double.MAX_VALUE); + progressBar.setPrefHeight(30); + + setPrefHeight(50); + getChildren().addAll(progressBar, label); + + segment.keystoreProperty().addListener((observable, oldValue, newValue) -> { + if(oldValue == null && newValue != null) { + Timeline timeline = new Timeline( + new KeyFrame(Duration.ZERO, new KeyValue(progressBar.progressProperty(), 0)), + new KeyFrame(Duration.millis(800), new KeyValue(progressBar.progressProperty(), 1)) + ); + timeline.setCycleCount(1); + timeline.play(); + } + }); + } + + @Override + protected void layoutChildren() { + super.layoutChildren(); + label.setVisible(label.prefWidth(-1) < getWidth() - getPadding().getLeft() - getPadding().getRight() - 8); + } + } + + public static class SignatureProgressSegmentLabel extends Label { + public SignatureProgressSegmentLabel(String text) { + super(text); + setPadding(new Insets(10)); + } + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5.java b/src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5.java index ecc5a0ac..1c3b9daa 100644 --- a/src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5.java +++ b/src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5.java @@ -27,6 +27,7 @@ public class FontAwesome5 extends GlyphFont { LOCK('\uf023'), LOCK_OPEN('\uf3c1'), PEN_FANCY('\uf5ac'), + QRCODE('\uf029'), 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 ab18d470..815d8079 100644 --- a/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java +++ b/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java @@ -4,13 +4,17 @@ 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.Keystore; +import com.sparrowwallet.drongo.wallet.KeystoreSource; 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.control.SignaturesProgressBar; import com.sparrowwallet.sparrow.event.*; +import com.sparrowwallet.sparrow.glyphfont.FontAwesome5Brands; import javafx.collections.FXCollections; import javafx.event.ActionEvent; import javafx.fxml.FXML; @@ -18,6 +22,7 @@ import javafx.fxml.Initializable; import javafx.scene.control.*; import javafx.scene.input.Clipboard; import javafx.scene.input.ClipboardContent; +import org.controlsfx.glyphfont.Glyph; import tornadofx.control.DateTimePicker; import tornadofx.control.Field; import tornadofx.control.Fieldset; @@ -27,9 +32,7 @@ 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.*; import java.util.stream.Collectors; public class HeadersController extends TransactionFormController implements Initializable { @@ -128,6 +131,12 @@ public class HeadersController extends TransactionFormController implements Init @FXML private Form signaturesForm; + @FXML + private SignaturesProgressBar signaturesProgressBar; + + @FXML + private Button signButton; + @FXML private Form broadcastForm; @@ -303,6 +312,17 @@ public class HeadersController extends TransactionFormController implements Init psbtInput.setSigHash(newValue); } }); + + headersForm.signingWalletProperty().addListener((observable, oldValue, signingWallet) -> { + initializeSignButton(signingWallet); + + Map> signedKeystoresMap = signingWallet.getSignedKeystores(headersForm.getPsbt()); + Optional> optSignedKeystores = signedKeystoresMap.values().stream().filter(list -> !list.isEmpty()).min(Comparator.comparingInt(List::size)); + optSignedKeystores.ifPresent(keystores -> headersForm.getSignedKeystores().setAll(keystores)); + + int threshold = signingWallet.getDefaultPolicy().getNumSignaturesRequired(); + signaturesProgressBar.initialize(headersForm.getSignedKeystores(), threshold); + }); } } @@ -370,6 +390,18 @@ public class HeadersController extends TransactionFormController implements Init } } + private void initializeSignButton(Wallet signingWallet) { + Optional softwareKeystore = signingWallet.getKeystores().stream().filter(keystore -> keystore.getSource().equals(KeystoreSource.SW_SEED)).findAny(); + Optional usbKeystore = signingWallet.getKeystores().stream().filter(keystore -> keystore.getSource().equals(KeystoreSource.HW_USB)).findAny(); + if(softwareKeystore.isEmpty() && usbKeystore.isEmpty()) { + signButton.setDisable(true); + } else if(softwareKeystore.isEmpty()) { + Glyph usbGlyph = new Glyph(FontAwesome5Brands.FONT_NAME, FontAwesome5Brands.Glyph.USB); + usbGlyph.setFontSize(20); + signButton.setGraphic(usbGlyph); + } + } + private static class BlockHeightContextMenu extends ContextMenu { public BlockHeightContextMenu(Sha256Hash blockHash) { MenuItem copyBlockHash = new MenuItem("Copy Block Hash"); @@ -404,6 +436,18 @@ public class HeadersController extends TransactionFormController implements Init EventManager.get().post(new FinalizePSBTEvent(headersForm.getPsbt(), signingWallet.getValue())); } + public void showPSBT(ActionEvent event) { + + } + + public void savePSBT(ActionEvent event) { + + } + + public void signPSBT(ActionEvent event) { + headersForm.getSignedKeystores().add(headersForm.getSigningWallet().getKeystores().get(0)); + } + @Subscribe public void transactionChanged(TransactionChangedEvent event) { if(headersForm.getTransaction().equals(event.getTransaction())) { diff --git a/src/main/java/com/sparrowwallet/sparrow/transaction/TransactionData.java b/src/main/java/com/sparrowwallet/sparrow/transaction/TransactionData.java index 6d34a515..1f0b3bdb 100644 --- a/src/main/java/com/sparrowwallet/sparrow/transaction/TransactionData.java +++ b/src/main/java/com/sparrowwallet/sparrow/transaction/TransactionData.java @@ -4,7 +4,11 @@ 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.Keystore; import com.sparrowwallet.drongo.wallet.Wallet; +import javafx.beans.property.SimpleObjectProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; import java.util.List; import java.util.Map; @@ -21,7 +25,8 @@ public class TransactionData { private int minOutputFetched; private int maxOutputFetched; - private Wallet signingWallet; + private final SimpleObjectProperty signingWallet = new SimpleObjectProperty<>(this, "signingWallet", null); + private final ObservableList signedKeystores = FXCollections.observableArrayList(); public TransactionData(PSBT psbt) { this.transaction = psbt.getTransaction(); @@ -114,10 +119,18 @@ public class TransactionData { } public Wallet getSigningWallet() { + return signingWallet.get(); + } + + public SimpleObjectProperty signingWalletProperty() { return signingWallet; } - public void setSigningWallet(Wallet signingWallet) { - this.signingWallet = signingWallet; + public void setSigningWallet(Wallet wallet) { + this.signingWallet.set(wallet); + } + + public ObservableList getSignedKeystores() { + return signedKeystores; } } diff --git a/src/main/java/com/sparrowwallet/sparrow/transaction/TransactionForm.java b/src/main/java/com/sparrowwallet/sparrow/transaction/TransactionForm.java index 449ab83a..1ef22d9a 100644 --- a/src/main/java/com/sparrowwallet/sparrow/transaction/TransactionForm.java +++ b/src/main/java/com/sparrowwallet/sparrow/transaction/TransactionForm.java @@ -4,7 +4,10 @@ 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.Keystore; import com.sparrowwallet.drongo.wallet.Wallet; +import javafx.beans.property.SimpleObjectProperty; +import javafx.collections.ObservableList; import javafx.scene.Node; import java.io.IOException; @@ -58,10 +61,18 @@ public abstract class TransactionForm { return txdata.getSigningWallet(); } + public SimpleObjectProperty signingWalletProperty() { + return txdata.signingWalletProperty(); + } + public void setSigningWallet(Wallet signingWallet) { txdata.setSigningWallet(signingWallet); } + public ObservableList getSignedKeystores() { + return txdata.getSignedKeystores(); + } + public boolean isEditable() { if(getBlockTransaction() != null) { return false; diff --git a/src/main/resources/com/sparrowwallet/sparrow/transaction/headers.css b/src/main/resources/com/sparrowwallet/sparrow/transaction/headers.css index 667cbdc3..d7b7abb1 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/transaction/headers.css +++ b/src/main/resources/com/sparrowwallet/sparrow/transaction/headers.css @@ -37,3 +37,87 @@ -fx-text-fill: rgb(202, 18, 67); } +.signatures-buttons { + -fx-padding: 10 20 10 20; +} + +.signatures-buttons .button { + -fx-pref-height: 75px; + -fx-max-width: Infinity; +} + +.signatures-progress-bar { + -fx-padding: 10 0 10 0; +} + +.signatures-progress-bar { + -fx-padding: 10 0 10 0; +} + +.signatures-progress-bar > .segment { + -fx-background-color: transparent; +} + +.signature-progress-segment-label { + -fx-text-fill: white; +} + +.signatures-progress-bar > .only-segment { + +} + +.signatures-progress-bar > .first-segment { + -fx-padding: 0 3 0 0; +} + +.signatures-progress-bar > .middle-segment { + -fx-padding: 0 3 0 3; +} + +.signatures-progress-bar > .last-segment { + -fx-padding: 0 0 0 3; +} + +.segment0 .progress-bar { + -fx-accent: CHART_COLOR_3; +} + +.segment1 .progress-bar { + -fx-accent: CHART_COLOR_4; +} + +.segment2 .progress-bar { + -fx-accent: CHART_COLOR_7; +} + +.segment3 .progress-bar { + -fx-accent: CHART_COLOR_5; +} + +.segment4 .progress-bar { + -fx-accent: CHART_COLOR_6; +} + +.segment5 .progress-bar { + -fx-accent: CHART_COLOR_2; +} + +.segment6 .progress-bar { + -fx-accent: CHART_COLOR_1; +} + +.segment7 .progress-bar { + -fx-accent: CHART_COLOR_8; +} + +.segment8 .progress-bar { + -fx-accent: #3DA0E3; +} + +.segment9 .progress-bar { + -fx-accent: #f9c74f; +} + +.segment10 .progress-bar { + -fx-accent: #f94144; +} \ No newline at end of file diff --git a/src/main/resources/com/sparrowwallet/sparrow/transaction/headers.fxml b/src/main/resources/com/sparrowwallet/sparrow/transaction/headers.fxml index ab1da1f2..cfe98178 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/transaction/headers.fxml +++ b/src/main/resources/com/sparrowwallet/sparrow/transaction/headers.fxml @@ -21,6 +21,7 @@ + @@ -165,6 +166,28 @@
+ + + + + + + + + +