diff --git a/build.gradle b/build.gradle index 1bc96e78..f67b98f8 100644 --- a/build.gradle +++ b/build.gradle @@ -48,7 +48,7 @@ dependencies { mainClassName = 'com.sparrowwallet.sparrow/com.sparrowwallet.sparrow.MainApp' run { - applicationDefaultJvmArgs = ["-Xdock:name=Sparrow", "-Xdock:icon=/Users/scy/git/sparrow/src/main/resources/sparrow.png", "--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls", "--add-opens=javafx.graphics/javafx.scene=org.controlsfx.controls", "--add-opens=javafx.controls/com.sun.javafx.scene.control.behavior=org.controlsfx.controls", "--add-opens=javafx.controls/com.sun.javafx.scene.control.inputmap=org.controlsfx.controls", "--add-opens=javafx.graphics/com.sun.javafx.scene.traversal=org.controlsfx.controls", "--add-opens=javafx.base/com.sun.javafx.event=org.controlsfx.controls"] + applicationDefaultJvmArgs = ["-Xdock:name=Sparrow", "-Xdock:icon=/Users/scy/git/sparrow/src/main/resources/sparrow.png", "--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls", "--add-opens=javafx.graphics/javafx.scene=org.controlsfx.controls", "--add-opens=javafx.controls/com.sun.javafx.scene.control.behavior=org.controlsfx.controls", "--add-opens=javafx.controls/com.sun.javafx.scene.control.inputmap=org.controlsfx.controls", "--add-opens=javafx.graphics/com.sun.javafx.scene.traversal=org.controlsfx.controls", "--add-opens=javafx.base/com.sun.javafx.event=org.controlsfx.controls", "--add-opens=javafx.controls/javafx.scene.control.cell=com.sparrowwallet.sparrow"] } jlink { @@ -63,7 +63,7 @@ jlink { options = ['--strip-debug', '--compress', '2', '--no-header-files', '--no-man-pages', '--ignore-signing-information', '--exclude-files', '**.png'] launcher { name = 'sparrow' - jvmArgs = ["--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls", "--add-opens=javafx.graphics/javafx.scene=org.controlsfx.controls", "--add-opens=javafx.controls/com.sun.javafx.scene.control.behavior=org.controlsfx.controls", "--add-opens=javafx.controls/com.sun.javafx.scene.control.inputmap=org.controlsfx.controls", "--add-opens=javafx.graphics/com.sun.javafx.scene.traversal=org.controlsfx.controls", "--add-opens=javafx.base/com.sun.javafx.event=org.controlsfx.controls"] + jvmArgs = ["--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls", "--add-opens=javafx.graphics/javafx.scene=org.controlsfx.controls", "--add-opens=javafx.controls/com.sun.javafx.scene.control.behavior=org.controlsfx.controls", "--add-opens=javafx.controls/com.sun.javafx.scene.control.inputmap=org.controlsfx.controls", "--add-opens=javafx.graphics/com.sun.javafx.scene.traversal=org.controlsfx.controls", "--add-opens=javafx.base/com.sun.javafx.event=org.controlsfx.controls", "--add-opens=javafx.controls/javafx.scene.control.cell=com.sparrowwallet.sparrow"] } addExtraDependencies("javafx") jpackage { diff --git a/drongo b/drongo index de70f445..eabcf4e8 160000 --- a/drongo +++ b/drongo @@ -1 +1 @@ -Subproject commit de70f44535916f93d18f66190a217d93f94e1240 +Subproject commit eabcf4e8f48ae18ff8d21436a2ab5e5153719944 diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java index 02b400aa..f89753ab 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppController.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppController.java @@ -252,12 +252,13 @@ public class AppController implements Initializable { FileType fileType = IOUtils.getFileType(file); if(FileType.JSON.equals(fileType)) { Wallet wallet = storage.loadWallet(); + restorePublicKeysFromSeed(wallet, null); Tab tab = addWalletTab(storage, wallet); tabs.getSelectionModel().select(tab); } else if(FileType.BINARY.equals(fileType)) { WalletPasswordDialog dlg = new WalletPasswordDialog(WalletPasswordDialog.PasswordRequirement.LOAD); Optional optionalPassword = dlg.showAndWait(); - if(!optionalPassword.isPresent()) { + if(optionalPassword.isEmpty()) { return; } diff --git a/src/main/java/com/sparrowwallet/sparrow/control/AddressTreeTable.java b/src/main/java/com/sparrowwallet/sparrow/control/AddressTreeTable.java new file mode 100644 index 00000000..cff10e21 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/control/AddressTreeTable.java @@ -0,0 +1,295 @@ +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 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.*; +import javafx.scene.control.cell.TextFieldTreeTableCell; +import javafx.scene.input.Clipboard; +import javafx.scene.input.ClipboardContent; +import javafx.scene.layout.HBox; +import javafx.scene.text.Font; +import javafx.util.converter.DefaultStringConverter; +import org.controlsfx.glyphfont.FontAwesome; +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) { + 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); + rootItem.getChildren().add(childItem); + address = childNode.getAddress().toString(); + } + + rootItem.setExpanded(true); + setRoot(rootItem); + setShowRoot(false); + + TreeTableColumn addressCol = new TreeTableColumn<>("Address / Outpoints"); + addressCol.setCellValueFactory((TreeTableColumn.CellDataFeatures param) -> { + return new ReadOnlyObjectWrapper<>(param.getValue().getValue()); + }); + addressCol.setCellFactory(p -> new DataCell()); + addressCol.setSortable(false); + getColumns().add(addressCol); + + if(address != null) { + 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(); + }); + labelCol.setCellFactory(p -> new LabelCell()); + labelCol.setSortable(false); + getColumns().add(labelCol); + + 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"); + actionCol.setCellFactory(p -> new ActionCell()); + actionCol.setSortable(false); + getColumns().add(actionCol); + + setEditable(true); + 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 { + public DataCell() { + super(); + getStyleClass().add("data-cell"); + } + + @Override + protected void updateItem(Data data, boolean empty) { + super.updateItem(data, empty); + + if(empty) { + setText(null); + setGraphic(null); + } else { + if(data.getWalletNode() != null) { + Address address = data.getWalletNode().getAddress(); + setText(address.toString()); + setContextMenu(new AddressContextMenu(address)); + } else { + //TODO: Add transaction outpoint + } + } + } + } + + private static class AddressContextMenu extends ContextMenu { + public AddressContextMenu(Address address) { + MenuItem copyAddress = new MenuItem("Copy Address"); + copyAddress.setOnAction(AE -> { + hide(); + ClipboardContent content = new ClipboardContent(); + content.putString(address.toString()); + Clipboard.getSystemClipboard().setContent(content); + }); + + MenuItem copyHex = new MenuItem("Copy Script Output Bytes"); + copyHex.setOnAction(AE -> { + hide(); + ClipboardContent content = new ClipboardContent(); + content.putString(Utils.bytesToHex(address.getOutputScriptData())); + Clipboard.getSystemClipboard().setContent(content); + }); + + getItems().addAll(copyAddress, copyHex); + } + } + + private static class LabelCell extends TextFieldTreeTableCell { + public LabelCell() { + super(new DefaultStringConverter()); + getStyleClass().add("label-cell"); + } + + @Override + public void updateItem(String label, boolean empty) { + super.updateItem(label, empty); + + if(empty) { + setText(null); + setGraphic(null); + } else { + setText(label); + setContextMenu(new LabelContextMenu(label)); + } + } + + @Override + public void commitEdit(String label) { + // This block is necessary to support commit on losing focus, because + // the baked-in mechanism sets our editing state to false before we can + // intercept the loss of focus. The default commitEdit(...) method + // simply bails if we are not editing... + if (!isEditing() && !label.equals(getItem())) { + TreeTableView treeTable = getTreeTableView(); + if(treeTable != null) { + TreeTableColumn column = getTableColumn(); + TreeTableColumn.CellEditEvent event = new TreeTableColumn.CellEditEvent<>( + treeTable, new TreeTablePosition<>(treeTable, getIndex(), column), + TreeTableColumn.editCommitEvent(), label + ); + Event.fireEvent(column, event); + } + } + + super.commitEdit(label); + } + + @Override + public void startEdit() { + super.startEdit(); + + try { + Field f = getClass().getSuperclass().getDeclaredField("textField"); + f.setAccessible(true); + TextField textField = (TextField)f.get(this); + textField.focusedProperty().addListener((obs, wasFocused, isNowFocused) -> { + if (!isNowFocused) { + commitEdit(getConverter().fromString(textField.getText())); + setText(getConverter().fromString(textField.getText())); + } + }); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + private static class LabelContextMenu extends ContextMenu { + public LabelContextMenu(String label) { + MenuItem copyLabel = new MenuItem("Copy Label"); + copyLabel.setOnAction(AE -> { + hide(); + ClipboardContent content = new ClipboardContent(); + content.putString(label); + Clipboard.getSystemClipboard().setContent(content); + }); + + getItems().add(copyLabel); + } + } + + private static class AmountCell extends TreeTableCell { + public AmountCell() { + super(); + getStyleClass().add("amount-cell"); + } + + @Override + protected void updateItem(Long amount, boolean empty) { + super.updateItem(amount, empty); + + if(empty || amount == null) { + setText(null); + setGraphic(null); + } else { + String satsValue = String.format(Locale.ENGLISH, "%,d", amount) + " sats"; + String btcValue = CoinLabel.BTC_FORMAT.format(amount.doubleValue() / Transaction.SATOSHIS_PER_BITCOIN) + " BTC"; + Tooltip tooltip = new Tooltip(); + if(amount > CoinLabel.MAX_SATS_SHOWN) { + tooltip.setText(satsValue); + setText(btcValue); + } else { + tooltip.setText(btcValue); + setText(satsValue); + } + setTooltip(tooltip); + } + } + } + + private static class ActionCell extends TreeTableCell { + private final HBox actionBox; + + public ActionCell() { + super(); + getStyleClass().add("action-cell"); + + actionBox = new HBox(); + actionBox.setSpacing(8); + actionBox.setAlignment(Pos.CENTER); + + 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())); + }); + + actionBox.getChildren().add(receiveButton); + } + + @Override + protected void updateItem(Void item, boolean empty) { + super.updateItem(item, empty); + if (empty) { + setGraphic(null); + } else { + setGraphic(actionBox); + } + } + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/control/CoinLabel.java b/src/main/java/com/sparrowwallet/sparrow/control/CoinLabel.java index ac70c9cb..0571a836 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/CoinLabel.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/CoinLabel.java @@ -16,7 +16,7 @@ import java.util.Locale; public class CoinLabel extends CopyableLabel { public static final int MAX_SATS_SHOWN = 1000000; - private DecimalFormat btcFormat = new DecimalFormat("0", DecimalFormatSymbols.getInstance(Locale.ENGLISH)); + public static final DecimalFormat BTC_FORMAT = new DecimalFormat("0", DecimalFormatSymbols.getInstance(Locale.ENGLISH)); private final LongProperty value = new SimpleLongProperty(); private Tooltip tooltip; @@ -28,7 +28,7 @@ public class CoinLabel extends CopyableLabel { public CoinLabel(String text) { super(text); - btcFormat.setMaximumFractionDigits(8); + BTC_FORMAT.setMaximumFractionDigits(8); valueProperty().addListener((observable, oldValue, newValue) -> setValueAsText((Long)newValue)); tooltip = new Tooltip(); contextMenu = new CoinContextMenu(); @@ -51,7 +51,7 @@ public class CoinLabel extends CopyableLabel { setContextMenu(contextMenu); String satsValue = String.format(Locale.ENGLISH, "%,d",value) + " sats"; - String btcValue = btcFormat.format(value.doubleValue() / Transaction.SATOSHIS_PER_BITCOIN) + " BTC"; + String btcValue = BTC_FORMAT.format(value.doubleValue() / Transaction.SATOSHIS_PER_BITCOIN) + " BTC"; if(value > MAX_SATS_SHOWN) { tooltip.setText(satsValue); setText(btcValue); @@ -78,7 +78,7 @@ public class CoinLabel extends CopyableLabel { copyBtcValue.setOnAction(AE -> { hide(); ClipboardContent content = new ClipboardContent(); - content.putString(btcFormat.format((double)getValue() / Transaction.SATOSHIS_PER_BITCOIN)); + content.putString(BTC_FORMAT.format((double)getValue() / Transaction.SATOSHIS_PER_BITCOIN)); Clipboard.getSystemClipboard().setContent(content); }); diff --git a/src/main/java/com/sparrowwallet/sparrow/control/MnemonicKeystoreImportPane.java b/src/main/java/com/sparrowwallet/sparrow/control/MnemonicKeystoreImportPane.java index b3f73bd3..5db2527a 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/MnemonicKeystoreImportPane.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/MnemonicKeystoreImportPane.java @@ -2,10 +2,7 @@ package com.sparrowwallet.sparrow.control; import com.sparrowwallet.drongo.KeyDerivation; import com.sparrowwallet.drongo.crypto.ChildNumber; -import com.sparrowwallet.drongo.wallet.Bip39MnemonicCode; -import com.sparrowwallet.drongo.wallet.DeterministicSeed; -import com.sparrowwallet.drongo.wallet.Keystore; -import com.sparrowwallet.drongo.wallet.Wallet; +import com.sparrowwallet.drongo.wallet.*; import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.event.KeystoreImportEvent; import com.sparrowwallet.sparrow.io.*; @@ -244,7 +241,9 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane { return true; } catch (ImportException e) { String errorMessage = e.getMessage(); - if(e.getCause() != null && e.getCause().getMessage() != null && !e.getCause().getMessage().isEmpty()) { + if(e.getCause() instanceof MnemonicException.MnemonicChecksumException) { + errorMessage = "Invalid word list - checksum incorrect"; + } else if(e.getCause() != null && e.getCause().getMessage() != null && !e.getCause().getMessage().isEmpty()) { errorMessage = e.getCause().getMessage(); } setError("Import Error", errorMessage); diff --git a/src/main/java/com/sparrowwallet/sparrow/event/ReceiveActionEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/ReceiveActionEvent.java new file mode 100644 index 00000000..05c55edc --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/event/ReceiveActionEvent.java @@ -0,0 +1,15 @@ +package com.sparrowwallet.sparrow.event; + +import com.sparrowwallet.drongo.wallet.Wallet; + +public class ReceiveActionEvent { + private Wallet.Node receiveNode; + + public ReceiveActionEvent(Wallet.Node receiveNode) { + this.receiveNode = receiveNode; + } + + public Wallet.Node getReceiveNode() { + return receiveNode; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/AddressesController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/AddressesController.java new file mode 100644 index 00000000..136a63e5 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/AddressesController.java @@ -0,0 +1,32 @@ +package com.sparrowwallet.sparrow.wallet; + +import com.sparrowwallet.drongo.KeyPurpose; +import com.sparrowwallet.drongo.wallet.Wallet; +import com.sparrowwallet.sparrow.EventManager; +import com.sparrowwallet.sparrow.control.AddressTreeTable; +import javafx.fxml.FXML; +import javafx.fxml.Initializable; + +import java.net.URL; +import java.util.ResourceBundle; + +public class AddressesController extends WalletFormController implements Initializable { + @FXML + private AddressTreeTable receiveTable; + + @FXML + private AddressTreeTable changeTable; + + @Override + public void initialize(URL location, ResourceBundle resources) { + EventManager.get().register(this); + } + + @Override + public void initializeView() { + Wallet wallet = walletForm.getWallet(); + + receiveTable.initialize(wallet.getNodes(KeyPurpose.RECEIVE)); + changeTable.initialize(wallet.getNodes(KeyPurpose.CHANGE)); + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletController.java index b874cd0b..a91d42f5 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletController.java @@ -47,9 +47,9 @@ public class WalletController extends WalletFormController implements Initializa for(Node walletFunction : walletPane.getChildren()) { if(walletFunction.getUserData().equals(function)) { existing = true; - walletFunction.setViewOrder(1); - } else { walletFunction.setViewOrder(0); + } else { + walletFunction.setViewOrder(1); } } @@ -57,6 +57,7 @@ public class WalletController extends WalletFormController implements Initializa if(!existing) { FXMLLoader functionLoader = new FXMLLoader(AppController.class.getResource("wallet/" + function.toString().toLowerCase() + ".fxml")); Node walletFunction = functionLoader.load(); + walletFunction.setUserData(function); WalletFormController controller = functionLoader.getController(); controller.setWalletForm(getWalletForm()); walletFunction.setViewOrder(1); diff --git a/src/main/resources/com/sparrowwallet/sparrow/wallet/addresses.css b/src/main/resources/com/sparrowwallet/sparrow/wallet/addresses.css new file mode 100644 index 00000000..8112456a --- /dev/null +++ b/src/main/resources/com/sparrowwallet/sparrow/wallet/addresses.css @@ -0,0 +1,19 @@ +.address-treetable-label { + -fx-font-weight: bold; + -fx-font-size: 1.2em; + -fx-padding: 10 0 10 0; +} + +.data-cell { + -fx-font-family: Courier; +} + +.label-cell .text-field { + -fx-padding: 0; +} + +.action-cell .button { + -fx-padding: 0; + -fx-pref-height: 18; + -fx-pref-width: 18; +} \ No newline at end of file diff --git a/src/main/resources/com/sparrowwallet/sparrow/wallet/addresses.fxml b/src/main/resources/com/sparrowwallet/sparrow/wallet/addresses.fxml new file mode 100644 index 00000000..ce022b12 --- /dev/null +++ b/src/main/resources/com/sparrowwallet/sparrow/wallet/addresses.fxml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + + +
+ +
+
+
diff --git a/src/main/resources/com/sparrowwallet/sparrow/wallet/settings.fxml b/src/main/resources/com/sparrowwallet/sparrow/wallet/settings.fxml index e2169dc7..31b1e686 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/wallet/settings.fxml +++ b/src/main/resources/com/sparrowwallet/sparrow/wallet/settings.fxml @@ -10,7 +10,7 @@ - +
diff --git a/src/main/resources/com/sparrowwallet/sparrow/wallet/wallet.css b/src/main/resources/com/sparrowwallet/sparrow/wallet/wallet.css index ae854ac9..c1c159c4 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/wallet/wallet.css +++ b/src/main/resources/com/sparrowwallet/sparrow/wallet/wallet.css @@ -25,6 +25,6 @@ -fx-opacity: 1; } -#walletPane { +.wallet-pane { -fx-background-color: -fx-background; -} \ No newline at end of file +} diff --git a/src/main/resources/com/sparrowwallet/sparrow/wallet/wallet.fxml b/src/main/resources/com/sparrowwallet/sparrow/wallet/wallet.fxml index f766655c..8b3fa64f 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/wallet/wallet.fxml +++ b/src/main/resources/com/sparrowwallet/sparrow/wallet/wallet.fxml @@ -65,7 +65,7 @@
- +