diff --git a/build.gradle b/build.gradle index 6c106ac3..54e5c3b2 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ plugins { id 'org.beryx.jlink' version '2.26.0' } -def sparrowVersion = '1.7.8' +def sparrowVersion = '1.7.10' def os = org.gradle.internal.os.OperatingSystem.current() def osName = os.getFamilyName() if(os.macOsX) { diff --git a/docs/reproducible.md b/docs/reproducible.md index df1a08f0..21410160 100644 --- a/docs/reproducible.md +++ b/docs/reproducible.md @@ -82,7 +82,7 @@ sudo apt install -y rpm fakeroot binutils First, assign a temporary variable in your shell for the specific release you want to build. For the current one specify: ```shell -GIT_TAG="1.7.7" +GIT_TAG="1.7.9" ``` The project can then be initially cloned as follows: diff --git a/drongo b/drongo index 38b04b8e..2b7b650f 160000 --- a/drongo +++ b/drongo @@ -1 +1 @@ -Subproject commit 38b04b8e0b802f6cd43b4e88730d4d3ed31227fc +Subproject commit 2b7b650faeeeda7fc25ab0962a6132e6531ced4c diff --git a/src/main/deploy/package/osx/Info.plist b/src/main/deploy/package/osx/Info.plist index b45037c0..4583d916 100644 --- a/src/main/deploy/package/osx/Info.plist +++ b/src/main/deploy/package/osx/Info.plist @@ -21,7 +21,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.7.8 + 1.7.10 CFBundleSignature ???? diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java index df5935d0..eb3103e2 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppController.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppController.java @@ -166,6 +166,9 @@ public class AppController implements Initializable { @FXML private MenuItem lockAllWallets; + @FXML + private MenuItem showWalletSummary; + @FXML private MenuItem searchWallet; @@ -210,6 +213,8 @@ public class AppController implements Initializable { private Timeline statusTimeline; + private SendToManyDialog sendToManyDialog; + private Tab previouslySelectedTab; private boolean subTabsVisible; @@ -218,6 +223,8 @@ public class AppController implements Initializable { private final Set emptyLoadingWallets = new LinkedHashSet<>(); + private final Map renamedWallets = new HashMap<>(); + private final ChangeListener serverToggleOnlineListener = (observable, oldValue, newValue) -> { Platform.runLater(() -> setServerToggleTooltip(getCurrentBlockHeight())); }; @@ -376,6 +383,7 @@ public class AppController implements Initializable { deleteWallet.disableProperty().bind(exportWallet.disableProperty()); closeTab.setDisable(true); lockWallet.setDisable(true); + showWalletSummary.disableProperty().bind(exportWallet.disableProperty()); searchWallet.disableProperty().bind(exportWallet.disableProperty()); refreshWallet.disableProperty().bind(Bindings.or(exportWallet.disableProperty(), Bindings.or(serverToggle.disableProperty(), AppServices.onlineProperty().not()))); sendToMany.disableProperty().bind(exportWallet.disableProperty()); @@ -467,7 +475,17 @@ public class AppController implements Initializable { } public void submitBugReport(ActionEvent event) { - AppServices.get().getApplication().getHostServices().showDocument("https://sparrowwallet.com/submitbugreport"); + ButtonType supportType = new ButtonType("Get Support", ButtonBar.ButtonData.LEFT); + ButtonType bugType = new ButtonType("Submit Bug Report", ButtonBar.ButtonData.YES); + Optional optResponse = showWarningDialog("Submit Bug Report", "Please note that this facility is for bug reports and feature requests only. There is a community of Sparrow users who can assist with support requests.", supportType, bugType); + + if(optResponse.isPresent()) { + if(optResponse.get() == bugType) { + AppServices.get().getApplication().getHostServices().showDocument("https://sparrowwallet.com/submitbugreport"); + } else { + openSupport(event); + } + } } public void showAbout(ActionEvent event) { @@ -839,6 +857,10 @@ public class AppController implements Initializable { } } + public void renameWallet(ActionEvent event) { + renameWallet(getSelectedWalletForm()); + } + public void deleteWallet(ActionEvent event) { deleteWallet(getSelectedWalletForm()); } @@ -1286,6 +1308,13 @@ public class AppController implements Initializable { } public void sendToMany(ActionEvent event) { + if(sendToManyDialog != null) { + Stage stage = (Stage)sendToManyDialog.getDialogPane().getScene().getWindow(); + stage.setAlwaysOnTop(true); + stage.setAlwaysOnTop(false); + return; + } + WalletForm selectedWalletForm = getSelectedWalletForm(); if(selectedWalletForm != null) { Wallet wallet = selectedWalletForm.getWallet(); @@ -1294,8 +1323,10 @@ public class AppController implements Initializable { bitcoinUnit = wallet.getAutoUnit(); } - SendToManyDialog sendToManyDialog = new SendToManyDialog(bitcoinUnit); + sendToManyDialog = new SendToManyDialog(bitcoinUnit); + sendToManyDialog.initModality(Modality.NONE); Optional> optPayments = sendToManyDialog.showAndWait(); + sendToManyDialog = null; optPayments.ifPresent(payments -> { if(!payments.isEmpty()) { EventManager.get().post(new SendActionEvent(wallet, new ArrayList<>(wallet.getSpendableUtxos().keySet()))); @@ -1442,6 +1473,21 @@ public class AppController implements Initializable { } } + public void showWalletSummary(ActionEvent event) { + Tab selectedTab = tabs.getSelectionModel().getSelectedItem(); + if(selectedTab != null) { + TabData tabData = (TabData) selectedTab.getUserData(); + if(tabData instanceof WalletTabData) { + TabPane subTabs = (TabPane) selectedTab.getContent(); + List walletForms = subTabs.getTabs().stream().map(subTab -> ((WalletTabData)subTab.getUserData()).getWalletForm()).collect(Collectors.toList()); + if(!walletForms.isEmpty()) { + WalletSummaryDialog walletSummaryDialog = new WalletSummaryDialog(walletForms); + walletSummaryDialog.showAndWait(); + } + } + } + } + public void refreshWallet(ActionEvent event) { WalletForm selectedWalletForm = getSelectedWalletForm(); if(selectedWalletForm != null) { @@ -1527,6 +1573,11 @@ public class AppController implements Initializable { tabs.getTabs().add(tab); tabs.getSelectionModel().select(tab); + + File oldWalletFile = renamedWallets.remove(storage.getWalletFile()); + if(oldWalletFile != null) { + deleteStorage(new Storage(oldWalletFile), false); + } } else { for(Tab walletTab : tabs.getTabs()) { TabData tabData = (TabData)walletTab.getUserData(); @@ -1603,6 +1654,9 @@ public class AppController implements Initializable { subTabLabel.setGraphic(getSubTabGlyph(wallet)); subTabLabel.setContentDisplay(ContentDisplay.TOP); subTabLabel.setAlignment(Pos.TOP_CENTER); + if(isSubTabLabelTruncated(subTabLabel, label)) { + subTabLabel.setTooltip(new Tooltip(label)); + } subTab.setGraphic(subTabLabel); FXMLLoader walletLoader = new FXMLLoader(getClass().getResource("wallet/wallet.fxml")); subTab.setContent(walletLoader.load()); @@ -1949,6 +2003,30 @@ public class AppController implements Initializable { } } + private void renameWallet(WalletForm selectedWalletForm) { + WalletNameDialog walletNameDialog = new WalletNameDialog(selectedWalletForm.getMasterWallet().getName(), false, null, true); + Optional optName = walletNameDialog.showAndWait(); + if(optName.isPresent()) { + File walletFile = Storage.getWalletFile(optName.get().getName() + "." + PersistenceType.DB.getExtension()); + if(walletFile.exists()) { + showErrorDialog("Error renaming wallet", "Wallet file " + walletFile.getAbsolutePath() + " already exists."); + return; + } + + Storage.CopyWalletService copyWalletService = new Storage.CopyWalletService(selectedWalletForm.getWallet(), walletFile); + copyWalletService.setOnSucceeded(event -> { + renamedWallets.put(walletFile, selectedWalletForm.getStorage().getWalletFile()); + tabs.getTabs().remove(tabs.getSelectionModel().getSelectedItem()); + openWalletFile(walletFile, true); + }); + copyWalletService.setOnFailed(event -> { + log.error("Error renaming wallet", event.getSource().getException()); + showErrorDialog("Error renaming wallet", event.getSource().getException().getMessage()); + }); + copyWalletService.start(); + } + } + private void deleteWallet(WalletForm selectedWalletForm) { Optional optButtonType = AppServices.showWarningDialog("Delete " + selectedWalletForm.getWallet().getMasterName() + "?", "The wallet file and any backups will be deleted. Are you sure?", ButtonType.NO, ButtonType.YES); if(optButtonType.isPresent() && optButtonType.get() == ButtonType.YES) { @@ -1964,7 +2042,7 @@ public class AppController implements Initializable { try { tabs.getTabs().remove(tabs.getSelectionModel().getSelectedItem()); - deleteStorage(storage); + deleteStorage(storage, true); } finally { encryptionFullKey.clear(); password.get().clear(); @@ -1986,15 +2064,15 @@ public class AppController implements Initializable { } } else { tabs.getTabs().remove(tabs.getSelectionModel().getSelectedItem()); - deleteStorage(storage); + deleteStorage(storage, true); } } } - private void deleteStorage(Storage storage) { + private void deleteStorage(Storage storage, boolean deleteBackups) { if(storage.isClosed()) { Platform.runLater(() -> { - Storage.DeleteWalletService deleteWalletService = new Storage.DeleteWalletService(storage); + Storage.DeleteWalletService deleteWalletService = new Storage.DeleteWalletService(storage, deleteBackups); deleteWalletService.setDelay(Duration.seconds(3)); deleteWalletService.setPeriod(Duration.hours(1)); deleteWalletService.setOnSucceeded(event -> { @@ -2010,7 +2088,7 @@ public class AppController implements Initializable { deleteWalletService.start(); }); } else { - Platform.runLater(() -> deleteStorage(storage)); + Platform.runLater(() -> deleteStorage(storage, deleteBackups)); } } @@ -2026,6 +2104,11 @@ public class AppController implements Initializable { if(optLabel.isPresent()) { String label = optLabel.get(); subTabLabel.setText(label); + if(isSubTabLabelTruncated(subTabLabel, label)) { + subTabLabel.setTooltip(new Tooltip(label)); + } else { + subTabLabel.setTooltip(null); + } Wallet renamedWallet = AppServices.get().getWallet(walletId); renamedWallet.setLabel(label); @@ -2050,9 +2133,18 @@ public class AppController implements Initializable { contextMenu.getItems().add(delete); } + contextMenu.setOnShowing(event -> { + Wallet renameWallet = AppServices.get().getWallet(walletId); + rename.setDisable(!renameWallet.isValid()); + }); + return contextMenu; } + private boolean isSubTabLabelTruncated(Label subTabLabel, String label) { + return TextUtils.computeTextWidth(subTabLabel.getFont(), label, 0.0D) > (90-6); + } + private void configureSwitchServer() { switchServer.getItems().clear(); diff --git a/src/main/java/com/sparrowwallet/sparrow/DefaultInteractionServices.java b/src/main/java/com/sparrowwallet/sparrow/DefaultInteractionServices.java index 7585497f..2e712f23 100644 --- a/src/main/java/com/sparrowwallet/sparrow/DefaultInteractionServices.java +++ b/src/main/java/com/sparrowwallet/sparrow/DefaultInteractionServices.java @@ -53,6 +53,8 @@ public class DefaultInteractionServices implements InteractionServices { alert.getDialogPane().setPrefHeight(200 + numLines * 20); } + alert.setResizable(true); + moveToActiveWindowScreen(alert); return alert.showAndWait(); } diff --git a/src/main/java/com/sparrowwallet/sparrow/SparrowWallet.java b/src/main/java/com/sparrowwallet/sparrow/SparrowWallet.java index 3089570e..036d1183 100644 --- a/src/main/java/com/sparrowwallet/sparrow/SparrowWallet.java +++ b/src/main/java/com/sparrowwallet/sparrow/SparrowWallet.java @@ -18,7 +18,7 @@ import java.util.*; public class SparrowWallet { public static final String APP_ID = "com.sparrowwallet.sparrow"; public static final String APP_NAME = "Sparrow"; - public static final String APP_VERSION = "1.7.8"; + public static final String APP_VERSION = "1.7.10"; public static final String APP_VERSION_SUFFIX = ""; public static final String APP_HOME_PROPERTY = "sparrow.home"; public static final String NETWORK_ENV_PROPERTY = "SPARROW_NETWORK"; diff --git a/src/main/java/com/sparrowwallet/sparrow/control/CoinTreeTable.java b/src/main/java/com/sparrowwallet/sparrow/control/CoinTreeTable.java index 87f68c0f..e292849e 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/CoinTreeTable.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/CoinTreeTable.java @@ -2,6 +2,7 @@ package com.sparrowwallet.sparrow.control; import com.sparrowwallet.drongo.BitcoinUnit; import com.sparrowwallet.drongo.wallet.Wallet; +import com.sparrowwallet.sparrow.CurrencyRate; import com.sparrowwallet.sparrow.UnitFormat; import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.EventManager; @@ -17,6 +18,7 @@ import javafx.geometry.Pos; import javafx.scene.Node; import javafx.scene.control.Hyperlink; import javafx.scene.control.Label; +import javafx.scene.control.TreeTableColumn; import javafx.scene.control.TreeTableView; import javafx.scene.layout.StackPane; @@ -28,6 +30,7 @@ import java.util.Optional; public class CoinTreeTable extends TreeTableView { private BitcoinUnit bitcoinUnit; private UnitFormat unitFormat; + private CurrencyRate currencyRate; public BitcoinUnit getBitcoinUnit() { return bitcoinUnit; @@ -64,6 +67,18 @@ public class CoinTreeTable extends TreeTableView { } } + public CurrencyRate getCurrencyRate() { + return currencyRate; + } + + public void setCurrencyRate(CurrencyRate currencyRate) { + this.currencyRate = currencyRate; + + if(!getChildren().isEmpty()) { + refresh(); + } + } + public void updateHistoryStatus(WalletHistoryStatusEvent event) { if(getRoot() != null) { Entry entry = getRoot().getValue(); @@ -119,4 +134,12 @@ public class CoinTreeTable extends TreeTableView { stackPane.setAlignment(Pos.CENTER); return stackPane; } + + public void setSortColumn(int columnIndex, TreeTableColumn.SortType sortType) { + if(columnIndex >= 0 && columnIndex < getColumns().size() && getSortOrder().isEmpty() && !getRoot().getChildren().isEmpty()) { + TreeTableColumn column = getColumns().get(columnIndex); + column.setSortType(sortType == null ? TreeTableColumn.SortType.DESCENDING : sortType); + getSortOrder().add(column); + } + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java b/src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java index e502854e..bb0c49bb 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java @@ -301,7 +301,7 @@ public class DevicePane extends TitledDescriptionPane { if(importButton instanceof SplitMenuButton importMenuButton) { if(wallet.getScriptType() == null) { - ScriptType[] scriptTypes = new ScriptType[] {ScriptType.P2WPKH, ScriptType.P2SH_P2WPKH, ScriptType.P2PKH}; + ScriptType[] scriptTypes = new ScriptType[] {ScriptType.P2WPKH, ScriptType.P2SH_P2WPKH, ScriptType.P2PKH, ScriptType.P2TR}; for(ScriptType scriptType : scriptTypes) { MenuItem item = new MenuItem(scriptType.getDescription()); final List derivation = scriptType.getDefaultDerivation(); diff --git a/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java b/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java index 88b9bc42..8640fefd 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java @@ -100,7 +100,7 @@ public class EntryCell extends TreeTableCell implements Confirmati actionBox.getChildren().add(viewTransactionButton); BlockTransaction blockTransaction = transactionEntry.getBlockTransaction(); - if(blockTransaction.getHeight() <= 0 && blockTransaction.getTransaction().isReplaceByFee() && + if(blockTransaction.getHeight() <= 0 && canRBF(blockTransaction) && Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) { Button increaseFeeButton = new Button(""); increaseFeeButton.setGraphic(getIncreaseFeeRBFGlyph()); @@ -216,7 +216,7 @@ public class EntryCell extends TreeTableCell implements Confirmati .map(e -> (HashIndexEntry)e) .filter(e -> e.getType().equals(HashIndexEntry.Type.INPUT) && e.isSpendable()) .map(e -> blockTransaction.getTransaction().getInputs().get((int)e.getHashIndex().getIndex())) - .filter(TransactionInput::isReplaceByFeeEnabled) + .filter(i -> Config.get().isMempoolFullRbf() || i.isReplaceByFeeEnabled()) .map(txInput -> walletTxos.keySet().stream().filter(txo -> txo.getHash().equals(txInput.getOutpoint().getHash()) && txo.getIndex() == txInput.getOutpoint().getIndex()).findFirst().get()) .collect(Collectors.toList()); @@ -240,6 +240,7 @@ public class EntryCell extends TreeTableCell implements Confirmati .map(e -> e.getBlockTransaction().getTransaction().getOutputs().get((int)e.getHashIndex().getIndex())) .collect(Collectors.toList()); + boolean consolidationTransaction = consolidationOutputs.size() == blockTransaction.getTransaction().getOutputs().size() && consolidationOutputs.size() == 1; long changeTotal = ourOutputs.stream().mapToLong(TransactionOutput::getValue).sum() - consolidationOutputs.stream().mapToLong(TransactionOutput::getValue).sum(); Transaction tx = blockTransaction.getTransaction(); double vSize = tx.getVirtualSize(); @@ -254,7 +255,7 @@ public class EntryCell extends TreeTableCell implements Confirmati List outputGroups = transactionEntry.getWallet().getGroupedUtxos(txoFilters, feeRate, AppServices.getMinimumRelayFeeRate(), Config.get().isGroupByAddress()) .stream().filter(outputGroup -> outputGroup.getEffectiveValue() >= 0).collect(Collectors.toList()); Collections.shuffle(outputGroups); - while((double)changeTotal / vSize < getMaxFeeRate() && !outputGroups.isEmpty() && !cancelTransaction) { + while((double)changeTotal / vSize < getMaxFeeRate() && !outputGroups.isEmpty() && !cancelTransaction && !consolidationTransaction) { //If there is insufficient change output, include another random output group so the fee can be increased OutputGroup outputGroup = outputGroups.remove(0); for(BlockTransactionHashIndex utxo : outputGroup.getUtxos()) { @@ -392,6 +393,10 @@ public class EntryCell extends TreeTableCell implements Confirmati Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), utxos, List.of(payment), null, blockTransaction.getFee(), true, null))); } + private static boolean canRBF(BlockTransaction blockTransaction) { + return Config.get().isMempoolFullRbf() || blockTransaction.getTransaction().isReplaceByFee(); + } + private static boolean canSignMessage(WalletNode walletNode) { Wallet wallet = walletNode.getWallet(); return wallet.getKeystores().size() == 1 && @@ -469,7 +474,7 @@ public class EntryCell extends TreeTableCell implements Confirmati tooltip += "\nFee rate: " + String.format("%.2f", feeRate) + " sats/vB"; } - tooltip += "\nRBF: " + (transactionEntry.getBlockTransaction().getTransaction().isReplaceByFee() ? "Enabled" : "Disabled"); + tooltip += "\nRBF: " + (canRBF(transactionEntry.getBlockTransaction()) ? "Enabled" : "Disabled"); } return tooltip; @@ -546,7 +551,7 @@ public class EntryCell extends TreeTableCell implements Confirmati }); getItems().add(viewTransaction); - if(blockTransaction.getTransaction().isReplaceByFee() && Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) { + if(canRBF(blockTransaction) && Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) { MenuItem increaseFee = new MenuItem("Increase Fee (RBF)"); increaseFee.setGraphic(getIncreaseFeeRBFGlyph()); increaseFee.setOnAction(AE -> { @@ -557,7 +562,7 @@ public class EntryCell extends TreeTableCell implements Confirmati getItems().add(increaseFee); } - if(blockTransaction.getTransaction().isReplaceByFee() && Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) { + if(canRBF(blockTransaction) && Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) { MenuItem cancelTx = new MenuItem("Cancel Transaction (RBF)"); cancelTx.setGraphic(getCancelTransactionRBFGlyph()); cancelTx.setOnAction(AE -> { @@ -789,6 +794,8 @@ public class EntryCell extends TreeTableCell implements Confirmati cell.getStyleClass().remove("transaction-row"); cell.getStyleClass().remove("node-row"); cell.getStyleClass().remove("utxo-row"); + cell.getStyleClass().remove("unconfirmed-row"); + cell.getStyleClass().remove("summary-row"); cell.getStyleClass().remove("address-cell"); cell.getStyleClass().remove("hashindex-row"); cell.getStyleClass().remove("confirming"); @@ -823,6 +830,10 @@ public class EntryCell extends TreeTableCell implements Confirmati if(hashIndexEntry.isSpent()) { cell.getStyleClass().add("spent"); } + } else if(entry instanceof WalletSummaryDialog.UnconfirmedEntry) { + cell.getStyleClass().add("unconfirmed-row"); + } else if(entry instanceof WalletSummaryDialog.SummaryEntry) { + cell.getStyleClass().add("summary-row"); } } } diff --git a/src/main/java/com/sparrowwallet/sparrow/control/FiatCell.java b/src/main/java/com/sparrowwallet/sparrow/control/FiatCell.java new file mode 100644 index 00000000..c1b24454 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/control/FiatCell.java @@ -0,0 +1,94 @@ +package com.sparrowwallet.sparrow.control; + +import com.sparrowwallet.drongo.protocol.Transaction; +import com.sparrowwallet.sparrow.CurrencyRate; +import com.sparrowwallet.sparrow.UnitFormat; +import com.sparrowwallet.sparrow.wallet.Entry; +import javafx.scene.control.ContextMenu; +import javafx.scene.control.MenuItem; +import javafx.scene.control.Tooltip; +import javafx.scene.control.TreeTableCell; +import javafx.scene.input.Clipboard; +import javafx.scene.input.ClipboardContent; +import org.controlsfx.tools.Platform; + +import java.math.BigDecimal; +import java.util.Currency; + +public class FiatCell extends TreeTableCell { + private final Tooltip tooltip; + private final FiatContextMenu contextMenu; + + public FiatCell() { + super(); + tooltip = new Tooltip(); + contextMenu = new FiatContextMenu(); + getStyleClass().add("coin-cell"); + if(Platform.getCurrent() == Platform.OSX) { + getStyleClass().add("number-field"); + } + } + + @Override + protected void updateItem(Number amount, boolean empty) { + super.updateItem(amount, empty); + + if(empty || amount == null) { + setText(null); + setGraphic(null); + setTooltip(null); + setContextMenu(null); + } else { + Entry entry = getTreeTableView().getTreeItem(getIndex()).getValue(); + EntryCell.applyRowStyles(this, entry); + + CoinTreeTable coinTreeTable = (CoinTreeTable) getTreeTableView(); + UnitFormat format = coinTreeTable.getUnitFormat(); + CurrencyRate currencyRate = coinTreeTable.getCurrencyRate(); + + if(currencyRate != null && currencyRate.isAvailable()) { + Currency currency = currencyRate.getCurrency(); + double btcRate = currencyRate.getBtcRate(); + + BigDecimal satsBalance = BigDecimal.valueOf(amount.longValue()); + BigDecimal btcBalance = satsBalance.divide(BigDecimal.valueOf(Transaction.SATOSHIS_PER_BITCOIN)); + BigDecimal fiatBalance = btcBalance.multiply(BigDecimal.valueOf(btcRate)); + + String label = format.formatCurrencyValue(fiatBalance.doubleValue()); + tooltip.setText("1 BTC = " + currency.getSymbol() + " " + format.formatCurrencyValue(btcRate)); + + setText(label); + setGraphic(null); + setTooltip(tooltip); + setContextMenu(contextMenu); + } else { + setText(null); + setGraphic(null); + setTooltip(null); + setContextMenu(null); + } + } + } + + private class FiatContextMenu extends ContextMenu { + public FiatContextMenu() { + MenuItem copyValue = new MenuItem("Copy Value"); + copyValue.setOnAction(AE -> { + hide(); + ClipboardContent content = new ClipboardContent(); + content.putString(getText()); + Clipboard.getSystemClipboard().setContent(content); + }); + + MenuItem copyRate = new MenuItem("Copy Rate"); + copyRate.setOnAction(AE -> { + hide(); + ClipboardContent content = new ClipboardContent(); + content.putString(getTooltip().getText()); + Clipboard.getSystemClipboard().setContent(content); + }); + + getItems().addAll(copyValue, copyRate); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/sparrowwallet/sparrow/control/FileWalletImportPane.java b/src/main/java/com/sparrowwallet/sparrow/control/FileWalletImportPane.java index a4abf902..2a581e3d 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/FileWalletImportPane.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/FileWalletImportPane.java @@ -12,7 +12,7 @@ public class FileWalletImportPane extends FileImportPane { private final WalletImport importer; public FileWalletImportPane(WalletImport importer) { - super(importer, importer.getName(), "Wallet import", importer.getWalletImportDescription(), "image/" + importer.getWalletModel().getType() + ".png", importer.isWalletImportScannable(), true); + super(importer, importer.getName(), "Wallet import", importer.getWalletImportDescription(), "image/" + importer.getWalletModel().getType() + ".png", importer.isWalletImportScannable(), importer.isWalletImportFileFormatAvailable()); this.importer = importer; } diff --git a/src/main/java/com/sparrowwallet/sparrow/control/MessageSignDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/MessageSignDialog.java index 1a851f2d..9853abc5 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/MessageSignDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/MessageSignDialog.java @@ -392,7 +392,7 @@ public class MessageSignDialog extends Dialog { } } - if(!verified && Bip322.isSupported(getAddress().getScriptType())) { + if(!verified && Bip322.isSupported(getAddress().getScriptType()) && !signature.getText().trim().isEmpty()) { try { verified = Bip322.verifyMessageBip322(getAddress().getScriptType(), getAddress(), message.getText().trim(), signature.getText().trim()); if(verified) { diff --git a/src/main/java/com/sparrowwallet/sparrow/control/QRScanDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/QRScanDialog.java index f6b9cf82..45996aed 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/QRScanDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/QRScanDialog.java @@ -502,18 +502,22 @@ public class QRScanDialog extends Dialog { if(cryptoOutput.getMultiKey() != null) { MultiKey multiKey = cryptoOutput.getMultiKey(); Map extendedPublicKeys = new LinkedHashMap<>(); + Map extendedPublicKeyLabels = new LinkedHashMap<>(); for(CryptoHDKey cryptoHDKey : multiKey.getHdKeys()) { ExtendedKey extendedKey = getExtendedKey(cryptoHDKey); KeyDerivation keyDerivation = getKeyDerivation(cryptoHDKey.getOrigin()); extendedPublicKeys.put(extendedKey, keyDerivation); + if(cryptoHDKey.getName() != null) { + extendedPublicKeyLabels.put(extendedKey, cryptoHDKey.getName()); + } } - return new OutputDescriptor(scriptType, multiKey.getThreshold(), extendedPublicKeys); + return new OutputDescriptor(scriptType, multiKey.getThreshold(), extendedPublicKeys, new LinkedHashMap<>(), extendedPublicKeyLabels); } else if(cryptoOutput.getEcKey() != null) { throw new IllegalArgumentException("EC keys are currently unsupported"); } else if(cryptoOutput.getHdKey() != null) { ExtendedKey extendedKey = getExtendedKey(cryptoOutput.getHdKey()); KeyDerivation keyDerivation = getKeyDerivation(cryptoOutput.getHdKey().getOrigin()); - return new OutputDescriptor(scriptType, extendedKey, keyDerivation); + return new OutputDescriptor(scriptType, extendedKey, keyDerivation, cryptoOutput.getHdKey().getName()); } throw new IllegalStateException("CryptoOutput did not contain sufficient information"); diff --git a/src/main/java/com/sparrowwallet/sparrow/control/SearchWalletDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/SearchWalletDialog.java index 6e094377..ef2ebd32 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/SearchWalletDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/SearchWalletDialog.java @@ -140,6 +140,8 @@ public class SearchWalletDialog extends Dialog { setResizable(true); + AppServices.moveToActiveWindowScreen(this); + Platform.runLater(search::requestFocus); } diff --git a/src/main/java/com/sparrowwallet/sparrow/control/TransactionsTreeTable.java b/src/main/java/com/sparrowwallet/sparrow/control/TransactionsTreeTable.java index 1d6ea82a..5bf37533 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/TransactionsTreeTable.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/TransactionsTreeTable.java @@ -50,8 +50,7 @@ public class TransactionsTreeTable extends CoinTreeTable { setPlaceholder(getDefaultPlaceholder(rootEntry.getWallet())); setEditable(true); setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY); - dateCol.setSortType(TreeTableColumn.SortType.DESCENDING); - getSortOrder().add(dateCol); + setSortColumn(0, TreeTableColumn.SortType.DESCENDING); } public void updateAll(WalletTransactionsEntry rootEntry) { @@ -61,16 +60,13 @@ public class TransactionsTreeTable extends CoinTreeTable { setRoot(rootItem); rootItem.setExpanded(true); - if(getColumns().size() > 0 && getSortOrder().isEmpty()) { - TreeTableColumn dateCol = getColumns().get(0); - getSortOrder().add(dateCol); - dateCol.setSortType(TreeTableColumn.SortType.DESCENDING); - } + setSortColumn(0, TreeTableColumn.SortType.DESCENDING); } public void updateHistory() { //Transaction entries should have already been updated using WalletTransactionsEntry.updateHistory, so only a resort required sort(); + setSortColumn(0, TreeTableColumn.SortType.DESCENDING); } public void updateLabel(Entry entry) { diff --git a/src/main/java/com/sparrowwallet/sparrow/control/TreeTableColumnConfig.java b/src/main/java/com/sparrowwallet/sparrow/control/TreeTableColumnConfig.java new file mode 100644 index 00000000..22cb65eb --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/control/TreeTableColumnConfig.java @@ -0,0 +1,46 @@ +package com.sparrowwallet.sparrow.control; + +import javafx.scene.control.TreeTableColumn; + +public class TreeTableColumnConfig { + private final int index; + private final Integer width; + private final TreeTableColumn.SortType sortType; + + public TreeTableColumnConfig(int index, Integer width, TreeTableColumn.SortType sortType) { + this.index = index; + this.width = width; + this.sortType = sortType; + } + + public TreeTableColumnConfig(int index, String width, String sortType) { + this.index = index; + this.width = width.isEmpty() ? null : Integer.valueOf(width, 10); + this.sortType = sortType.isEmpty() ? null : TreeTableColumn.SortType.valueOf(sortType); + } + + public int getIndex() { + return index; + } + + public Integer getWidth() { + return width; + } + + public TreeTableColumn.SortType getSortType() { + return sortType; + } + + public String toString() { + return index + "-" + (width == null ? "" : width) + "-" + (sortType == null ? "" : sortType); + } + + public static TreeTableColumnConfig fromString(String columnConfig) { + String[] parts = columnConfig.split("-", 3); + if(parts.length == 3) { + return new TreeTableColumnConfig(Integer.parseInt(parts[0]), parts[1], parts[2]); + } + + return new TreeTableColumnConfig(Integer.parseInt(parts[0]), (Integer)null, null); + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/control/TreeTableConfig.java b/src/main/java/com/sparrowwallet/sparrow/control/TreeTableConfig.java new file mode 100644 index 00000000..0028ef2a --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/control/TreeTableConfig.java @@ -0,0 +1,42 @@ +package com.sparrowwallet.sparrow.control; + +import com.sparrowwallet.sparrow.wallet.Entry; +import javafx.scene.control.TreeTableColumn; + +import java.util.ArrayList; +import java.util.List; +import java.util.StringJoiner; + +public class TreeTableConfig { + private final List columnConfigs; + + public TreeTableConfig(CoinTreeTable treeTable) { + columnConfigs = new ArrayList<>(); + TreeTableColumn sortColumn = treeTable.getSortOrder().isEmpty() ? null : treeTable.getSortOrder().get(0); + for(int i = 0; i < treeTable.getColumns().size(); i++) { + TreeTableColumn column = treeTable.getColumns().get(i); + //TODO: Support column widths + columnConfigs.add(new TreeTableColumnConfig(i, null, sortColumn == column ? column.getSortType() : null)); + } + } + + public TreeTableConfig(List columnConfigs) { + this.columnConfigs = columnConfigs; + } + + public String toString() { + StringJoiner joiner = new StringJoiner("|"); + columnConfigs.stream().forEach(col -> joiner.add(col.toString())); + return joiner.toString(); + } + + public static TreeTableConfig fromString(String tableConfig) { + List columnConfigs = new ArrayList<>(); + String[] parts = tableConfig.split("\\|"); + for(String part : parts) { + columnConfigs.add(TreeTableColumnConfig.fromString(part)); + } + + return new TreeTableConfig(columnConfigs); + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/control/UtxosTreeTable.java b/src/main/java/com/sparrowwallet/sparrow/control/UtxosTreeTable.java index e790a9c8..f937e167 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/UtxosTreeTable.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/UtxosTreeTable.java @@ -83,8 +83,7 @@ public class UtxosTreeTable extends CoinTreeTable { setPlaceholder(getDefaultPlaceholder(rootEntry.getWallet())); setEditable(true); setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY); - amountCol.setSortType(TreeTableColumn.SortType.DESCENDING); - getSortOrder().add(amountCol); + setSortColumn(getColumns().size() - 1, TreeTableColumn.SortType.DESCENDING); getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); } @@ -96,17 +95,14 @@ public class UtxosTreeTable extends CoinTreeTable { setRoot(rootItem); rootItem.setExpanded(true); - if(getColumns().size() > 0 && getSortOrder().isEmpty()) { - TreeTableColumn amountCol = getColumns().get(getColumns().size() - 1); - getSortOrder().add(amountCol); - amountCol.setSortType(TreeTableColumn.SortType.DESCENDING); - } + setSortColumn(getColumns().size() - 1, TreeTableColumn.SortType.DESCENDING); } public void updateHistory() { //Utxo entries should have already been updated, so only a resort required if(!getRoot().getChildren().isEmpty()) { sort(); + setSortColumn(getColumns().size() - 1, TreeTableColumn.SortType.DESCENDING); } } diff --git a/src/main/java/com/sparrowwallet/sparrow/control/WalletImportDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/WalletImportDialog.java index ba213cd2..c5719cab 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/WalletImportDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/WalletImportDialog.java @@ -59,7 +59,7 @@ public class WalletImportDialog extends Dialog { } } - List walletImporters = new ArrayList<>(List.of(new Bip129(), new CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new KeystoneMultisig(), new Descriptor(), new SpecterDesktop(), new BlueWalletMultisig(), new Sparrow())); + List walletImporters = new ArrayList<>(List.of(new Bip129(), new CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new KeystoneMultisig(), new Descriptor(), new SpecterDesktop(), new BlueWalletMultisig(), new Sparrow(), new JadeMultisig())); if(!selectedWalletForms.isEmpty()) { walletImporters.add(new WalletLabels(selectedWalletForms)); } diff --git a/src/main/java/com/sparrowwallet/sparrow/control/WalletNameDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/WalletNameDialog.java index af4d3d95..b9a58e51 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/WalletNameDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/WalletNameDialog.java @@ -42,9 +42,13 @@ public class WalletNameDialog extends Dialog } public WalletNameDialog(String initialName, boolean hasExistingTransactions, Date startDate) { + this(initialName, hasExistingTransactions, startDate, false); + } + + public WalletNameDialog(String initialName, boolean hasExistingTransactions, Date startDate, boolean rename) { final DialogPane dialogPane = getDialogPane(); AppServices.setStageIcon(dialogPane.getScene().getWindow()); - boolean requestBirthDate = (Config.get().getServerType() == null || Config.get().getServerType() == ServerType.BITCOIN_CORE); + boolean requestBirthDate = !rename && (Config.get().getServerType() == null || Config.get().getServerType() == ServerType.BITCOIN_CORE); setTitle("Wallet Name"); dialogPane.setHeaderText("Enter a name for this wallet:"); @@ -121,16 +125,16 @@ public class WalletNameDialog extends Dialog )); }); - final ButtonType okButtonType = new javafx.scene.control.ButtonType("Create Wallet", ButtonBar.ButtonData.OK_DONE); + final ButtonType okButtonType = new javafx.scene.control.ButtonType((rename ? "Rename" : "Create") + " Wallet", ButtonBar.ButtonData.OK_DONE); dialogPane.getButtonTypes().addAll(okButtonType); Button okButton = (Button) dialogPane.lookupButton(okButtonType); BooleanBinding isInvalid = Bindings.createBooleanBinding(() -> - name.getText().length() == 0 || Storage.walletExists(name.getText()) || (existingCheck.isSelected() && existingPicker.getValue() == null), name.textProperty(), existingCheck.selectedProperty(), existingPicker.valueProperty()); + name.getText().trim().length() == 0 || Storage.walletExists(name.getText()) || (existingCheck.isSelected() && existingPicker.getValue() == null), name.textProperty(), existingCheck.selectedProperty(), existingPicker.valueProperty()); okButton.disableProperty().bind(isInvalid); name.setPromptText("Wallet Name"); Platform.runLater(name::requestFocus); - setResultConverter(dialogButton -> dialogButton == okButtonType ? new NameAndBirthDate(name.getText(), existingPicker.getValue()) : null); + setResultConverter(dialogButton -> dialogButton == okButtonType ? new NameAndBirthDate(name.getText().trim(), existingPicker.getValue()) : null); } public static class NameAndBirthDate { diff --git a/src/main/java/com/sparrowwallet/sparrow/control/WalletSummaryDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/WalletSummaryDialog.java new file mode 100644 index 00000000..1d0bd575 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/control/WalletSummaryDialog.java @@ -0,0 +1,171 @@ +package com.sparrowwallet.sparrow.control; + +import com.sparrowwallet.drongo.wallet.Wallet; +import com.sparrowwallet.sparrow.AppServices; +import com.sparrowwallet.sparrow.CurrencyRate; +import com.sparrowwallet.sparrow.io.Config; +import com.sparrowwallet.sparrow.net.ExchangeSource; +import com.sparrowwallet.sparrow.wallet.Entry; +import com.sparrowwallet.sparrow.wallet.Function; +import com.sparrowwallet.sparrow.wallet.WalletForm; +import com.sparrowwallet.sparrow.wallet.WalletTransactionsEntry; +import javafx.beans.property.ReadOnlyObjectWrapper; +import javafx.geometry.Side; +import javafx.scene.chart.NumberAxis; +import javafx.scene.control.*; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +public class WalletSummaryDialog extends Dialog { + public WalletSummaryDialog(List walletForms) { + if(walletForms.isEmpty()) { + throw new IllegalArgumentException("No wallets selected to summarize"); + } + + Wallet masterWallet = walletForms.get(0).getMasterWallet(); + + final DialogPane dialogPane = getDialogPane(); + dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm()); + dialogPane.getStylesheets().add(AppServices.class.getResource("dialog.css").toExternalForm()); + dialogPane.getStylesheets().add(AppServices.class.getResource("wallet/wallet.css").toExternalForm()); + dialogPane.getStylesheets().add(AppServices.class.getResource("wallet/transactions.css").toExternalForm()); + + AppServices.setStageIcon(dialogPane.getScene().getWindow()); + dialogPane.setHeaderText("Wallet Summary for " + masterWallet.getName()); + + Image image = new Image("image/sparrow-small.png", 50, 50, false, false); + if(!image.isError()) { + ImageView imageView = new ImageView(); + imageView.setSmooth(false); + imageView.setImage(image); + dialogPane.setGraphic(imageView); + } + + HBox hBox = new HBox(40); + + CoinTreeTable table = new CoinTreeTable(); + + TreeTableColumn nameColumn = new TreeTableColumn<>("Wallet"); + nameColumn.setCellValueFactory((TreeTableColumn.CellDataFeatures param) -> { + return new ReadOnlyObjectWrapper<>(param.getValue().getValue().getLabel()); + }); + nameColumn.setCellFactory(p -> new LabelCell()); + table.getColumns().add(nameColumn); + + TreeTableColumn balanceColumn = new TreeTableColumn<>("Balance"); + balanceColumn.setCellValueFactory((TreeTableColumn.CellDataFeatures param) -> { + return new ReadOnlyObjectWrapper<>(param.getValue().getValue().getValue()); + }); + balanceColumn.setCellFactory(p -> new CoinCell()); + table.getColumns().add(balanceColumn); + table.setUnitFormat(masterWallet); + + CurrencyRate currencyRate = AppServices.getFiatCurrencyExchangeRate(); + if(currencyRate != null && currencyRate.isAvailable() && Config.get().getExchangeSource() != ExchangeSource.NONE) { + TreeTableColumn fiatColumn = new TreeTableColumn<>(currencyRate.getCurrency().getSymbol()); + fiatColumn.setCellValueFactory((TreeTableColumn.CellDataFeatures param) -> { + return new ReadOnlyObjectWrapper<>(param.getValue().getValue().getValue()); + }); + fiatColumn.setCellFactory(p -> new FiatCell()); + table.getColumns().add(fiatColumn); + table.setCurrencyRate(currencyRate); + } + + SummaryEntry rootEntry = new SummaryEntry(walletForms); + TreeItem rootItem = new TreeItem<>(rootEntry); + for(Entry childEntry : rootEntry.getChildren()) { + TreeItem childItem = new TreeItem<>(childEntry); + rootItem.getChildren().add(childItem); + childItem.getChildren().add(new TreeItem<>(new UnconfirmedEntry((WalletTransactionsEntry)childEntry))); + } + + table.setShowRoot(true); + table.setRoot(rootItem); + rootItem.setExpanded(true); + + table.setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY); + table.setPrefWidth(450); + + VBox vBox = new VBox(); + vBox.getChildren().add(table); + + hBox.getChildren().add(vBox); + + NumberAxis xAxis = new NumberAxis(); + xAxis.setSide(Side.BOTTOM); + xAxis.setForceZeroInRange(false); + xAxis.setMinorTickVisible(false); + NumberAxis yAxis = new NumberAxis(); + yAxis.setSide(Side.LEFT); + BalanceChart balanceChart = new BalanceChart(xAxis, yAxis); + balanceChart.initialize(new WalletTransactionsEntry(masterWallet, true)); + balanceChart.setAnimated(false); + balanceChart.setLegendVisible(false); + balanceChart.setVerticalGridLinesVisible(false); + + hBox.getChildren().add(balanceChart); + + getDialogPane().setContent(hBox); + + ButtonType okButtonType = new javafx.scene.control.ButtonType("Done", ButtonBar.ButtonData.OK_DONE); + dialogPane.getButtonTypes().addAll(okButtonType); + + AppServices.moveToActiveWindowScreen(this); + } + + public static class SummaryEntry extends Entry { + private SummaryEntry(List walletForms) { + super(walletForms.get(0).getWallet(), walletForms.get(0).getWallet().getName(), walletForms.stream().map(WalletForm::getWalletTransactionsEntry).collect(Collectors.toList())); + } + + @Override + public Long getValue() { + long value = 0; + for(Entry entry : getChildren()) { + value += entry.getValue(); + } + + return value; + } + + @Override + public String getEntryType() { + return null; + } + + @Override + public Function getWalletFunction() { + return Function.TRANSACTIONS; + } + } + + public static class UnconfirmedEntry extends Entry { + private final WalletTransactionsEntry walletTransactionsEntry; + + private UnconfirmedEntry(WalletTransactionsEntry walletTransactionsEntry) { + super(walletTransactionsEntry.getWallet(), "Unconfirmed", Collections.emptyList()); + this.walletTransactionsEntry = walletTransactionsEntry; + } + + @Override + public Long getValue() { + return walletTransactionsEntry.getMempoolBalance(); + } + + @Override + public String getEntryType() { + return null; + } + + @Override + public Function getWalletFunction() { + return Function.TRANSACTIONS; + } + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/control/WebcamService.java b/src/main/java/com/sparrowwallet/sparrow/control/WebcamService.java index 2473721d..74d7f263 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/WebcamService.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/WebcamService.java @@ -14,6 +14,7 @@ import javafx.concurrent.ScheduledService; import javafx.concurrent.Task; import javafx.embed.swing.SwingFXUtils; import javafx.scene.image.Image; +import net.sourceforge.zbar.ZBar; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -155,6 +156,13 @@ public class WebcamService extends ScheduledService { LuminanceSource source = new BufferedImageLuminanceSource(bufferedImage); BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source)); + if(ZBar.isEnabled()) { + ZBar.Scan scan = ZBar.scan(bufferedImage); + if(scan != null) { + return new Result(scan.stringData(), scan.rawData(), new ResultPoint[0], BarcodeFormat.QR_CODE); + } + } + try { return qrReader.decode(bitmap, Map.of(DecodeHintType.TRY_HARDER, Boolean.TRUE)); } catch(ReaderException e) { diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Config.java b/src/main/java/com/sparrowwallet/sparrow/io/Config.java index 079a5f17..5d26a0a7 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/Config.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/Config.java @@ -58,6 +58,7 @@ public class Config { private int enumerateHwPeriod = ENUMERATE_HW_PERIOD_SECS; private QRDensity qrDensity; private Boolean hdCapture; + private boolean useZbar = true; private String webcamDevice; private ServerType serverType; private Server publicElectrumServer; @@ -77,6 +78,7 @@ public class Config { private int maxPageSize = DEFAULT_PAGE_SIZE; private boolean usePayNym; private boolean sameAppMixing; + private boolean mempoolFullRbf; private Double appWidth; private Double appHeight; @@ -404,6 +406,10 @@ public class Config { flush(); } + public boolean isUseZbar() { + return useZbar; + } + public String getWebcamDevice() { return webcamDevice; } @@ -665,6 +671,15 @@ public class Config { flush(); } + public boolean isMempoolFullRbf() { + return mempoolFullRbf; + } + + public void setMempoolFullRbf(boolean mempoolFullRbf) { + this.mempoolFullRbf = mempoolFullRbf; + flush(); + } + public Double getAppWidth() { return appWidth; } diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Hwi.java b/src/main/java/com/sparrowwallet/sparrow/io/Hwi.java index dd68f5b4..8e06057c 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/Hwi.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/Hwi.java @@ -36,7 +36,7 @@ public class Hwi { private static final Logger log = LoggerFactory.getLogger(Hwi.class); private static final String HWI_HOME_DIR = "hwi"; private static final String HWI_VERSION_PREFIX = "hwi-"; - private static final String HWI_VERSION = "2.2.1"; + private static final String HWI_VERSION = "2.3.1"; private static final String HWI_VERSION_DIR = HWI_VERSION_PREFIX + HWI_VERSION; private static boolean isPromptActive = false; diff --git a/src/main/java/com/sparrowwallet/sparrow/io/JadeMultisig.java b/src/main/java/com/sparrowwallet/sparrow/io/JadeMultisig.java index b0c0d3a2..0f6ce703 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/JadeMultisig.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/JadeMultisig.java @@ -1,8 +1,11 @@ package com.sparrowwallet.sparrow.io; +import com.sparrowwallet.drongo.wallet.Keystore; import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.drongo.wallet.WalletModel; +import java.io.InputStream; + public class JadeMultisig extends ColdcardMultisig { @Override public String getName() { @@ -38,4 +41,30 @@ public class JadeMultisig extends ColdcardMultisig { public boolean walletExportRequiresDecryption() { return false; } + + @Override + public String getWalletImportDescription() { + return "Import the QR created using Options > Wallet > Registered Wallets on your Jade."; + } + + @Override + public boolean isWalletImportScannable() { + return true; + } + + @Override + public boolean isWalletImportFileFormatAvailable() { + return false; + } + + @Override + public Wallet importWallet(InputStream inputStream, String password) throws ImportException { + Wallet wallet = super.importWallet(inputStream, password); + for(Keystore keystore : wallet.getKeystores()) { + keystore.setLabel(keystore.getLabel().replace("Coldcard", "Jade")); + keystore.setWalletModel(WalletModel.JADE); + } + + return wallet; + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Sparrow.java b/src/main/java/com/sparrowwallet/sparrow/io/Sparrow.java index 5aeb82fe..4eac7586 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/Sparrow.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/Sparrow.java @@ -39,8 +39,10 @@ public class Sparrow implements WalletImport, WalletExport { Storage tempStorage = new Storage(persistence, tempFile); tempStorage.setKeyDeriver(storage.getKeyDeriver()); tempStorage.setEncryptionPubKey(storage.getEncryptionPubKey()); - tempStorage.saveWallet(exportedWallet); - for(Wallet childWallet : exportedWallet.getChildWallets()) { + + Wallet copy = exportedWallet.copy(); + tempStorage.saveWallet(copy); + for(Wallet childWallet : copy.getChildWallets()) { tempStorage.saveWallet(childWallet); } persistence.close(); diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Storage.java b/src/main/java/com/sparrowwallet/sparrow/io/Storage.java index 1abf8ca8..505acd59 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/Storage.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/Storage.java @@ -265,8 +265,11 @@ public class Storage { persistence.copyWallet(walletFile, outputStream); } - public boolean delete() { - deleteBackups(); + public boolean delete(boolean deleteBackups) { + if(deleteBackups) { + deleteBackups(); + } + return IOUtils.secureDelete(walletFile); } @@ -735,18 +738,44 @@ public class Storage { } } + public static class CopyWalletService extends Service { + private final Wallet wallet; + private final File newWalletFile; + + public CopyWalletService(Wallet wallet, File newWalletFile) { + this.wallet = wallet; + this.newWalletFile = newWalletFile; + } + + @Override + protected Task createTask() { + return new Task<>() { + protected Void call() throws IOException, ExportException { + Sparrow export = new Sparrow(); + try(BufferedOutputStream outputStream = new BufferedOutputStream(new FileOutputStream(newWalletFile))) { + export.exportWallet(wallet, outputStream); + } + + return null; + } + }; + } + } + public static class DeleteWalletService extends ScheduledService { private final Storage storage; + private final boolean deleteBackups; - public DeleteWalletService(Storage storage) { + public DeleteWalletService(Storage storage, boolean deleteBackups) { this.storage = storage; + this.deleteBackups = deleteBackups; } @Override protected Task createTask() { return new Task<>() { protected Boolean call() { - return storage.delete(); + return storage.delete(deleteBackups); } }; } diff --git a/src/main/java/com/sparrowwallet/sparrow/io/WalletImport.java b/src/main/java/com/sparrowwallet/sparrow/io/WalletImport.java index ee206761..1fbf36ba 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/WalletImport.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/WalletImport.java @@ -8,4 +8,7 @@ public interface WalletImport extends FileImport { String getWalletImportDescription(); Wallet importWallet(InputStream inputStream, String password) throws ImportException; boolean isWalletImportScannable(); + default boolean isWalletImportFileFormatAvailable() { + return true; + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/io/db/DbPersistence.java b/src/main/java/com/sparrowwallet/sparrow/io/db/DbPersistence.java index a7d74020..7c3da094 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/db/DbPersistence.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/db/DbPersistence.java @@ -583,7 +583,7 @@ public class DbPersistence implements Persistence { @Override public boolean isClosed() { - return dataSource.isClosed(); + return dataSource == null || dataSource.isClosed(); } @Override diff --git a/src/main/java/com/sparrowwallet/sparrow/io/db/MixConfigDao.java b/src/main/java/com/sparrowwallet/sparrow/io/db/MixConfigDao.java index 773bb2d3..77c03ff5 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/db/MixConfigDao.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/db/MixConfigDao.java @@ -21,6 +21,7 @@ public interface MixConfigDao { default void addMixConfig(Wallet wallet) { if(wallet.getMixConfig() != null) { + wallet.getMixConfig().setId(null); addOrUpdate(wallet, wallet.getMixConfig()); } } diff --git a/src/main/java/com/sparrowwallet/sparrow/net/IpAddressMatcher.java b/src/main/java/com/sparrowwallet/sparrow/net/IpAddressMatcher.java index 456fb325..654770a6 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/IpAddressMatcher.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/IpAddressMatcher.java @@ -96,9 +96,8 @@ public final class IpAddressMatcher { private InetAddress parseAddress(String address) { try { return InetAddress.getByName(address); - } - catch (UnknownHostException e) { - throw new IllegalArgumentException("Failed to parse address: " + address, e); + } catch(UnknownHostException e) { + throw new IllegalArgumentException("Failed to resolve address: " + address, e); } } diff --git a/src/main/java/com/sparrowwallet/sparrow/net/PublicElectrumServer.java b/src/main/java/com/sparrowwallet/sparrow/net/PublicElectrumServer.java index c439adf5..5cb23655 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/PublicElectrumServer.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/PublicElectrumServer.java @@ -14,6 +14,7 @@ public enum PublicElectrumServer { EMZY_DE("electrum.emzy.de", "ssl://electrum.emzy.de:50002", Network.MAINNET), BITAROO_NET("electrum.bitaroo.net", "ssl://electrum.bitaroo.net:50002", Network.MAINNET), DIYNODES_COM("electrum.diynodes.com", "ssl://electrum.diynodes.com:50022", Network.MAINNET), + SETHFORPRIVACY_COM("fulcrum.sethforprivacy.com", "ssl://fulcrum.sethforprivacy.com:50002", Network.MAINNET), TESTNET_ARANGUREN_ORG("testnet.aranguren.org", "ssl://testnet.aranguren.org:51002", Network.TESTNET); PublicElectrumServer(String name, String url, Network network) { diff --git a/src/main/java/com/sparrowwallet/sparrow/net/Tor.java b/src/main/java/com/sparrowwallet/sparrow/net/Tor.java index 03d39678..bdf3bef2 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/Tor.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/Tor.java @@ -86,6 +86,10 @@ public class Tor { dormantCanceledByStartup.set(TorConfig.Option.AorTorF.getTrue()); builder.put(dormantCanceledByStartup); + TorConfig.Setting.Ports.Control controlPort = new TorConfig.Setting.Ports.Control(); + controlPort.set(TorConfig.Option.AorDorPort.Auto.INSTANCE); + builder.put(controlPort); + return builder.build(); } }; diff --git a/src/main/java/com/sparrowwallet/sparrow/net/cormorant/bitcoind/BitcoindClient.java b/src/main/java/com/sparrowwallet/sparrow/net/cormorant/bitcoind/BitcoindClient.java index 3302f1cf..7a88c2f5 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/cormorant/bitcoind/BitcoindClient.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/cormorant/bitcoind/BitcoindClient.java @@ -141,11 +141,13 @@ public class BitcoindClient { boolean exists = listWalletDirResult.wallets().stream().anyMatch(walletDirResult -> walletDirResult.name().equals(CORE_WALLET_NAME)); legacyWalletExists = listWalletDirResult.wallets().stream().anyMatch(walletDirResult -> walletDirResult.name().equals(Bwt.DEFAULT_CORE_WALLET)); - if(!exists) { + List loadedWallets = getBitcoindService().listWallets(); + boolean loaded = loadedWallets.contains(CORE_WALLET_NAME); + + if(!exists && !loaded) { getBitcoindService().createWallet(CORE_WALLET_NAME, true, true, "", true, true, true, false); } else { - List wallets = getBitcoindService().listWallets(); - if(!wallets.contains(CORE_WALLET_NAME)) { + if(!loaded) { getBitcoindService().loadWallet(CORE_WALLET_NAME, true); } } diff --git a/src/main/java/com/sparrowwallet/sparrow/preferences/GeneralPreferencesController.java b/src/main/java/com/sparrowwallet/sparrow/preferences/GeneralPreferencesController.java index 739a34c1..5fb0a750 100644 --- a/src/main/java/com/sparrowwallet/sparrow/preferences/GeneralPreferencesController.java +++ b/src/main/java/com/sparrowwallet/sparrow/preferences/GeneralPreferencesController.java @@ -18,6 +18,7 @@ import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.fxml.FXML; import javafx.scene.control.ComboBox; +import javafx.scene.control.Label; import javafx.util.StringConverter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -45,6 +46,9 @@ public class GeneralPreferencesController extends PreferencesDetailController { @FXML private ComboBox exchangeSource; + @FXML + private Label currenciesLoadWarning; + @FXML private UnlabeledToggleSwitch loadRecentWallets; @@ -87,6 +91,9 @@ public class GeneralPreferencesController extends PreferencesDetailController { EventManager.get().post(new FeeRatesSourceChangedEvent(newValue)); }); + currenciesLoadWarning.managedProperty().bind(currenciesLoadWarning.visibleProperty()); + currenciesLoadWarning.setVisible(false); + blockExplorers.setItems(getBlockExplorerList()); blockExplorers.setConverter(new StringConverter<>() { @Override @@ -237,6 +244,8 @@ public class GeneralPreferencesController extends PreferencesDetailController { fiatCurrency.setDisable(true); } + currenciesLoadWarning.setVisible(exchangeSource.getValue() != ExchangeSource.NONE && currencies.isEmpty()); + //Always fire event regardless of previous selection to update rates EventManager.get().post(new FiatCurrencySelectedEvent(exchangeSource.getValue(), fiatCurrency.getValue())); diff --git a/src/main/java/com/sparrowwallet/sparrow/preferences/ServerAliasDialog.java b/src/main/java/com/sparrowwallet/sparrow/preferences/ServerAliasDialog.java index 11766ba8..c6617eae 100644 --- a/src/main/java/com/sparrowwallet/sparrow/preferences/ServerAliasDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/preferences/ServerAliasDialog.java @@ -26,6 +26,7 @@ import java.util.stream.Collectors; public class ServerAliasDialog extends Dialog { private final ServerType serverType; private final TableView serverTable; + private final Button closeButton; public ServerAliasDialog(ServerType serverType) { this.serverType = serverType; @@ -76,6 +77,7 @@ public class ServerAliasDialog extends Dialog { Button selectButton = (Button)dialogPane.lookupButton(selectButtonType); Button deleteButton = (Button)dialogPane.lookupButton(deleteButtonType); + closeButton = (Button)dialogPane.lookupButton(ButtonType.CLOSE); selectButton.setDefaultButton(true); selectButton.setDisable(true); deleteButton.setDisable(true); @@ -112,8 +114,14 @@ public class ServerAliasDialog extends Dialog { serverTable.getItems().remove(serverEntry); if(serverType == ServerType.BITCOIN_CORE) { Config.get().removeRecentCoreServer(serverEntry.getServer()); + if(serverEntry.getServer().equals(Config.get().getCoreServer()) && !serverTable.getItems().isEmpty()) { + closeButton.setDisable(true); + } } else { Config.get().removeRecentElectrumServer(serverEntry.getServer()); + if(serverEntry.getServer().equals(Config.get().getElectrumServer()) && !serverTable.getItems().isEmpty()) { + closeButton.setDisable(true); + } } } }); diff --git a/src/main/java/com/sparrowwallet/sparrow/terminal/preferences/PublicElectrumDialog.java b/src/main/java/com/sparrowwallet/sparrow/terminal/preferences/PublicElectrumDialog.java index f3d90b24..8dc068d2 100644 --- a/src/main/java/com/sparrowwallet/sparrow/terminal/preferences/PublicElectrumDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/terminal/preferences/PublicElectrumDialog.java @@ -37,7 +37,7 @@ public class PublicElectrumDialog extends ServerProxyDialog { url.setSelectedItem(PublicElectrumServer.fromServer(Config.get().getPublicElectrumServer())); url.addListener((selectedIndex, previousSelection, changedByUserInteraction) -> { if(selectedIndex != previousSelection) { - Config.get().setPublicElectrumServer(PublicElectrumServer.values()[selectedIndex].getServer()); + Config.get().setPublicElectrumServer(PublicElectrumServer.getServers().get(selectedIndex).getServer()); } }); mainPanel.addComponent(url); diff --git a/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/UtxosDialog.java b/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/UtxosDialog.java index 42ac5e67..952eaf32 100644 --- a/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/UtxosDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/UtxosDialog.java @@ -192,6 +192,9 @@ public class UtxosDialog extends WalletDialog { SparrowTerminal.get().getGuiThread().invokeLater(() -> { TableModel tableModel = getTableModel(walletUtxosEntry); utxos.setTableModel(tableModel); + if(utxos.getTheme() != null && utxos.getRenderer().getViewTopRow() >= tableModel.getRowCount()) { + utxos.getRenderer().setViewTopRow(0); + } }); } diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/KeystoreController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/KeystoreController.java index 7bcf11d2..eea9a126 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/KeystoreController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/KeystoreController.java @@ -271,7 +271,7 @@ public class KeystoreController extends WalletFormController implements Initiali validationSupport.registerValidator(label, Validator.combine( Validator.createEmptyValidator("Label is required"), (Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Label is not unique", walletForm.getWallet().getKeystores().stream().filter(k -> k != keystore).map(Keystore::getLabel).collect(Collectors.toList()).contains(newValue)), - (Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Label is too long", newValue.replace(" ", "").length() > 16) + (Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Label is too long", newValue.replace(" ", "").length() > Keystore.MAX_LABEL_LENGTH) )); validationSupport.registerValidator(xpub, Validator.combine( @@ -551,6 +551,9 @@ public class KeystoreController extends WalletFormController implements Initiali fingerprint.setText(keyDerivation.getMasterFingerprint()); derivation.setText(keyDerivation.getDerivationPath()); xpub.setText(extendedKey.toString()); + if(result.outputDescriptor.getExtendedPublicKeyLabel(extendedKey) != null) { + label.setText(result.outputDescriptor.getExtendedPublicKeyLabel(extendedKey)); + } } else if(result.wallets != null) { for(Wallet wallet : result.wallets) { if(getWalletForm().getWallet().getScriptType().equals(wallet.getScriptType()) && !wallet.getKeystores().isEmpty()) { @@ -558,6 +561,9 @@ public class KeystoreController extends WalletFormController implements Initiali fingerprint.setText(keystore.getKeyDerivation().getMasterFingerprint()); derivation.setText(keystore.getKeyDerivation().getDerivationPath()); xpub.setText(keystore.getExtendedPublicKey().toString()); + if(!Keystore.DEFAULT_LABEL.equals(keystore.getLabel())) { + label.setText(keystore.getLabel()); + } return; } } diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsController.java index 9bb40611..d5aa1526 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsController.java @@ -40,6 +40,7 @@ import java.util.*; import java.util.stream.Collectors; import static com.sparrowwallet.sparrow.AppServices.showErrorDialog; +import static com.sparrowwallet.sparrow.AppServices.showWarningDialog; public class SettingsController extends WalletFormController implements Initializable { private static final Logger log = LoggerFactory.getLogger(SettingsController.class); @@ -97,6 +98,7 @@ public class SettingsController extends WalletFormController implements Initiali private final SimpleIntegerProperty totalKeystores = new SimpleIntegerProperty(0); private boolean initialising = true; + private boolean reverting; @Override public void initialize(URL location, ResourceBundle resources) { @@ -141,9 +143,32 @@ public class SettingsController extends WalletFormController implements Initiali } }); - scriptType.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, scriptType) -> { - if(scriptType != null) { - walletForm.getWallet().setScriptType(scriptType); + scriptType.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> { + if(newValue != null) { + if(oldValue != null && !reverting && walletForm.getWallet().getKeystores().stream().anyMatch(keystore -> keystore.getExtendedPublicKey() != null)) { + Optional optType = showWarningDialog("Clear keystores?", + "You are changing the script type on a wallet with existing key information. Usually this means the keys need to be re-imported using a different derivation path.\n\n" + + "Do you want to clear the current key information?", ButtonType.YES, ButtonType.NO, ButtonType.CANCEL); + if(optType.isPresent()) { + if(optType.get() == ButtonType.CANCEL) { + reverting = true; + Platform.runLater(() -> { + scriptType.getSelectionModel().select(oldValue); + reverting = false; + }); + return; + } else if(optType.get() == ButtonType.YES) { + clearKeystoreTabs(); + if(walletForm.getWallet().getPolicyType() == PolicyType.MULTI) { + totalKeystores.bind(multisigControl.highValueProperty()); + } else { + totalKeystores.set(1); + } + } + } + } + + walletForm.getWallet().setScriptType(newValue); } EventManager.get().post(new SettingsChangedEvent(walletForm.getWallet(), SettingsChangedEvent.Type.SCRIPT_TYPE)); @@ -215,7 +240,10 @@ public class SettingsController extends WalletFormController implements Initiali totalKeystores.setValue(0); walletForm.revert(); initialising = true; + reverting = true; setFieldsFromWallet(walletForm.getWallet()); + reverting = false; + initialising = false; }); apply.setOnAction(event -> { @@ -313,12 +341,12 @@ public class SettingsController extends WalletFormController implements Initiali if(optionalResult.isPresent()) { QRScanDialog.Result result = optionalResult.get(); if(result.outputDescriptor != null) { - setDescriptorText(result.outputDescriptor.toString()); + replaceWallet(result.outputDescriptor.toWallet()); } else if(result.wallets != null) { for(Wallet wallet : result.wallets) { if(scriptType.getValue().equals(wallet.getScriptType()) && !wallet.getKeystores().isEmpty()) { OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(wallet); - setDescriptorText(outputDescriptor.toString()); + replaceWallet(outputDescriptor.toWallet()); break; } } @@ -397,7 +425,7 @@ public class SettingsController extends WalletFormController implements Initiali CryptoCoinInfo cryptoCoinInfo = new CryptoCoinInfo(CryptoCoinInfo.Type.BITCOIN.ordinal(), Network.get() == Network.MAINNET ? CryptoCoinInfo.Network.MAINNET.ordinal() : CryptoCoinInfo.Network.TESTNET.ordinal()); List pathComponents = keystore.getKeyDerivation().getDerivation().stream().map(cNum -> new IndexPathComponent(cNum.num(), cNum.isHardened())).collect(Collectors.toList()); CryptoKeypath cryptoKeypath = new CryptoKeypath(pathComponents, Utils.hexToBytes(keystore.getKeyDerivation().getMasterFingerprint()), pathComponents.size()); - return new CryptoHDKey(false, extendedKey.getKey().getPubKey(), extendedKey.getKey().getChainCode(), cryptoCoinInfo, cryptoKeypath, null, extendedKey.getParentFingerprint()); + return new CryptoHDKey(false, extendedKey.getKey().getPubKey(), extendedKey.getKey().getChainCode(), cryptoCoinInfo, cryptoKeypath, null, extendedKey.getParentFingerprint(), keystore.getLabel(), null); } public void editDescriptor(ActionEvent event) { diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletForm.java b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletForm.java index 2a850d2d..e55c2ead 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletForm.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletForm.java @@ -506,16 +506,22 @@ public class WalletForm { for(WalletNode childNode : wallet.getNode(keyPurpose).getChildren()) { for(BlockTransactionHashIndex receivedRef : childNode.getTransactionOutputs()) { if(receivedRef.getHash().equals(transactionEntry.getBlockTransaction().getHash())) { - if((receivedRef.getLabel() == null || receivedRef.getLabel().isEmpty()) && wallet.getStandardAccountType() != StandardAccount.WHIRLPOOL_PREMIX) { + String prevRefLabel = ""; + if((receivedRef.getLabel() == null || receivedRef.getLabel().isEmpty() + || receivedRef.getLabel().endsWith(" (sent)") || receivedRef.getLabel().endsWith(" (change)") || receivedRef.getLabel().endsWith(" (received)")) + && wallet.getStandardAccountType() != StandardAccount.WHIRLPOOL_PREMIX) { + prevRefLabel = receivedRef.getLabel() == null ? "" : receivedRef.getLabel(); receivedRef.setLabel(entry.getLabel() + (keyPurpose == KeyPurpose.CHANGE ? (event.getWallet().isBip47() ? " (sent)" : " (change)") : " (received)")); labelChangedEntries.put(new HashIndexEntry(event.getWallet(), receivedRef, HashIndexEntry.Type.OUTPUT, keyPurpose), entry); } - if((childNode.getLabel() == null || childNode.getLabel().isEmpty())) { + if(childNode.getLabel() == null || childNode.getLabel().isEmpty() + || prevRefLabel.equals(childNode.getLabel() + " (sent)") || prevRefLabel.equals(childNode.getLabel() + " (change)") || prevRefLabel.equals(childNode.getLabel() + " (received)")) { childNode.setLabel(entry.getLabel()); labelChangedEntries.put(new NodeEntry(event.getWallet(), childNode), entry); } } - if(receivedRef.isSpent() && receivedRef.getSpentBy().getHash().equals(transactionEntry.getBlockTransaction().getHash()) && (receivedRef.getSpentBy().getLabel() == null || receivedRef.getSpentBy().getLabel().isEmpty())) { + if(receivedRef.isSpent() && receivedRef.getSpentBy().getHash().equals(transactionEntry.getBlockTransaction().getHash()) + && (receivedRef.getSpentBy().getLabel() == null || receivedRef.getSpentBy().getLabel().isEmpty() || receivedRef.getSpentBy().getLabel().endsWith(" (input)"))) { receivedRef.getSpentBy().setLabel(entry.getLabel() + " (input)"); labelChangedEntries.put(new HashIndexEntry(event.getWallet(), receivedRef.getSpentBy(), HashIndexEntry.Type.INPUT, keyPurpose), entry); } @@ -591,6 +597,10 @@ public class WalletForm { public void walletLabelChanged(WalletLabelChangedEvent event) { if(event.getWallet() == wallet) { Platform.runLater(() -> EventManager.get().post(new WalletDataChangedEvent(wallet))); + + if(walletTransactionsEntry != null) { + walletTransactionsEntry.labelProperty().set(event.getWallet().getDisplayName()); + } } } diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletTransactionsEntry.java b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletTransactionsEntry.java index ad06421f..bbbdc014 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletTransactionsEntry.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletTransactionsEntry.java @@ -23,7 +23,11 @@ public class WalletTransactionsEntry extends Entry { private static final Logger log = LoggerFactory.getLogger(WalletTransactionsEntry.class); public WalletTransactionsEntry(Wallet wallet) { - super(wallet, wallet.getName(), getWalletTransactions(wallet).stream().map(WalletTransaction::getTransactionEntry).collect(Collectors.toList())); + this(wallet, false); + } + + public WalletTransactionsEntry(Wallet wallet, boolean includeAllChildWallets) { + super(wallet, wallet.getDisplayName(), getWalletTransactions(wallet, includeAllChildWallets).stream().map(WalletTransaction::getTransactionEntry).collect(Collectors.toList())); calculateBalances(false); //No need to resort } @@ -73,7 +77,7 @@ public class WalletTransactionsEntry extends Entry { .collect(Collectors.toUnmodifiableMap(entry -> new HashIndex(entry.getKey().getHash(), entry.getKey().getIndex()), Map.Entry::getKey, BinaryOperator.maxBy(BlockTransactionHashIndex::compareTo))); - Collection entries = getWalletTransactions(getWallet()); + Collection entries = getWalletTransactions(getWallet(), false); Set current = entries.stream().map(WalletTransaction::getTransactionEntry).collect(Collectors.toCollection(LinkedHashSet::new)); Set previous = new LinkedHashSet<>(getChildren()); @@ -101,7 +105,7 @@ public class WalletTransactionsEntry extends Entry { } } - private static Collection getWalletTransactions(Wallet wallet) { + private static Collection getWalletTransactions(Wallet wallet, boolean includeAllChildWallets) { Map walletTransactionMap = new HashMap<>(wallet.getTransactions().size()); for(KeyPurpose keyPurpose : wallet.getWalletKeyPurposes()) { @@ -109,7 +113,7 @@ public class WalletTransactionsEntry extends Entry { } for(Wallet childWallet : wallet.getChildWallets()) { - if(childWallet.isNested()) { + if(includeAllChildWallets || childWallet.isNested()) { for(KeyPurpose keyPurpose : childWallet.getWalletKeyPurposes()) { getWalletTransactions(childWallet, walletTransactionMap, childWallet.getNode(keyPurpose)); } @@ -218,6 +222,14 @@ public class WalletTransactionsEntry extends Entry { return mempoolBalance; } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof WalletTransactionsEntry)) return false; + + return super.equals(o); + } + private static class WalletTransaction implements Comparable { private final Wallet wallet; private final BlockTransaction blockTransaction; diff --git a/src/main/java/net/sourceforge/zbar/Config.java b/src/main/java/net/sourceforge/zbar/Config.java new file mode 100644 index 00000000..a5a47c9c --- /dev/null +++ b/src/main/java/net/sourceforge/zbar/Config.java @@ -0,0 +1,76 @@ +/*------------------------------------------------------------------------ + * Config + * + * Copyright 2010 (c) Jeff Brown + * + * This file is part of the ZBar Bar Code Reader. + * + * The ZBar Bar Code Reader is free software; you can redistribute it + * and/or modify it under the terms of the GNU Lesser Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * The ZBar Bar Code Reader is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser Public License for more details. + * + * You should have received a copy of the GNU Lesser Public License + * along with the ZBar Bar Code Reader; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301 USA + * + * http://sourceforge.net/projects/zbar + *------------------------------------------------------------------------*/ + +package net.sourceforge.zbar; + +/** + * Decoder configuration options. + */ +public class Config { + /** + * Enable symbology/feature. + */ + public static final int ENABLE = 0; + /** + * Enable check digit when optional. + */ + public static final int ADD_CHECK = 1; + /** + * Return check digit when present. + */ + public static final int EMIT_CHECK = 2; + /** + * Enable full ASCII character set. + */ + public static final int ASCII = 3; + + /** + * Minimum data length for valid decode. + */ + public static final int MIN_LEN = 0x20; + /** + * Maximum data length for valid decode. + */ + public static final int MAX_LEN = 0x21; + + /** + * Required video consistency frames. + */ + public static final int UNCERTAINTY = 0x40; + + /** + * Enable scanner to collect position data. + */ + public static final int POSITION = 0x80; + + /** + * Image scanner vertical scan density. + */ + public static final int X_DENSITY = 0x100; + /** + * Image scanner horizontal scan density. + */ + public static final int Y_DENSITY = 0x101; +} diff --git a/src/main/java/net/sourceforge/zbar/Image.java b/src/main/java/net/sourceforge/zbar/Image.java new file mode 100644 index 00000000..e2123091 --- /dev/null +++ b/src/main/java/net/sourceforge/zbar/Image.java @@ -0,0 +1,197 @@ +/*------------------------------------------------------------------------ + * Image + * + * Copyright 2007-2010 (c) Jeff Brown + * + * This file is part of the ZBar Bar Code Reader. + * + * The ZBar Bar Code Reader is free software; you can redistribute it + * and/or modify it under the terms of the GNU Lesser Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * The ZBar Bar Code Reader is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser Public License for more details. + * + * You should have received a copy of the GNU Lesser Public License + * along with the ZBar Bar Code Reader; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301 USA + * + * http://sourceforge.net/projects/zbar + *------------------------------------------------------------------------*/ + +package net.sourceforge.zbar; + +import java.io.Closeable; + +/** + * stores image data samples along with associated format and size + * metadata. + */ +public class Image implements Closeable { + static { + init(); + } + + /** + * C pointer to a zbar_symbol_t. + */ + private long peer; + private Object data; + + public Image() { + peer = create(); + } + + public Image(int width, int height) { + this(); + setSize(width, height); + } + + public Image(int width, int height, String format) { + this(); + setSize(width, height); + setFormat(format); + } + + public Image(String format) { + this(); + setFormat(format); + } + + Image(long peer) { + this.peer = peer; + } + + private static native void init(); + + /** + * Create an associated peer instance. + */ + private native long create(); + + public void close() { + destroy(); + } + + /** + * Clean up native data associated with an instance. + */ + public synchronized void destroy() { + if(peer != 0) { + destroy(peer); + peer = 0; + } + } + + /** + * Destroy the associated peer instance. + */ + private native void destroy(long peer); + + /** + * Image format conversion. + * + * @returns a @em new image with the sample data from the original + * image converted to the requested format fourcc. the original + * image is unaffected. + */ + public Image convert(String format) { + long newpeer = convert(peer, format); + if(newpeer == 0) { + return (null); + } + return (new Image(newpeer)); + } + + private native long convert(long peer, String format); + + /** + * Retrieve the image format fourcc. + */ + public native String getFormat(); + + /** + * Specify the fourcc image format code for image sample data. + */ + public native void setFormat(String format); + + /** + * Retrieve a "sequence" (page/frame) number associated with this + * image. + */ + public native int getSequence(); + + /** + * Associate a "sequence" (page/frame) number with this image. + */ + public native void setSequence(int seq); + + /** + * Retrieve the width of the image. + */ + public native int getWidth(); + + /** + * Retrieve the height of the image. + */ + public native int getHeight(); + + /** + * Retrieve the size of the image. + */ + public native int[] getSize(); + + /** + * Specify the pixel size of the image. + */ + public native void setSize(int[] size); + + /** + * Specify the pixel size of the image. + */ + public native void setSize(int width, int height); + + /** + * Retrieve the crop region of the image. + */ + public native int[] getCrop(); + + /** + * Specify the crop region of the image. + */ + public native void setCrop(int[] crop); + + /** + * Specify the crop region of the image. + */ + public native void setCrop(int x, int y, int width, int height); + + /** + * Retrieve the image sample data. + */ + public native byte[] getData(); + + /** + * Specify image sample data. + */ + public native void setData(byte[] data); + + /** + * Specify image sample data. + */ + public native void setData(int[] data); + + /** + * Retrieve the decoded results associated with this image. + */ + public SymbolSet getSymbols() { + return (new SymbolSet(getSymbols(peer))); + } + + private native long getSymbols(long peer); + +} diff --git a/src/main/java/net/sourceforge/zbar/ImageScanner.java b/src/main/java/net/sourceforge/zbar/ImageScanner.java new file mode 100644 index 00000000..39298a11 --- /dev/null +++ b/src/main/java/net/sourceforge/zbar/ImageScanner.java @@ -0,0 +1,110 @@ +/*------------------------------------------------------------------------ + * ImageScanner + * + * Copyright 2007-2010 (c) Jeff Brown + * + * This file is part of the ZBar Bar Code Reader. + * + * The ZBar Bar Code Reader is free software; you can redistribute it + * and/or modify it under the terms of the GNU Lesser Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * The ZBar Bar Code Reader is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser Public License for more details. + * + * You should have received a copy of the GNU Lesser Public License + * along with the ZBar Bar Code Reader; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301 USA + * + * http://sourceforge.net/projects/zbar + *------------------------------------------------------------------------*/ + +package net.sourceforge.zbar; + +import java.io.Closeable; + +/** + * Read barcodes from 2-D images. + */ +public class ImageScanner implements Closeable { + static { + init(); + } + + /** + * C pointer to a zbar_image_scanner_t. + */ + private long peer; + + public ImageScanner() { + peer = create(); + } + + private static native void init(); + + /** + * Create an associated peer instance. + */ + private native long create(); + + public void close() { + destroy(); + } + + /** + * Clean up native data associated with an instance. + */ + public synchronized void destroy() { + if(peer != 0) { + destroy(peer); + peer = 0; + } + } + + /** + * Destroy the associated peer instance. + */ + private native void destroy(long peer); + + /** + * Set config for indicated symbology (0 for all) to specified value. + */ + public native void setConfig(int symbology, int config, int value) throws IllegalArgumentException; + + /** + * Parse configuration string and apply to image scanner. + */ + public native void parseConfig(String config); + + /** + * Enable or disable the inter-image result cache (default disabled). + * Mostly useful for scanning video frames, the cache filters duplicate + * results from consecutive images, while adding some consistency + * checking and hysteresis to the results. Invoking this method also + * clears the cache. + */ + public native void enableCache(boolean enable); + + /** + * Retrieve decode results for last scanned image. + * + * @returns the SymbolSet result container + */ + public SymbolSet getResults() { + return (new SymbolSet(getResults(peer))); + } + + private native long getResults(long peer); + + /** + * Scan for symbols in provided Image. + * The image format must currently be "Y800" or "GRAY". + * + * @returns the number of symbols successfully decoded from the image. + */ + public native int scanImage(Image image); +} diff --git a/src/main/java/net/sourceforge/zbar/Modifier.java b/src/main/java/net/sourceforge/zbar/Modifier.java new file mode 100644 index 00000000..22b5571e --- /dev/null +++ b/src/main/java/net/sourceforge/zbar/Modifier.java @@ -0,0 +1,44 @@ +/*------------------------------------------------------------------------ + * Modifier + * + * Copyright 2010 (c) Jeff Brown + * + * This file is part of the ZBar Bar Code Reader. + * + * The ZBar Bar Code Reader is free software; you can redistribute it + * and/or modify it under the terms of the GNU Lesser Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * The ZBar Bar Code Reader is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser Public License for more details. + * + * You should have received a copy of the GNU Lesser Public License + * along with the ZBar Bar Code Reader; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301 USA + * + * http://sourceforge.net/projects/zbar + *------------------------------------------------------------------------*/ + +package net.sourceforge.zbar; + +/** + * Decoder symbology modifiers. + */ +public class Modifier { + /** + * barcode tagged as GS1 (EAN.UCC) reserved + * (eg, FNC1 before first data character). + * data may be parsed as a sequence of GS1 AIs + */ + public static final int GS1 = 0; + + /** + * barcode tagged as AIM reserved + * (eg, FNC1 after first character or digit pair) + */ + public static final int AIM = 1; +} diff --git a/src/main/java/net/sourceforge/zbar/Orientation.java b/src/main/java/net/sourceforge/zbar/Orientation.java new file mode 100644 index 00000000..fa278f2e --- /dev/null +++ b/src/main/java/net/sourceforge/zbar/Orientation.java @@ -0,0 +1,52 @@ +/*------------------------------------------------------------------------ + * Orientation + * + * Copyright 2010 (c) Jeff Brown + * + * This file is part of the ZBar Bar Code Reader. + * + * The ZBar Bar Code Reader is free software; you can redistribute it + * and/or modify it under the terms of the GNU Lesser Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * The ZBar Bar Code Reader is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser Public License for more details. + * + * You should have received a copy of the GNU Lesser Public License + * along with the ZBar Bar Code Reader; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301 USA + * + * http://sourceforge.net/projects/zbar + *------------------------------------------------------------------------*/ + +package net.sourceforge.zbar; + +/** + * Decoded symbol coarse orientation. + */ +public class Orientation { + /** + * Unable to determine orientation. + */ + public static final int UNKNOWN = -1; + /** + * Upright, read left to right. + */ + public static final int UP = 0; + /** + * sideways, read top to bottom + */ + public static final int RIGHT = 1; + /** + * upside-down, read right to left + */ + public static final int DOWN = 2; + /** + * sideways, read bottom to top + */ + public static final int LEFT = 3; +} diff --git a/src/main/java/net/sourceforge/zbar/Symbol.java b/src/main/java/net/sourceforge/zbar/Symbol.java new file mode 100644 index 00000000..254b100a --- /dev/null +++ b/src/main/java/net/sourceforge/zbar/Symbol.java @@ -0,0 +1,265 @@ +/*------------------------------------------------------------------------ + * Symbol + * + * Copyright 2007-2010 (c) Jeff Brown + * + * This file is part of the ZBar Bar Code Reader. + * + * The ZBar Bar Code Reader is free software; you can redistribute it + * and/or modify it under the terms of the GNU Lesser Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * The ZBar Bar Code Reader is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser Public License for more details. + * + * You should have received a copy of the GNU Lesser Public License + * along with the ZBar Bar Code Reader; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301 USA + * + * http://sourceforge.net/projects/zbar + *------------------------------------------------------------------------*/ + +package net.sourceforge.zbar; + +import java.io.Closeable; + +/** + * Immutable container for decoded result symbols associated with an image + * or a composite symbol. + */ +public class Symbol implements Closeable { + /** + * No symbol decoded. + */ + public static final int NONE = 0; + /** + * Symbol detected but not decoded. + */ + public static final int PARTIAL = 1; + + /** + * EAN-8. + */ + public static final int EAN8 = 8; + /** + * UPC-E. + */ + public static final int UPCE = 9; + /** + * ISBN-10 (from EAN-13). + */ + public static final int ISBN10 = 10; + /** + * UPC-A. + */ + public static final int UPCA = 12; + /** + * EAN-13. + */ + public static final int EAN13 = 13; + /** + * ISBN-13 (from EAN-13). + */ + public static final int ISBN13 = 14; + /** + * Interleaved 2 of 5. + */ + public static final int I25 = 25; + /** + * DataBar (RSS-14). + */ + public static final int DATABAR = 34; + /** + * DataBar Expanded. + */ + public static final int DATABAR_EXP = 35; + /** + * Codabar. + */ + public static final int CODABAR = 38; + /** + * Code 39. + */ + public static final int CODE39 = 39; + /** + * PDF417. + */ + public static final int PDF417 = 57; + /** + * QR Code. + */ + public static final int QRCODE = 64; + /** + * Code 93. + */ + public static final int CODE93 = 93; + /** + * Code 128. + */ + public static final int CODE128 = 128; + + static { + init(); + } + + /** + * C pointer to a zbar_symbol_t. + */ + private long peer; + /** + * Cached attributes. + */ + private int type; + + /** + * Symbols are only created by other package methods. + */ + Symbol(long peer) { + this.peer = peer; + } + + private static native void init(); + + public void close() { + destroy(); + } + + /** + * Clean up native data associated with an instance. + */ + public synchronized void destroy() { + if(peer != 0) { + destroy(peer); + peer = 0; + } + } + + /** + * Release the associated peer instance. + */ + private native void destroy(long peer); + + /** + * Retrieve type of decoded symbol. + */ + public int getType() { + if(type == 0) { + type = getType(peer); + } + return (type); + } + + private native int getType(long peer); + + /** + * Retrieve symbology boolean configs settings used during decode. + */ + public native int getConfigMask(); + + /** + * Retrieve symbology characteristics detected during decode. + */ + public native int getModifierMask(); + + /** + * Retrieve data decoded from symbol as a String. + */ + public native String getData(); + + /** + * Retrieve raw data bytes decoded from symbol. + */ + public native byte[] getDataBytes(); + + /** + * Retrieve a symbol confidence metric. Quality is an unscaled, + * relative quantity: larger values are better than smaller + * values, where "large" and "small" are application dependent. + */ + public native int getQuality(); + + /** + * Retrieve current cache count. When the cache is enabled for + * the image_scanner this provides inter-frame reliability and + * redundancy information for video streams. + * + * @returns < 0 if symbol is still uncertain + * @returns 0 if symbol is newly verified + * @returns > 0 for duplicate symbols + */ + public native int getCount(); + + /** + * Retrieve an approximate, axis-aligned bounding box for the + * symbol. + */ + public int[] getBounds() { + int n = getLocationSize(peer); + if(n <= 0) { + return (null); + } + + int[] bounds = new int[4]; + int xmin = Integer.MAX_VALUE; + int xmax = Integer.MIN_VALUE; + int ymin = Integer.MAX_VALUE; + int ymax = Integer.MIN_VALUE; + + for(int i = 0; i < n; i++) { + int x = getLocationX(peer, i); + if(xmin > x) { + xmin = x; + } + if(xmax < x) { + xmax = x; + } + + int y = getLocationY(peer, i); + if(ymin > y) { + ymin = y; + } + if(ymax < y) { + ymax = y; + } + } + bounds[0] = xmin; + bounds[1] = ymin; + bounds[2] = xmax - xmin; + bounds[3] = ymax - ymin; + return (bounds); + } + + private native int getLocationSize(long peer); + + private native int getLocationX(long peer, int idx); + + private native int getLocationY(long peer, int idx); + + public int[] getLocationPoint(int idx) { + int[] p = new int[2]; + p[0] = getLocationX(peer, idx); + p[1] = getLocationY(peer, idx); + return (p); + } + + /** + * Retrieve general axis-aligned, orientation of decoded + * symbol. + */ + public native int getOrientation(); + + /** + * Retrieve components of a composite result. + */ + public SymbolSet getComponents() { + return (new SymbolSet(getComponents(peer))); + } + + private native long getComponents(long peer); + + native long next(); +} diff --git a/src/main/java/net/sourceforge/zbar/SymbolIterator.java b/src/main/java/net/sourceforge/zbar/SymbolIterator.java new file mode 100644 index 00000000..a359a168 --- /dev/null +++ b/src/main/java/net/sourceforge/zbar/SymbolIterator.java @@ -0,0 +1,75 @@ +/*------------------------------------------------------------------------ + * SymbolIterator + * + * Copyright 2007-2010 (c) Jeff Brown + * + * This file is part of the ZBar Bar Code Reader. + * + * The ZBar Bar Code Reader is free software; you can redistribute it + * and/or modify it under the terms of the GNU Lesser Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * The ZBar Bar Code Reader is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser Public License for more details. + * + * You should have received a copy of the GNU Lesser Public License + * along with the ZBar Bar Code Reader; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301 USA + * + * http://sourceforge.net/projects/zbar + *------------------------------------------------------------------------*/ + +package net.sourceforge.zbar; + +/** + * Iterator over a SymbolSet. + */ +public class SymbolIterator implements java.util.Iterator { + /** + * Next symbol to be returned by the iterator. + */ + private Symbol current; + + /** + * SymbolIterators are only created by internal interface methods. + */ + SymbolIterator(Symbol first) { + current = first; + } + + /** + * Returns true if the iteration has more elements. + */ + public boolean hasNext() { + return (current != null); + } + + /** + * Retrieves the next element in the iteration. + */ + public Symbol next() { + if(current == null) { + throw (new java.util.NoSuchElementException("access past end of SymbolIterator")); + } + + Symbol result = current; + long sym = current.next(); + if(sym != 0) { + current = new Symbol(sym); + } else { + current = null; + } + return (result); + } + + /** + * Raises UnsupportedOperationException. + */ + public void remove() { + throw (new UnsupportedOperationException("SymbolIterator is immutable")); + } +} diff --git a/src/main/java/net/sourceforge/zbar/SymbolSet.java b/src/main/java/net/sourceforge/zbar/SymbolSet.java new file mode 100644 index 00000000..62313d7b --- /dev/null +++ b/src/main/java/net/sourceforge/zbar/SymbolSet.java @@ -0,0 +1,93 @@ +/*------------------------------------------------------------------------ + * SymbolSet + * + * Copyright 2007-2010 (c) Jeff Brown + * + * This file is part of the ZBar Bar Code Reader. + * + * The ZBar Bar Code Reader is free software; you can redistribute it + * and/or modify it under the terms of the GNU Lesser Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * The ZBar Bar Code Reader is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser Public License for more details. + * + * You should have received a copy of the GNU Lesser Public License + * along with the ZBar Bar Code Reader; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301 USA + * + * http://sourceforge.net/projects/zbar + *------------------------------------------------------------------------*/ + +package net.sourceforge.zbar; + +import java.io.Closeable; + +/** + * Immutable container for decoded result symbols associated with an image + * or a composite symbol. + */ +public class SymbolSet extends java.util.AbstractCollection implements Closeable { + static { + init(); + } + + /** + * C pointer to a zbar_symbol_set_t. + */ + private long peer; + + /** + * SymbolSets are only created by other package methods. + */ + SymbolSet(long peer) { + this.peer = peer; + } + + private static native void init(); + + public void close() { + destroy(); + } + + /** + * Clean up native data associated with an instance. + */ + public synchronized void destroy() { + if(peer != 0) { + destroy(peer); + peer = 0; + } + } + + /** + * Release the associated peer instance. + */ + private native void destroy(long peer); + + /** + * Retrieve an iterator over the Symbol elements in this collection. + */ + public java.util.Iterator iterator() { + long sym = firstSymbol(peer); + if(sym == 0) { + return (new SymbolIterator(null)); + } + + return (new SymbolIterator(new Symbol(sym))); + } + + /** + * Retrieve the number of elements in the collection. + */ + public native int size(); + + /** + * Retrieve C pointer to first symbol in the set. + */ + private native long firstSymbol(long peer); +} diff --git a/src/main/java/net/sourceforge/zbar/ZBar.java b/src/main/java/net/sourceforge/zbar/ZBar.java new file mode 100644 index 00000000..abe136a3 --- /dev/null +++ b/src/main/java/net/sourceforge/zbar/ZBar.java @@ -0,0 +1,136 @@ +package net.sourceforge.zbar; + +import com.sparrowwallet.sparrow.net.NativeUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.*; +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferByte; +import java.util.Iterator; + +public class ZBar { + private static final Logger log = LoggerFactory.getLogger(ZBar.class); + + private final static boolean enabled; + + static { // static initializer + if(com.sparrowwallet.sparrow.io.Config.get().isUseZbar()) { + enabled = loadLibrary(); + } else { + enabled = false; + } + } + + public static boolean isEnabled() { + return enabled; + } + + public static Scan scan(BufferedImage bufferedImage) { + try { + BufferedImage grayscale = new BufferedImage(bufferedImage.getWidth(), bufferedImage.getHeight(), BufferedImage.TYPE_BYTE_GRAY); + Graphics2D g2d = (Graphics2D)grayscale.getGraphics(); + g2d.drawImage(bufferedImage, 0, 0, null); + g2d.dispose(); + + byte[] data = convertToY800(grayscale); + + try(Image image = new Image()) { + image.setSize(grayscale.getWidth(), grayscale.getHeight()); + image.setFormat("Y800"); + image.setData(data); + + try(ImageScanner scanner = new ImageScanner()) { + scanner.setConfig(Symbol.NONE, Config.ENABLE, 0); + scanner.setConfig(Symbol.QRCODE, Config.ENABLE, 1); + int result = scanner.scanImage(image); + if(result != 0) { + try(SymbolSet results = scanner.getResults()) { + Scan scan = null; + for(Iterator iter = results.iterator(); iter.hasNext(); ) { + try(Symbol symbol = iter.next()) { + scan = new Scan(getRawBytes(symbol.getData()), symbol.getData()); + } + } + return scan; + } + } + } + } + } catch(Exception e) { + log.debug("Error scanning with ZBar", e); + } + + return null; + } + + private static byte[] convertToY800(BufferedImage image) { + // Ensure the image is grayscale + if (image.getType() != BufferedImage.TYPE_BYTE_GRAY) { + throw new IllegalArgumentException("Input image must be grayscale"); + } + + // Get the underlying byte array of the image data + byte[] imageData = ((DataBufferByte) image.getRaster().getDataBuffer()).getData(); + + // Check if the image size is even + int width = image.getWidth(); + int height = image.getHeight(); + if (width % 2 != 0 || height % 2 != 0) { + throw new IllegalArgumentException("Image dimensions must be even"); + } + + // Prepare the output byte array in Y800 format + byte[] outputData = new byte[width * height]; + int outputIndex = 0; + + // Convert the grayscale image data to Y800 format + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + int pixel = imageData[y * width + x] & 0xFF; // Extract the grayscale value + + // Write the grayscale value to the output byte array + outputData[outputIndex++] = (byte) pixel; + } + } + + return outputData; + } + + private static boolean loadLibrary() { + try { + String osName = System.getProperty("os.name"); + String osArch = System.getProperty("os.arch"); + if(osName.startsWith("Mac") && osArch.equals("aarch64")) { + NativeUtils.loadLibraryFromJar("/native/osx/aarch64/libzbar.dylib"); + } else if(osName.startsWith("Mac")) { + NativeUtils.loadLibraryFromJar("/native/osx/x64/libzbar.dylib"); + } else if(osName.startsWith("Windows")) { + NativeUtils.loadLibraryFromJar("/native/windows/x64/iconv-2.dll"); + NativeUtils.loadLibraryFromJar("/native/windows/x64/zbar.dll"); + } else if(osArch.equals("aarch64")) { + NativeUtils.loadLibraryFromJar("/native/linux/aarch64/libzbar.so"); + } else { + NativeUtils.loadLibraryFromJar("/native/linux/x64/libzbar.so"); + } + + return true; + } catch(Exception e) { + log.warn("Could not load ZBar native libraries, disabling. " + e.getMessage()); + } + + return false; + } + + private static byte[] getRawBytes(String str) { + char[] chars = str.toCharArray(); + byte[] bytes = new byte[chars.length]; + for(int i = 0; i < chars.length; i++) { + bytes[i] = (byte)(chars[i]); + } + + return bytes; + } + + public record Scan(byte[] rawData, String stringData) {} +} diff --git a/src/main/resources/com/sparrowwallet/sparrow/app.fxml b/src/main/resources/com/sparrowwallet/sparrow/app.fxml index 9917d27c..aa5c1181 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/app.fxml +++ b/src/main/resources/com/sparrowwallet/sparrow/app.fxml @@ -49,6 +49,7 @@ + @@ -123,6 +124,7 @@ + diff --git a/src/main/resources/com/sparrowwallet/sparrow/general.css b/src/main/resources/com/sparrowwallet/sparrow/general.css index 66ac50e6..bdcaa245 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/general.css +++ b/src/main/resources/com/sparrowwallet/sparrow/general.css @@ -320,3 +320,7 @@ CellView > .text-input.text-field { -fx-background-color: -fx-control-inner-background; } +.field-warning { + -fx-text-fill: rgb(238, 210, 2); + -fx-padding: 0 0 0 12; +} diff --git a/src/main/resources/com/sparrowwallet/sparrow/preferences/general.fxml b/src/main/resources/com/sparrowwallet/sparrow/preferences/general.fxml index d837e883..33ba8238 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/preferences/general.fxml +++ b/src/main/resources/com/sparrowwallet/sparrow/preferences/general.fxml @@ -15,6 +15,7 @@ + @@ -60,6 +61,11 @@ +
diff --git a/src/main/resources/com/sparrowwallet/sparrow/transaction/headers.fxml b/src/main/resources/com/sparrowwallet/sparrow/transaction/headers.fxml index c74cbda4..ab07619b 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/transaction/headers.fxml +++ b/src/main/resources/com/sparrowwallet/sparrow/transaction/headers.fxml @@ -287,6 +287,9 @@ + + +