From 3db7fc1e9966b02f41020c1822f15626577285ec Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Tue, 26 May 2020 17:55:35 +0200 Subject: [PATCH] receive pane --- build.gradle | 2 +- drongo | 2 +- .../sparrowwallet/sparrow/BaseController.java | 113 ++++++++++++++++++ .../sparrow/control/AddressTreeTable.java | 111 +++++++---------- .../sparrow/control/CopyableTextField.java | 64 ++++++++++ .../sparrow/event/ReceiveActionEvent.java | 12 +- .../com/sparrowwallet/sparrow/io/Hwi.java | 4 +- .../TransactionFormController.java | 110 +---------------- .../sparrow/wallet/AddressesController.java | 4 +- .../sparrowwallet/sparrow/wallet/Entry.java | 33 +++++ .../sparrow/wallet/NodeEntry.java | 25 ++++ .../sparrow/wallet/ReceiveController.java | 104 ++++++++++++++++ .../sparrow/wallet/WalletForm.java | 43 ++++++- .../sparrow/wallet/WalletFormController.java | 4 +- src/main/java/module-info.java | 3 +- .../com/sparrowwallet/sparrow/general.css | 19 +++ .../sparrow/{transaction => }/script.css | 0 .../sparrow/transaction/input.fxml | 2 +- .../sparrow/transaction/output.fxml | 2 +- .../sparrow/wallet/addresses.css | 2 +- .../sparrowwallet/sparrow/wallet/receive.css | 8 ++ .../sparrowwallet/sparrow/wallet/receive.fxml | 83 +++++++++++++ 22 files changed, 557 insertions(+), 193 deletions(-) create mode 100644 src/main/java/com/sparrowwallet/sparrow/BaseController.java create mode 100644 src/main/java/com/sparrowwallet/sparrow/control/CopyableTextField.java create mode 100644 src/main/java/com/sparrowwallet/sparrow/wallet/Entry.java create mode 100644 src/main/java/com/sparrowwallet/sparrow/wallet/NodeEntry.java create mode 100644 src/main/java/com/sparrowwallet/sparrow/wallet/ReceiveController.java rename src/main/resources/com/sparrowwallet/sparrow/{transaction => }/script.css (100%) create mode 100644 src/main/resources/com/sparrowwallet/sparrow/wallet/receive.css create mode 100644 src/main/resources/com/sparrowwallet/sparrow/wallet/receive.fxml diff --git a/build.gradle b/build.gradle index f67b98f8..212d8e11 100644 --- a/build.gradle +++ b/build.gradle @@ -32,7 +32,7 @@ dependencies { implementation('com.google.code.gson:gson:2.8.6') implementation('org.fxmisc.richtext:richtextfx:0.10.4') implementation('no.tornado:tornadofx-controls:1.0.4') - implementation('org.apache.commons:commons-compress:1.20') + implementation('com.google.zxing:javase:3.4.0') implementation('org.controlsfx:controlsfx:11.0.1' ) { exclude group: 'org.openjfx', module: 'javafx-base' exclude group: 'org.openjfx', module: 'javafx-graphics' diff --git a/drongo b/drongo index eabcf4e8..78714135 160000 --- a/drongo +++ b/drongo @@ -1 +1 @@ -Subproject commit eabcf4e8f48ae18ff8d21436a2ab5e5153719944 +Subproject commit 7871413573e67ed7539cf03d6deadd1a2c4abafa diff --git a/src/main/java/com/sparrowwallet/sparrow/BaseController.java b/src/main/java/com/sparrowwallet/sparrow/BaseController.java new file mode 100644 index 00000000..3bedd60e --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/BaseController.java @@ -0,0 +1,113 @@ +package com.sparrowwallet.sparrow; + +import com.sparrowwallet.drongo.protocol.Script; +import com.sparrowwallet.drongo.protocol.ScriptChunk; +import com.sparrowwallet.sparrow.transaction.ScriptContextMenu; +import javafx.geometry.Point2D; +import javafx.scene.control.Label; +import javafx.stage.Popup; +import org.fxmisc.richtext.CodeArea; +import org.fxmisc.richtext.event.MouseOverTextEvent; +import org.fxmisc.richtext.model.TwoDimensional; + +import java.time.Duration; + +import static com.sparrowwallet.drongo.protocol.ScriptType.*; +import static com.sparrowwallet.drongo.protocol.ScriptType.P2WSH; +import static org.fxmisc.richtext.model.TwoDimensional.Bias.Backward; + +public abstract class BaseController { + protected void appendScript(CodeArea codeArea, Script script) { + appendScript(codeArea, script, null, null); + } + + protected void appendScript(CodeArea codeArea, Script script, Script redeemScript, Script witnessScript) { + if(P2PKH.isScriptType(script)) { + codeArea.append(script.getChunks().get(0).toString(), "script-opcode"); + codeArea.append(" ", ""); + codeArea.append(script.getChunks().get(1).toString(), "script-opcode"); + codeArea.append(" ", ""); + codeArea.append("", "script-hash"); + codeArea.append(" ", ""); + codeArea.append(script.getChunks().get(3).toString(), "script-opcode"); + codeArea.append(" ", ""); + codeArea.append(script.getChunks().get(4).toString(), "script-opcode"); + } else if(P2SH.isScriptType(script)) { + codeArea.append(script.getChunks().get(0).toString(), "script-opcode"); + codeArea.append(" ", ""); + codeArea.append("", "script-hash"); + codeArea.append(" ", ""); + codeArea.append(script.getChunks().get(2).toString(), "script-opcode"); + } else if(P2WPKH.isScriptType(script)) { + codeArea.append(script.getChunks().get(0).toString(), "script-opcode"); + codeArea.append(" ", ""); + codeArea.append("", "script-hash"); + } else if(P2WSH.isScriptType(script)) { + codeArea.append(script.getChunks().get(0).toString(), "script-opcode"); + codeArea.append(" ", ""); + codeArea.append("", "script-hash"); + } else { + int signatureCount = 1; + int pubKeyCount = 1; + for (int i = 0; i < script.getChunks().size(); i++) { + ScriptChunk chunk = script.getChunks().get(i); + if(chunk.isOpCode()) { + codeArea.append(chunk.toString(), "script-opcode"); + } else if(chunk.isSignature()) { + codeArea.append("", "script-signature"); + } else if(chunk.isPubKey()) { + codeArea.append("", "script-pubkey"); + } else if(chunk.isScript()) { + Script nestedScript = chunk.getScript(); + if (nestedScript.equals(redeemScript)) { + codeArea.append("", "script-redeem"); + } else if (nestedScript.equals(witnessScript)) { + codeArea.append("", "script-redeem"); + } else { + codeArea.append("(", "script-nest"); + appendScript(codeArea, nestedScript); + codeArea.append(")", "script-nest"); + } + } else { + codeArea.append(chunk.toString(), "script-other"); + } + + if(i < script.getChunks().size() - 1) { + codeArea.append(" ", ""); + } + } + } + + addScriptPopup(codeArea, script); + } + + protected void addScriptPopup(CodeArea area, Script script) { + ScriptContextMenu contextMenu = new ScriptContextMenu(area, script); + area.setContextMenu(contextMenu); + + Popup popup = new Popup(); + Label popupMsg = new Label(); + popupMsg.getStyleClass().add("tooltip"); + popup.getContent().add(popupMsg); + + area.setMouseOverTextDelay(Duration.ofMillis(150)); + area.addEventHandler(MouseOverTextEvent.MOUSE_OVER_TEXT_BEGIN, e -> { + TwoDimensional.Position position = area.getParagraph(0).getStyleSpans().offsetToPosition(e.getCharacterIndex(), Backward); + if(position.getMajor() % 2 == 0) { + ScriptChunk hoverChunk = script.getChunks().get(position.getMajor()/2); + if(!hoverChunk.isOpCode()) { + Point2D pos = e.getScreenPosition(); + popupMsg.setText(describeScriptChunk(hoverChunk)); + popup.show(area, pos.getX(), pos.getY() + 10); + } + } + }); + area.addEventHandler(MouseOverTextEvent.MOUSE_OVER_TEXT_END, e -> { + popup.hide(); + }); + } + + protected String describeScriptChunk(ScriptChunk chunk) { + return chunk.toString(); + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/control/AddressTreeTable.java b/src/main/java/com/sparrowwallet/sparrow/control/AddressTreeTable.java index cff10e21..c6c907c1 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/AddressTreeTable.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/AddressTreeTable.java @@ -3,12 +3,11 @@ package com.sparrowwallet.sparrow.control; import com.sparrowwallet.drongo.Utils; import com.sparrowwallet.drongo.address.Address; import com.sparrowwallet.drongo.protocol.Transaction; -import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.event.ReceiveActionEvent; +import com.sparrowwallet.sparrow.wallet.Entry; +import com.sparrowwallet.sparrow.wallet.NodeEntry; import javafx.beans.property.ReadOnlyObjectWrapper; -import javafx.beans.property.SimpleStringProperty; -import javafx.beans.property.StringProperty; import javafx.event.Event; import javafx.geometry.Pos; import javafx.scene.control.*; @@ -24,26 +23,24 @@ import org.controlsfx.glyphfont.Glyph; import java.lang.reflect.Field; import java.util.Locale; -public class AddressTreeTable extends TreeTableView { - public void initialize(Wallet.Node rootNode) { +public class AddressTreeTable extends TreeTableView { + public void initialize(NodeEntry rootEntry) { getStyleClass().add("address-treetable"); String address = null; - Data rootData = new Data(rootNode); - TreeItem rootItem = new TreeItem<>(rootData); - for(Wallet.Node childNode : rootNode.getChildren()) { - Data childData = new Data(childNode); - TreeItem childItem = new TreeItem<>(childData); + TreeItem rootItem = new TreeItem<>(rootEntry); + for(Entry childEntry : rootEntry.getChildren()) { + TreeItem childItem = new TreeItem<>(childEntry); rootItem.getChildren().add(childItem); - address = childNode.getAddress().toString(); + address = rootEntry.getNode().getAddress().toString(); } rootItem.setExpanded(true); setRoot(rootItem); setShowRoot(false); - TreeTableColumn addressCol = new TreeTableColumn<>("Address / Outpoints"); - addressCol.setCellValueFactory((TreeTableColumn.CellDataFeatures param) -> { + TreeTableColumn addressCol = new TreeTableColumn<>("Address / Outpoints"); + addressCol.setCellValueFactory((TreeTableColumn.CellDataFeatures param) -> { return new ReadOnlyObjectWrapper<>(param.getValue().getValue()); }); addressCol.setCellFactory(p -> new DataCell()); @@ -54,23 +51,26 @@ public class AddressTreeTable extends TreeTableView { addressCol.setMinWidth(TextUtils.computeTextWidth(Font.font("Courier"), address, 0.0)); } - TreeTableColumn labelCol = new TreeTableColumn<>("Label"); - labelCol.setCellValueFactory((TreeTableColumn.CellDataFeatures param) -> { - return param.getValue().getValue().getLabelProperty(); + TreeTableColumn labelCol = new TreeTableColumn<>("Label"); + labelCol.setCellValueFactory((TreeTableColumn.CellDataFeatures param) -> { + return param.getValue().getValue().labelProperty(); }); labelCol.setCellFactory(p -> new LabelCell()); labelCol.setSortable(false); getColumns().add(labelCol); - TreeTableColumn amountCol = new TreeTableColumn<>("Amount"); - amountCol.setCellValueFactory((TreeTableColumn.CellDataFeatures param) -> { + TreeTableColumn amountCol = new TreeTableColumn<>("Amount"); + amountCol.setCellValueFactory((TreeTableColumn.CellDataFeatures param) -> { return new ReadOnlyObjectWrapper<>(param.getValue().getValue().getAmount()); }); amountCol.setCellFactory(p -> new AmountCell()); amountCol.setSortable(false); getColumns().add(amountCol); - TreeTableColumn actionCol = new TreeTableColumn<>("Actions"); + TreeTableColumn actionCol = new TreeTableColumn<>("Actions"); + actionCol.setCellValueFactory((TreeTableColumn.CellDataFeatures param) -> { + return new ReadOnlyObjectWrapper<>(param.getValue().getValue()); + }); actionCol.setCellFactory(p -> new ActionCell()); actionCol.setSortable(false); getColumns().add(actionCol); @@ -79,53 +79,23 @@ public class AddressTreeTable extends TreeTableView { setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY); } - public static class Data { - private final Wallet.Node walletNode; - private final SimpleStringProperty labelProperty; - private final Long amount; - - public Data(Wallet.Node walletNode) { - this.walletNode = walletNode; - - labelProperty = new SimpleStringProperty(walletNode.getLabel()); - labelProperty.addListener((observable, oldValue, newValue) -> walletNode.setLabel(newValue)); - - amount = walletNode.getAmount(); - } - - public Wallet.Node getWalletNode() { - return walletNode; - } - - public String getLabel() { - return labelProperty.get(); - } - - public StringProperty getLabelProperty() { - return labelProperty; - } - - public Long getAmount() { - return amount; - } - } - - private static class DataCell extends TreeTableCell { + private static class DataCell extends TreeTableCell { public DataCell() { super(); - getStyleClass().add("data-cell"); + getStyleClass().add("address-cell"); } @Override - protected void updateItem(Data data, boolean empty) { - super.updateItem(data, empty); + protected void updateItem(Entry entry, boolean empty) { + super.updateItem(entry, empty); if(empty) { setText(null); setGraphic(null); } else { - if(data.getWalletNode() != null) { - Address address = data.getWalletNode().getAddress(); + if(entry instanceof NodeEntry) { + NodeEntry nodeEntry = (NodeEntry)entry; + Address address = nodeEntry.getNode().getAddress(); setText(address.toString()); setContextMenu(new AddressContextMenu(address)); } else { @@ -157,7 +127,7 @@ public class AddressTreeTable extends TreeTableView { } } - private static class LabelCell extends TextFieldTreeTableCell { + private static class LabelCell extends TextFieldTreeTableCell { public LabelCell() { super(new DefaultStringConverter()); getStyleClass().add("label-cell"); @@ -183,10 +153,10 @@ public class AddressTreeTable extends TreeTableView { // intercept the loss of focus. The default commitEdit(...) method // simply bails if we are not editing... if (!isEditing() && !label.equals(getItem())) { - TreeTableView treeTable = getTreeTableView(); + TreeTableView treeTable = getTreeTableView(); if(treeTable != null) { - TreeTableColumn column = getTableColumn(); - TreeTableColumn.CellEditEvent event = new TreeTableColumn.CellEditEvent<>( + TreeTableColumn column = getTableColumn(); + TreeTableColumn.CellEditEvent event = new TreeTableColumn.CellEditEvent<>( treeTable, new TreeTablePosition<>(treeTable, getIndex(), column), TreeTableColumn.editCommitEvent(), label ); @@ -231,7 +201,7 @@ public class AddressTreeTable extends TreeTableView { } } - private static class AmountCell extends TreeTableCell { + private static class AmountCell extends TreeTableCell { public AmountCell() { super(); getStyleClass().add("amount-cell"); @@ -260,8 +230,9 @@ public class AddressTreeTable extends TreeTableView { } } - private static class ActionCell extends TreeTableCell { + private static class ActionCell extends TreeTableCell { private final HBox actionBox; + private final Button receiveButton; public ActionCell() { super(); @@ -271,23 +242,27 @@ public class AddressTreeTable extends TreeTableView { actionBox.setSpacing(8); actionBox.setAlignment(Pos.CENTER); - Button receiveButton = new Button(""); + receiveButton = new Button(""); Glyph receiveGlyph = new Glyph("FontAwesome", FontAwesome.Glyph.ARROW_DOWN); receiveGlyph.setFontSize(12); receiveButton.setGraphic(receiveGlyph); receiveButton.setOnAction(event -> { - EventManager.get().post(new ReceiveActionEvent(getTreeTableView().getTreeItem(getIndex()).getValue().getWalletNode())); + NodeEntry nodeEntry = (NodeEntry)getTreeTableView().getTreeItem(getIndex()).getValue(); + EventManager.get().post(new ReceiveActionEvent(nodeEntry)); }); - - actionBox.getChildren().add(receiveButton); } @Override - protected void updateItem(Void item, boolean empty) { - super.updateItem(item, empty); + protected void updateItem(Entry entry, boolean empty) { + super.updateItem(entry, empty); if (empty) { setGraphic(null); } else { + actionBox.getChildren().remove(0, actionBox.getChildren().size()); + if(entry instanceof NodeEntry) { + actionBox.getChildren().add(receiveButton); + } + setGraphic(actionBox); } } diff --git a/src/main/java/com/sparrowwallet/sparrow/control/CopyableTextField.java b/src/main/java/com/sparrowwallet/sparrow/control/CopyableTextField.java new file mode 100644 index 00000000..f516209e --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/control/CopyableTextField.java @@ -0,0 +1,64 @@ +package com.sparrowwallet.sparrow.control; + +import javafx.animation.FadeTransition; +import javafx.beans.InvalidationListener; +import javafx.beans.Observable; +import javafx.beans.property.ObjectProperty; +import javafx.scene.Cursor; +import javafx.scene.Node; +import javafx.scene.input.Clipboard; +import javafx.scene.input.ClipboardContent; +import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; +import javafx.util.Duration; +import org.controlsfx.control.textfield.CustomTextField; + +public class CopyableTextField extends CustomTextField { + private static final Duration FADE_DURATION = Duration.millis(350); + + public CopyableTextField() { + super(); + getStyleClass().add("copyable-text-field"); + setupCopyButtonField(super.rightProperty()); + } + + private void setupCopyButtonField(ObjectProperty rightProperty) { + Region copyButton = new Region(); + copyButton.getStyleClass().addAll("graphic"); //$NON-NLS-1$ + StackPane copyButtonPane = new StackPane(copyButton); + copyButtonPane.getStyleClass().addAll("copy-button"); //$NON-NLS-1$ + copyButtonPane.setOpacity(0.0); + copyButtonPane.setCursor(Cursor.DEFAULT); + copyButtonPane.setOnMouseReleased(e -> { + ClipboardContent content = new ClipboardContent(); + content.putString(getText()); + Clipboard.getSystemClipboard().setContent(content); + }); + + rightProperty.set(copyButtonPane); + + final FadeTransition fader = new FadeTransition(FADE_DURATION, copyButtonPane); + fader.setCycleCount(1); + + textProperty().addListener(new InvalidationListener() { + @Override + public void invalidated(Observable arg0) { + String text = getText(); + boolean isTextEmpty = text == null || text.isEmpty(); + boolean isButtonVisible = fader.getNode().getOpacity() > 0; + + if (isTextEmpty && isButtonVisible) { + setButtonVisible(false); + } else if (!isTextEmpty && !isButtonVisible) { + setButtonVisible(true); + } + } + + private void setButtonVisible( boolean visible ) { + fader.setFromValue(visible? 0.0: 1.0); + fader.setToValue(visible? 1.0: 0.0); + fader.play(); + } + }); + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/event/ReceiveActionEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/ReceiveActionEvent.java index 05c55edc..7a26c343 100644 --- a/src/main/java/com/sparrowwallet/sparrow/event/ReceiveActionEvent.java +++ b/src/main/java/com/sparrowwallet/sparrow/event/ReceiveActionEvent.java @@ -1,15 +1,15 @@ package com.sparrowwallet.sparrow.event; -import com.sparrowwallet.drongo.wallet.Wallet; +import com.sparrowwallet.sparrow.wallet.NodeEntry; public class ReceiveActionEvent { - private Wallet.Node receiveNode; + private NodeEntry receiveEntry; - public ReceiveActionEvent(Wallet.Node receiveNode) { - this.receiveNode = receiveNode; + public ReceiveActionEvent(NodeEntry receiveEntry) { + this.receiveEntry = receiveEntry; } - public Wallet.Node getReceiveNode() { - return receiveNode; + public NodeEntry getReceiveEntry() { + return receiveEntry; } } diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Hwi.java b/src/main/java/com/sparrowwallet/sparrow/io/Hwi.java index f3a8c75f..ee2d47c7 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/Hwi.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/Hwi.java @@ -7,7 +7,6 @@ import com.sparrowwallet.drongo.wallet.WalletModel; import javafx.concurrent.ScheduledService; import javafx.concurrent.Service; import javafx.concurrent.Task; -import org.apache.commons.compress.compressors.lz4.FramedLZ4CompressorInputStream; import org.controlsfx.tools.Platform; import java.io.*; @@ -20,6 +19,7 @@ import java.nio.file.attribute.PosixFilePermissions; import java.util.Arrays; import java.util.List; import java.util.Set; +import java.util.zip.GZIPInputStream; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; @@ -130,7 +130,7 @@ public class Hwi { File tempExec = tempExecPath.toFile(); //tempExec.deleteOnExit(); OutputStream tempExecStream = new BufferedOutputStream(new FileOutputStream(tempExec)); - ByteStreams.copy(new FramedLZ4CompressorInputStream(inputStream), tempExecStream); + ByteStreams.copy(new GZIPInputStream(inputStream), tempExecStream); inputStream.close(); tempExecStream.flush(); tempExecStream.close(); diff --git a/src/main/java/com/sparrowwallet/sparrow/transaction/TransactionFormController.java b/src/main/java/com/sparrowwallet/sparrow/transaction/TransactionFormController.java index d15ab13b..4621a19b 100644 --- a/src/main/java/com/sparrowwallet/sparrow/transaction/TransactionFormController.java +++ b/src/main/java/com/sparrowwallet/sparrow/transaction/TransactionFormController.java @@ -1,25 +1,17 @@ package com.sparrowwallet.sparrow.transaction; import com.sparrowwallet.drongo.address.Address; -import com.sparrowwallet.drongo.protocol.*; +import com.sparrowwallet.drongo.protocol.NonStandardScriptException; +import com.sparrowwallet.drongo.protocol.TransactionOutput; +import com.sparrowwallet.sparrow.BaseController; import javafx.collections.FXCollections; import javafx.collections.ObservableList; -import javafx.geometry.Point2D; import javafx.scene.chart.PieChart; -import javafx.scene.control.Label; import javafx.scene.control.Tooltip; -import javafx.stage.Popup; -import org.fxmisc.richtext.CodeArea; -import org.fxmisc.richtext.event.MouseOverTextEvent; -import org.fxmisc.richtext.model.TwoDimensional; -import java.time.Duration; import java.util.List; -import static org.fxmisc.richtext.model.TwoDimensional.Bias.Backward; -import static com.sparrowwallet.drongo.protocol.ScriptType.*; - -public abstract class TransactionFormController { +public abstract class TransactionFormController extends BaseController { protected void addPieData(PieChart pie, List outputs) { ObservableList outputsPieData = FXCollections.observableArrayList(); @@ -52,98 +44,4 @@ public abstract class TransactionFormController { data.pieValueProperty().addListener((observable, oldValue, newValue) -> tooltip.setText(newValue + "%")); }); } - - protected void appendScript(CodeArea codeArea, Script script) { - appendScript(codeArea, script, null, null); - } - - protected void appendScript(CodeArea codeArea, Script script, Script redeemScript, Script witnessScript) { - if(P2PKH.isScriptType(script)) { - codeArea.append(script.getChunks().get(0).toString(), "script-opcode"); - codeArea.append(" ", ""); - codeArea.append(script.getChunks().get(1).toString(), "script-opcode"); - codeArea.append(" ", ""); - codeArea.append("", "script-hash"); - codeArea.append(" ", ""); - codeArea.append(script.getChunks().get(3).toString(), "script-opcode"); - codeArea.append(" ", ""); - codeArea.append(script.getChunks().get(4).toString(), "script-opcode"); - } else if(P2SH.isScriptType(script)) { - codeArea.append(script.getChunks().get(0).toString(), "script-opcode"); - codeArea.append(" ", ""); - codeArea.append("", "script-hash"); - codeArea.append(" ", ""); - codeArea.append(script.getChunks().get(2).toString(), "script-opcode"); - } else if(P2WPKH.isScriptType(script)) { - codeArea.append(script.getChunks().get(0).toString(), "script-opcode"); - codeArea.append(" ", ""); - codeArea.append("", "script-hash"); - } else if(P2WSH.isScriptType(script)) { - codeArea.append(script.getChunks().get(0).toString(), "script-opcode"); - codeArea.append(" ", ""); - codeArea.append("", "script-hash"); - } else { - int signatureCount = 1; - int pubKeyCount = 1; - for (int i = 0; i < script.getChunks().size(); i++) { - ScriptChunk chunk = script.getChunks().get(i); - if(chunk.isOpCode()) { - codeArea.append(chunk.toString(), "script-opcode"); - } else if(chunk.isSignature()) { - codeArea.append("", "script-signature"); - } else if(chunk.isPubKey()) { - codeArea.append("", "script-pubkey"); - } else if(chunk.isScript()) { - Script nestedScript = chunk.getScript(); - if (nestedScript.equals(redeemScript)) { - codeArea.append("", "script-redeem"); - } else if (nestedScript.equals(witnessScript)) { - codeArea.append("", "script-redeem"); - } else { - codeArea.append("(", "script-nest"); - appendScript(codeArea, nestedScript); - codeArea.append(")", "script-nest"); - } - } else { - codeArea.append(chunk.toString(), "script-other"); - } - - if(i < script.getChunks().size() - 1) { - codeArea.append(" ", ""); - } - } - } - - addScriptPopup(codeArea, script); - } - - protected void addScriptPopup(CodeArea area, Script script) { - ScriptContextMenu contextMenu = new ScriptContextMenu(area, script); - area.setContextMenu(contextMenu); - - Popup popup = new Popup(); - Label popupMsg = new Label(); - popupMsg.getStyleClass().add("tooltip"); - popup.getContent().add(popupMsg); - - area.setMouseOverTextDelay(Duration.ofMillis(150)); - area.addEventHandler(MouseOverTextEvent.MOUSE_OVER_TEXT_BEGIN, e -> { - TwoDimensional.Position position = area.getParagraph(0).getStyleSpans().offsetToPosition(e.getCharacterIndex(), Backward); - if(position.getMajor() % 2 == 0) { - ScriptChunk hoverChunk = script.getChunks().get(position.getMajor()/2); - if(!hoverChunk.isOpCode()) { - Point2D pos = e.getScreenPosition(); - popupMsg.setText(describeScriptChunk(hoverChunk)); - popup.show(area, pos.getX(), pos.getY() + 10); - } - } - }); - area.addEventHandler(MouseOverTextEvent.MOUSE_OVER_TEXT_END, e -> { - popup.hide(); - }); - } - - protected String describeScriptChunk(ScriptChunk chunk) { - return chunk.toString(); - } } diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/AddressesController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/AddressesController.java index 136a63e5..1e535ebb 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/AddressesController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/AddressesController.java @@ -26,7 +26,7 @@ public class AddressesController extends WalletFormController implements Initial public void initializeView() { Wallet wallet = walletForm.getWallet(); - receiveTable.initialize(wallet.getNodes(KeyPurpose.RECEIVE)); - changeTable.initialize(wallet.getNodes(KeyPurpose.CHANGE)); + receiveTable.initialize(getWalletForm().getNodeEntry(KeyPurpose.RECEIVE)); + changeTable.initialize(getWalletForm().getNodeEntry(KeyPurpose.CHANGE)); } } diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/Entry.java b/src/main/java/com/sparrowwallet/sparrow/wallet/Entry.java new file mode 100644 index 00000000..3ad3d426 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/Entry.java @@ -0,0 +1,33 @@ +package com.sparrowwallet.sparrow.wallet; + +import javafx.beans.property.SimpleStringProperty; + +import java.util.ArrayList; +import java.util.List; + +public abstract class Entry { + private final SimpleStringProperty labelProperty; + private final List children = new ArrayList<>(); + + public Entry(String label) { + this.labelProperty = new SimpleStringProperty(label); + } + + public Entry(SimpleStringProperty labelProperty) { + this.labelProperty = labelProperty; + } + + public String getLabel() { + return labelProperty.get(); + } + + public SimpleStringProperty labelProperty() { + return labelProperty; + } + + public List getChildren() { + return children; + } + + public abstract Long getAmount(); +} diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/NodeEntry.java b/src/main/java/com/sparrowwallet/sparrow/wallet/NodeEntry.java new file mode 100644 index 00000000..7a62925e --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/NodeEntry.java @@ -0,0 +1,25 @@ +package com.sparrowwallet.sparrow.wallet; + +import com.sparrowwallet.drongo.wallet.Wallet; + +public class NodeEntry extends Entry { + private final Wallet.Node node; + + public NodeEntry(Wallet.Node node) { + super(node.getLabel()); + this.node = node; + + labelProperty().addListener((observable, oldValue, newValue) -> node.setLabel(newValue)); + } + + @Override + public Long getAmount() { + //TODO: Iterate through TransactionEntries to calculate amount + + return null; + } + + public Wallet.Node getNode() { + return node; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/ReceiveController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/ReceiveController.java new file mode 100644 index 00000000..7cf36015 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/ReceiveController.java @@ -0,0 +1,104 @@ +package com.sparrowwallet.sparrow.wallet; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.client.j2se.MatrixToImageConfig; +import com.google.zxing.client.j2se.MatrixToImageWriter; +import com.google.zxing.common.BitMatrix; +import com.google.zxing.qrcode.QRCodeWriter; +import com.sparrowwallet.drongo.KeyPurpose; +import com.sparrowwallet.sparrow.EventManager; +import com.sparrowwallet.sparrow.control.CopyableLabel; +import com.sparrowwallet.sparrow.control.CopyableTextField; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.fxml.Initializable; +import javafx.scene.control.TextField; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import org.fxmisc.richtext.CodeArea; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.net.URL; +import java.util.ResourceBundle; + +public class ReceiveController extends WalletFormController implements Initializable { + @FXML + private CopyableTextField address; + + @FXML + private TextField label; + + @FXML + private CopyableLabel derivationPath; + + @FXML + private CopyableLabel lastUsed; + + @FXML + private ImageView qrCode; + + @FXML + private CodeArea scriptPubKeyArea; + + private NodeEntry currentEntry; + + @Override + public void initialize(URL location, ResourceBundle resources) { + EventManager.get().register(this); + } + + @Override + public void initializeView() { + + } + + public void setNodeEntry(NodeEntry nodeEntry) { + if(currentEntry != null) { + label.textProperty().unbindBidirectional(currentEntry.labelProperty()); + } + + this.currentEntry = nodeEntry; + + address.setText(nodeEntry.getNode().getAddress().toString()); + + label.textProperty().bindBidirectional(nodeEntry.labelProperty()); + + derivationPath.setText(nodeEntry.getNode().getDerivationPath()); + + //TODO: Find last used block height if available (red flag?) + lastUsed.setText("Unknown"); + + Image qrImage = getQrCode(nodeEntry.getNode().getAddress().toString()); + if(qrImage != null) { + qrCode.setImage(qrImage); + } + + scriptPubKeyArea.clear(); + appendScript(scriptPubKeyArea, nodeEntry.getNode().getOutputScript(), null, null); + } + + private Image getQrCode(String address) { + try { + QRCodeWriter qrCodeWriter = new QRCodeWriter(); + BitMatrix qrMatrix = qrCodeWriter.encode(address, BarcodeFormat.QR_CODE, 150, 150); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + MatrixToImageWriter.writeToStream(qrMatrix, "PNG", baos, new MatrixToImageConfig()); + + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + return new Image(bais); + } catch(Exception e) { + e.printStackTrace(); + } + + return null; + } + + public void getNewAddress(ActionEvent event) { + NodeEntry freshEntry = getWalletForm().getFreshNodeEntry(KeyPurpose.RECEIVE, currentEntry); + setNodeEntry(freshEntry); + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletForm.java b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletForm.java index 30d1bb75..eb9b2546 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletForm.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletForm.java @@ -1,18 +1,22 @@ package com.sparrowwallet.sparrow.wallet; -import com.sparrowwallet.drongo.crypto.ECKey; -import com.sparrowwallet.drongo.crypto.Pbkdf2KeyDeriver; +import com.sparrowwallet.drongo.KeyPurpose; import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.sparrow.io.Storage; import java.io.File; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; public class WalletForm { private final Storage storage; private Wallet oldWallet; private Wallet wallet; + private final List accountEntries = new ArrayList<>(); + public WalletForm(Storage storage, Wallet currentWallet) { this.storage = storage; this.oldWallet = currentWallet; @@ -39,4 +43,39 @@ public class WalletForm { storage.storeWallet(wallet); oldWallet = wallet.copy(); } + + public NodeEntry getNodeEntry(KeyPurpose keyPurpose) { + NodeEntry purposeEntry; + Optional optionalPurposeEntry = accountEntries.stream().filter(entry -> entry.getNode().getKeyPurpose().equals(keyPurpose)).findFirst(); + if(optionalPurposeEntry.isPresent()) { + purposeEntry = optionalPurposeEntry.get(); + } else { + Wallet.Node purposeNode = getWallet().getNode(keyPurpose); + purposeEntry = new NodeEntry(purposeNode); + for(Wallet.Node childNode : purposeNode.getChildren()) { + NodeEntry childEntry = new NodeEntry(childNode); + purposeEntry.getChildren().add(childEntry); + } + + accountEntries.add(purposeEntry); + } + + return purposeEntry; + } + + public NodeEntry getFreshNodeEntry(KeyPurpose keyPurpose, NodeEntry currentEntry) { + NodeEntry rootEntry = getNodeEntry(keyPurpose); + Wallet.Node freshNode = getWallet().getFreshNode(keyPurpose, currentEntry == null ? null : currentEntry.getNode()); + + for(Entry childEntry : rootEntry.getChildren()) { + NodeEntry nodeEntry = (NodeEntry)childEntry; + if(nodeEntry.getNode().equals(freshNode)) { + return nodeEntry; + } + } + + NodeEntry freshEntry = new NodeEntry(freshNode); + rootEntry.getChildren().add(freshEntry); + return freshEntry; + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletFormController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletFormController.java index 845b531e..2552ba9a 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletFormController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletFormController.java @@ -1,6 +1,8 @@ package com.sparrowwallet.sparrow.wallet; -public abstract class WalletFormController { +import com.sparrowwallet.sparrow.BaseController; + +public abstract class WalletFormController extends BaseController { public WalletForm walletForm; public WalletForm getWalletForm() { diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 2d4ac7d7..6ab058bd 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -10,6 +10,7 @@ open module com.sparrowwallet.sparrow { requires com.google.common; requires flowless; requires com.google.gson; - requires org.apache.commons.compress; + requires com.google.zxing; + requires com.google.zxing.javase; requires javafx.swing; } \ No newline at end of file diff --git a/src/main/resources/com/sparrowwallet/sparrow/general.css b/src/main/resources/com/sparrowwallet/sparrow/general.css index e8aaf2d4..8a01cfed 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/general.css +++ b/src/main/resources/com/sparrowwallet/sparrow/general.css @@ -98,3 +98,22 @@ .titled-description-pane .hyperlink:hover:visited { -fx-underline: true; } + +.copyable-text-field .copy-button { + -fx-padding: 0 3 0 0; +} + +.copyable-text-field .copy-button > .graphic { + -fx-background-color: #949494; + -fx-scale-shape: false; + -fx-padding: 4.5 4.5 4.5 4.5; /* Graphic is 9x9 px */ + -fx-shape: "M 6.429688 7.875 L 6.429688 8.578125 C 6.429688 8.8125 6.210938 9 5.945312 9 L 0.480469 9 C 0.214844 9 0 8.8125 0 8.578125 L 0 2.109375 C 0 1.875 0.214844 1.6875 0.480469 1.6875 L 1.929688 1.6875 L 1.929688 6.890625 C 1.929688 7.433594 2.433594 7.875 3.054688 7.875 Z M 6.429688 1.828125 L 6.429688 0 L 3.054688 0 C 2.789062 0 2.570312 0.1875 2.570312 0.421875 L 2.570312 6.890625 C 2.570312 7.125 2.789062 7.3125 3.054688 7.3125 L 8.519531 7.3125 C 8.785156 7.3125 9 7.125 9 6.890625 L 9 2.25 L 6.910156 2.25 C 6.644531 2.25 6.429688 2.058594 6.429688 1.828125 Z M 8.859375 1.28125 L 7.535156 0.125 C 7.445312 0.0429688 7.320312 0 7.191406 0 L 7.070312 0 L 7.070312 1.6875 L 9 1.6875 L 9 1.582031 C 9 1.46875 8.949219 1.363281 8.859375 1.28125 Z M 8.859375 1.28125 "; +} + +.copyable-text-field .copy-button:hover > .graphic { + -fx-background-color: #0184bc; +} + +.copyable-text-field .copy-button:pressed > .graphic { + -fx-background-color: #116a8d; +} \ No newline at end of file diff --git a/src/main/resources/com/sparrowwallet/sparrow/transaction/script.css b/src/main/resources/com/sparrowwallet/sparrow/script.css similarity index 100% rename from src/main/resources/com/sparrowwallet/sparrow/transaction/script.css rename to src/main/resources/com/sparrowwallet/sparrow/script.css diff --git a/src/main/resources/com/sparrowwallet/sparrow/transaction/input.fxml b/src/main/resources/com/sparrowwallet/sparrow/transaction/input.fxml index dae09ec8..0bfc976c 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/transaction/input.fxml +++ b/src/main/resources/com/sparrowwallet/sparrow/transaction/input.fxml @@ -17,7 +17,7 @@ - + diff --git a/src/main/resources/com/sparrowwallet/sparrow/transaction/output.fxml b/src/main/resources/com/sparrowwallet/sparrow/transaction/output.fxml index e668e41c..bbd00891 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/transaction/output.fxml +++ b/src/main/resources/com/sparrowwallet/sparrow/transaction/output.fxml @@ -15,7 +15,7 @@ - + diff --git a/src/main/resources/com/sparrowwallet/sparrow/wallet/addresses.css b/src/main/resources/com/sparrowwallet/sparrow/wallet/addresses.css index 8112456a..51509146 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/wallet/addresses.css +++ b/src/main/resources/com/sparrowwallet/sparrow/wallet/addresses.css @@ -4,7 +4,7 @@ -fx-padding: 10 0 10 0; } -.data-cell { +.address-cell { -fx-font-family: Courier; } diff --git a/src/main/resources/com/sparrowwallet/sparrow/wallet/receive.css b/src/main/resources/com/sparrowwallet/sparrow/wallet/receive.css new file mode 100644 index 00000000..7c845271 --- /dev/null +++ b/src/main/resources/com/sparrowwallet/sparrow/wallet/receive.css @@ -0,0 +1,8 @@ +.address-text-field { + -fx-font-family: Courier; +} + +.qr-code { + -fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.8), 10, 0, 0, 0); + -fx-padding: 20; +} \ No newline at end of file diff --git a/src/main/resources/com/sparrowwallet/sparrow/wallet/receive.fxml b/src/main/resources/com/sparrowwallet/sparrow/wallet/receive.fxml new file mode 100644 index 00000000..35e78e0b --- /dev/null +++ b/src/main/resources/com/sparrowwallet/sparrow/wallet/receive.fxml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + +
+
+ + + + + + + + + + + + +
+
+ + + + + + + +
+
+ + + + + + + +
+
+ +
+
+ + + + + + + + +
\ No newline at end of file