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 @@
+