Merge branch 'master' into satochip_debug

This commit is contained in:
Toporin 2023-09-06 13:25:08 +01:00
commit cbcb40c973
68 changed files with 1794 additions and 71 deletions

View file

@ -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) {

View file

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

2
drongo

@ -1 +1 @@
Subproject commit 38b04b8e0b802f6cd43b4e88730d4d3ed31227fc
Subproject commit 2b7b650faeeeda7fc25ab0962a6132e6531ced4c

View file

@ -21,7 +21,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.7.8</string>
<string>1.7.10</string>
<key>CFBundleSignature</key>
<string>????</string>
<!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories -->

View file

@ -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<Wallet> emptyLoadingWallets = new LinkedHashSet<>();
private final Map<File, File> renamedWallets = new HashMap<>();
private final ChangeListener<Boolean> 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<ButtonType> 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<List<Payment>> 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<WalletForm> 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<WalletNameDialog.NameAndBirthDate> 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<ButtonType> 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();

View file

@ -53,6 +53,8 @@ public class DefaultInteractionServices implements InteractionServices {
alert.getDialogPane().setPrefHeight(200 + numLines * 20);
}
alert.setResizable(true);
moveToActiveWindowScreen(alert);
return alert.showAndWait();
}

View file

@ -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";

View file

@ -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<Entry> {
private BitcoinUnit bitcoinUnit;
private UnitFormat unitFormat;
private CurrencyRate currencyRate;
public BitcoinUnit getBitcoinUnit() {
return bitcoinUnit;
@ -64,6 +67,18 @@ public class CoinTreeTable extends TreeTableView<Entry> {
}
}
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<Entry> {
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<Entry, ?> column = getColumns().get(columnIndex);
column.setSortType(sortType == null ? TreeTableColumn.SortType.DESCENDING : sortType);
getSortOrder().add(column);
}
}
}

View file

@ -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<ChildNumber> derivation = scriptType.getDefaultDerivation();

View file

@ -100,7 +100,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> 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<Entry, Entry> 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<Entry, Entry> 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<Entry, Entry> implements Confirmati
List<OutputGroup> 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<Entry, Entry> 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<Entry, Entry> 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<Entry, Entry> 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<Entry, Entry> 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<Entry, Entry> 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<Entry, Entry> 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");
}
}
}

View file

@ -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<Entry, Number> {
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);
}
}
}

View file

@ -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;
}

View file

@ -392,7 +392,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
}
}
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) {

View file

@ -502,18 +502,22 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
if(cryptoOutput.getMultiKey() != null) {
MultiKey multiKey = cryptoOutput.getMultiKey();
Map<ExtendedKey, KeyDerivation> extendedPublicKeys = new LinkedHashMap<>();
Map<ExtendedKey, String> 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");

View file

@ -140,6 +140,8 @@ public class SearchWalletDialog extends Dialog<Entry> {
setResizable(true);
AppServices.moveToActiveWindowScreen(this);
Platform.runLater(search::requestFocus);
}

View file

@ -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<Entry, ?> 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) {

View file

@ -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);
}
}

View file

@ -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<TreeTableColumnConfig> columnConfigs;
public TreeTableConfig(CoinTreeTable treeTable) {
columnConfigs = new ArrayList<>();
TreeTableColumn<Entry, ?> sortColumn = treeTable.getSortOrder().isEmpty() ? null : treeTable.getSortOrder().get(0);
for(int i = 0; i < treeTable.getColumns().size(); i++) {
TreeTableColumn<Entry, ?> column = treeTable.getColumns().get(i);
//TODO: Support column widths
columnConfigs.add(new TreeTableColumnConfig(i, null, sortColumn == column ? column.getSortType() : null));
}
}
public TreeTableConfig(List<TreeTableColumnConfig> 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<TreeTableColumnConfig> columnConfigs = new ArrayList<>();
String[] parts = tableConfig.split("\\|");
for(String part : parts) {
columnConfigs.add(TreeTableColumnConfig.fromString(part));
}
return new TreeTableConfig(columnConfigs);
}
}

View file

@ -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<Entry, ?> 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);
}
}

View file

@ -59,7 +59,7 @@ public class WalletImportDialog extends Dialog<Wallet> {
}
}
List<WalletImport> 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<WalletImport> 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));
}

View file

@ -42,9 +42,13 @@ public class WalletNameDialog extends Dialog<WalletNameDialog.NameAndBirthDate>
}
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<WalletNameDialog.NameAndBirthDate>
));
});
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 {

View file

@ -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<Void> {
public WalletSummaryDialog(List<WalletForm> 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<Entry, String> nameColumn = new TreeTableColumn<>("Wallet");
nameColumn.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, String> param) -> {
return new ReadOnlyObjectWrapper<>(param.getValue().getValue().getLabel());
});
nameColumn.setCellFactory(p -> new LabelCell());
table.getColumns().add(nameColumn);
TreeTableColumn<Entry, Number> balanceColumn = new TreeTableColumn<>("Balance");
balanceColumn.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, Number> 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<Entry, Number> fiatColumn = new TreeTableColumn<>(currencyRate.getCurrency().getSymbol());
fiatColumn.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, Number> 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<Entry> rootItem = new TreeItem<>(rootEntry);
for(Entry childEntry : rootEntry.getChildren()) {
TreeItem<Entry> 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<WalletForm> 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;
}
}
}

View file

@ -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<Image> {
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) {

View file

@ -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;
}

View file

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

View file

@ -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;
}
}

View file

@ -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();

View file

@ -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<Void> {
private final Wallet wallet;
private final File newWalletFile;
public CopyWalletService(Wallet wallet, File newWalletFile) {
this.wallet = wallet;
this.newWalletFile = newWalletFile;
}
@Override
protected Task<Void> 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<Boolean> {
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<Boolean> createTask() {
return new Task<>() {
protected Boolean call() {
return storage.delete();
return storage.delete(deleteBackups);
}
};
}

View file

@ -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;
}
}

View file

@ -583,7 +583,7 @@ public class DbPersistence implements Persistence {
@Override
public boolean isClosed() {
return dataSource.isClosed();
return dataSource == null || dataSource.isClosed();
}
@Override

View file

@ -21,6 +21,7 @@ public interface MixConfigDao {
default void addMixConfig(Wallet wallet) {
if(wallet.getMixConfig() != null) {
wallet.getMixConfig().setId(null);
addOrUpdate(wallet, wallet.getMixConfig());
}
}

View file

@ -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);
}
}

View file

@ -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) {

View file

@ -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();
}
};

View file

@ -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<String> 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<String> wallets = getBitcoindService().listWallets();
if(!wallets.contains(CORE_WALLET_NAME)) {
if(!loaded) {
getBitcoindService().loadWallet(CORE_WALLET_NAME, true);
}
}

View file

@ -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> 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()));

View file

@ -26,6 +26,7 @@ import java.util.stream.Collectors;
public class ServerAliasDialog extends Dialog<Server> {
private final ServerType serverType;
private final TableView<ServerEntry> serverTable;
private final Button closeButton;
public ServerAliasDialog(ServerType serverType) {
this.serverType = serverType;
@ -76,6 +77,7 @@ public class ServerAliasDialog extends Dialog<Server> {
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<Server> {
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);
}
}
}
});

View file

@ -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);

View file

@ -192,6 +192,9 @@ public class UtxosDialog extends WalletDialog {
SparrowTerminal.get().getGuiThread().invokeLater(() -> {
TableModel<TableCell> tableModel = getTableModel(walletUtxosEntry);
utxos.setTableModel(tableModel);
if(utxos.getTheme() != null && utxos.getRenderer().getViewTopRow() >= tableModel.getRowCount()) {
utxos.getRenderer().setViewTopRow(0);
}
});
}

View file

@ -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;
}
}

View file

@ -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<ButtonType> 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<PathComponent> 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) {

View file

@ -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());
}
}
}

View file

@ -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<WalletTransactionsEntry.WalletTransaction> entries = getWalletTransactions(getWallet());
Collection<WalletTransactionsEntry.WalletTransaction> entries = getWalletTransactions(getWallet(), false);
Set<Entry> current = entries.stream().map(WalletTransaction::getTransactionEntry).collect(Collectors.toCollection(LinkedHashSet::new));
Set<Entry> previous = new LinkedHashSet<>(getChildren());
@ -101,7 +105,7 @@ public class WalletTransactionsEntry extends Entry {
}
}
private static Collection<WalletTransaction> getWalletTransactions(Wallet wallet) {
private static Collection<WalletTransaction> getWalletTransactions(Wallet wallet, boolean includeAllChildWallets) {
Map<BlockTransaction, WalletTransaction> 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<WalletTransaction> {
private final Wallet wallet;
private final BlockTransaction blockTransaction;

View file

@ -0,0 +1,76 @@
/*------------------------------------------------------------------------
* Config
*
* Copyright 2010 (c) Jeff Brown <spadix@users.sourceforge.net>
*
* 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;
}

View file

@ -0,0 +1,197 @@
/*------------------------------------------------------------------------
* Image
*
* Copyright 2007-2010 (c) Jeff Brown <spadix@users.sourceforge.net>
*
* 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);
}

View file

@ -0,0 +1,110 @@
/*------------------------------------------------------------------------
* ImageScanner
*
* Copyright 2007-2010 (c) Jeff Brown <spadix@users.sourceforge.net>
*
* 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);
}

View file

@ -0,0 +1,44 @@
/*------------------------------------------------------------------------
* Modifier
*
* Copyright 2010 (c) Jeff Brown <spadix@users.sourceforge.net>
*
* 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;
}

View file

@ -0,0 +1,52 @@
/*------------------------------------------------------------------------
* Orientation
*
* Copyright 2010 (c) Jeff Brown <spadix@users.sourceforge.net>
*
* 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;
}

View file

@ -0,0 +1,265 @@
/*------------------------------------------------------------------------
* Symbol
*
* Copyright 2007-2010 (c) Jeff Brown <spadix@users.sourceforge.net>
*
* 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();
}

View file

@ -0,0 +1,75 @@
/*------------------------------------------------------------------------
* SymbolIterator
*
* Copyright 2007-2010 (c) Jeff Brown <spadix@users.sourceforge.net>
*
* 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<Symbol> {
/**
* 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"));
}
}

View file

@ -0,0 +1,93 @@
/*------------------------------------------------------------------------
* SymbolSet
*
* Copyright 2007-2010 (c) Jeff Brown <spadix@users.sourceforge.net>
*
* 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<Symbol> 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<Symbol> 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);
}

View file

@ -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<Symbol> 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) {}
}

View file

@ -49,6 +49,7 @@
<SeparatorMenuItem styleClass="osxHide" />
<MenuItem styleClass="osxHide" mnemonicParsing="false" text="Preferences..." accelerator="Shortcut+P" onAction="#openPreferences"/>
<SeparatorMenuItem />
<MenuItem fx:id="renameWallet" mnemonicParsing="false" text="Rename Wallet..." onAction="#renameWallet"/>
<MenuItem fx:id="deleteWallet" mnemonicParsing="false" text="Delete Wallet..." onAction="#deleteWallet"/>
<MenuItem fx:id="closeTab" mnemonicParsing="false" text="Close Tab" accelerator="Shortcut+W" onAction="#closeTab"/>
<MenuItem styleClass="osxHide" mnemonicParsing="false" text="Quit" accelerator="Shortcut+Q" onAction="#quit"/>
@ -123,6 +124,7 @@
<MenuItem fx:id="lockWallet" mnemonicParsing="false" text="Lock Wallet" accelerator="Shortcut+L" onAction="#lockWallet"/>
<MenuItem fx:id="lockAllWallets" mnemonicParsing="false" text="Lock All Wallets" accelerator="Shortcut+Shift+L" onAction="#lockWallets"/>
<SeparatorMenuItem />
<MenuItem fx:id="showWalletSummary" mnemonicParsing="false" text="Show Wallet Summary" onAction="#showWalletSummary"/>
<MenuItem fx:id="searchWallet" mnemonicParsing="false" text="Search Wallet" accelerator="Shortcut+Shift+S" onAction="#searchWallet"/>
<MenuItem fx:id="refreshWallet" mnemonicParsing="false" text="Refresh Wallet" accelerator="Shortcut+R" onAction="#refreshWallet"/>
</items>

View file

@ -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;
}

View file

@ -15,6 +15,7 @@
<?import com.sparrowwallet.sparrow.control.UnlabeledToggleSwitch?>
<?import com.sparrowwallet.sparrow.control.HelpLabel?>
<?import com.sparrowwallet.sparrow.net.FeeRatesSource?>
<?import org.controlsfx.glyphfont.Glyph?>
<GridPane hgap="10.0" vgap="10.0" stylesheets="@preferences.css, @../general.css" xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.sparrowwallet.sparrow.preferences.GeneralPreferencesController">
<padding>
@ -60,6 +61,11 @@
</FXCollections>
</items>
</ComboBox>
<Label fx:id="currenciesLoadWarning" text="Error retrieving currencies">
<graphic>
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="EXCLAMATION_TRIANGLE" styleClass="field-warning" />
</graphic>
</Label>
</Field>
</Fieldset>
<Fieldset inputGrow="SOMETIMES" text="Wallet" styleClass="wideLabelFieldSet">

View file

@ -287,6 +287,9 @@
<graphic>
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="20" icon="ARROW_DOWN" />
</graphic>
<tooltip>
<Tooltip text="Connect to a server (bottom right toggle) to broadcast a transaction" />
</tooltip>
</Button>
<Button fx:id="payjoinButton" defaultButton="true" HBox.hgrow="ALWAYS" text="Get Payjoin Transaction" contentDisplay="TOP" wrapText="true" textAlignment="CENTER" onAction="#getPayjoinTransaction">
<graphic>

View file

@ -140,4 +140,12 @@
.address-text-field {
-fx-font-size: 13px;
-fx-font-family: 'Roboto Mono';
}
.unconfirmed-row {
-fx-opacity: 0.7;
}
.summary-row {
-fx-font-weight: bold;
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.