Compare commits

...
36 Commits
Author SHA1 Message Date
Craig Raw be86b4feaa fix access issue with macos show/hide windowing commands 2025-02-27 11:08:01 +02:00
Craig Raw 37763e9557 verify dropped release file instead of first platform specific release file found 2025-02-27 11:03:53 +02:00
Craig Raw 80c4f4f5f6 make wallet labels export and import scannable 2025-02-26 12:01:05 +02:00
Craig Raw 6c3fe93d1e exclude heights of confirming txes from wallet labels export 2025-02-26 11:44:27 +02:00
Craig Raw 76eff2de48 merge wallet labels optional fields draft implementation 2025-02-26 10:47:17 +02:00
Craig Raw 07a6818823 use default key origin information when importing a descriptor without key origin info 2025-02-25 10:55:37 +02:00
Craig Raw 2253a1bb97 add support for onekey pro and classic 1s hardware wallets 2025-02-20 17:04:56 +02:00
Craig Raw 36ee8add08 add bip47 notification transaction test 2025-02-19 11:31:32 +02:00
Craig Raw 883e75c0df add copy payment code to transaction diagram outputs context menu 2025-02-19 08:45:52 +02:00
Craig Raw cc908b09c7 upgrade to libusb 1.0.27 on all platforms 2025-02-18 15:30:34 +02:00
Craig Raw ce963ed5b6 add specific handling for invalid windows device drivers on trezor devices 2025-02-18 13:32:02 +02:00
Craig Raw 951e33dc06 fix handling of high account numbers on ledger devices 2025-02-18 12:45:50 +02:00
Craig Raw 6a6a6b1cca additionally check for trezor model against internal name, improve exception handling on no match 2025-02-16 08:43:45 +02:00
Craig Raw 8953d404fa fix stripping leading zeros from master fingerprint on importing some trezor models 2025-02-14 18:42:44 +02:00
Thauan AmorimandGitHub b366177782 add show transaction as qr button to signed transaction tab when offline
* [feature/1630] Add QR code button on signed transaction screen

* [feature/1630] Button positioning improvements

* [feature/1630] Added owner to qrDisplayDialog
2025-02-13 09:05:11 +02:00
Craig Raw d0c827c2c7 fix various minor issues around multisig keystore labelling and export button visibility 2025-02-13 08:43:55 +02:00
Craig Raw 5c29bf51b7 handle scanning and pasting server urls in the electrum format 2025-02-11 14:03:43 +02:00
Craig Raw d426703dcc fix account discovery on bitbox02 2025-02-11 13:18:21 +02:00
Craig Raw 78f0721168 bump to v2.1.3 2025-02-08 16:12:38 +02:00
Craig Raw 20d3f07059 draft implementation of optional bip329 fields 2025-02-08 11:43:59 +02:00
Craig Raw 1140a678ad followup 2025-02-08 11:38:41 +02:00
Craig Raw 6e8d44bc8c set script type import button as default after bip39 wallet discovery returns empty 2025-02-08 09:44:21 +02:00
Craig Raw ad3b384feb fix loading hex txn files with trailing whitespace 2025-02-08 09:30:21 +02:00
Craig Raw f38350b38d prefill derivation to default path for script type on watch only keystores 2025-02-07 16:22:20 +02:00
Craig Raw 62060c9839 fix unsigned byte to int conversion for ledger get_more_elements command 2025-02-07 15:56:35 +02:00
Craig Raw 8975f6f666 followup 2025-02-07 14:57:21 +02:00
Craig Raw c7351cd191 indicate historical rates support in exchange source drop-down 2025-02-07 14:52:17 +02:00
Craig Raw 62b1dc3900 support coldcard p2tr address display and show correct address for script type on message sign 2025-02-06 16:12:49 +02:00
Craig Raw f37ff47850 fix invalid claimed length error on transaction file load 2025-02-06 15:36:37 +02:00
Craig Raw cfaa1f6c6e add local network usage description for macos 15 2025-02-06 10:08:28 +02:00
Craig Raw 91c94b94eb upgrade flyway to v9.22.3 2025-02-06 09:37:51 +02:00
Craig Raw a5eb7da067 bump to v2.1.2 2025-02-05 09:21:21 +02:00
Craig Raw 195dbcef3b ensure /sys devices are writeable before calling udevadm 2025-02-04 20:53:50 +02:00
Craig Raw 24955604e2 use standard font in label cells on macos 2025-02-04 20:16:28 +02:00
Craig Raw 0305afbc02 catch and log any linkage errors while enumerating hwws 2025-02-04 19:56:07 +02:00
Craig Raw d4c3c3afa8 bump to v2.1.1 2025-02-04 13:58:47 +02:00
36 changed files with 429 additions and 107 deletions
+4 -2
View File
@@ -5,7 +5,7 @@ plugins {
id 'org.gradlex.extra-java-module-info' version '1.9'
}
def sparrowVersion = '2.1.0'
def sparrowVersion = '2.1.3'
def os = org.gradle.internal.os.OperatingSystem.current()
def osName = os.getFamilyName()
if(os.macOsX) {
@@ -72,7 +72,7 @@ dependencies {
implementation('org.jdbi:jdbi3-sqlobject:3.20.0') {
exclude group: 'org.slf4j'
}
implementation('org.flywaydb:flyway-core:9.1.3')
implementation('org.flywaydb:flyway-core:9.22.3')
implementation('org.fxmisc.richtext:richtextfx:0.10.4')
implementation('no.tornado:tornadofx-controls:1.0.4')
implementation('com.google.zxing:javase:3.4.0') {
@@ -189,6 +189,7 @@ application {
"--add-opens=javafx.graphics/com.sun.javafx.tk=centerdevice.nsmenufx",
"--add-opens=javafx.graphics/com.sun.javafx.tk.quantum=centerdevice.nsmenufx",
"--add-opens=javafx.graphics/com.sun.glass.ui=centerdevice.nsmenufx",
"--add-opens=javafx.graphics/com.sun.glass.ui.mac=centerdevice.nsmenufx",
"--add-opens=javafx.controls/com.sun.javafx.scene.control=centerdevice.nsmenufx",
"--add-opens=javafx.graphics/com.sun.javafx.menu=centerdevice.nsmenufx",
"--add-opens=javafx.graphics/com.sun.glass.ui=com.sparrowwallet.sparrow",
@@ -237,6 +238,7 @@ jlink {
"--add-opens=javafx.graphics/com.sun.javafx.tk=centerdevice.nsmenufx",
"--add-opens=javafx.graphics/com.sun.javafx.tk.quantum=centerdevice.nsmenufx",
"--add-opens=javafx.graphics/com.sun.glass.ui=centerdevice.nsmenufx",
"--add-opens=javafx.graphics/com.sun.glass.ui.mac=centerdevice.nsmenufx",
"--add-opens=javafx.controls/com.sun.javafx.scene.control=centerdevice.nsmenufx",
"--add-opens=javafx.graphics/com.sun.javafx.menu=centerdevice.nsmenufx",
"--add-opens=javafx.graphics/com.sun.glass.ui=com.sparrowwallet.sparrow",
+1 -1
Submodule drongo updated: 342c85a39e...5fd8e9416a
+1 -1
Submodule lark updated: b379338cc2...b41e83a002
@@ -1,5 +1,5 @@
Package: sparrow
Version: 2.1.0-1
Version: 2.1.3-1
Section: utils
Maintainer: Craig Raw <mail@sparrowwallet.com>
Priority: optional
@@ -1,5 +1,5 @@
Package: sparrow
Version: 2.1.0-1
Version: 2.1.3-1
Section: utils
Maintainer: Craig Raw <mail@sparrowwallet.com>
Priority: optional
+1 -1
View File
@@ -31,7 +31,7 @@ case "$1" in
if ! groups "${SUDO_USER:-$(whoami)}" | grep -q plugdev; then
usermod -aG plugdev "${SUDO_USER:-$(whoami)}"
fi
if [ -x /bin/udevadm ]; then
if [ -w /sys/devices ] && [ -w /sys/kernel/uevent_seqnum ] && [ -x /bin/udevadm ]; then
/bin/udevadm control --reload
/bin/udevadm trigger
fi
+2 -2
View File
@@ -1,6 +1,6 @@
Summary: Sparrow
Name: sparrow
Version: 2.1.0
Version: 2.1.3
Release: 1
License: ASL 2.0
Vendor: Unknown
@@ -86,7 +86,7 @@ fi
if ! groups "${SUDO_USER:-$(whoami)}" | grep -q plugdev; then
usermod -aG plugdev "${SUDO_USER:-$(whoami)}"
fi
if [ -x /bin/udevadm ]; then
if [ -w /sys/devices ] && [ -w /sys/kernel/uevent_seqnum ] && [ -x /bin/udevadm ]; then
/bin/udevadm control --reload
/bin/udevadm trigger
fi
+3 -1
View File
@@ -21,7 +21,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>2.1.0</string>
<string>2.1.3</string>
<key>CFBundleSignature</key>
<string>????</string>
<!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories -->
@@ -35,6 +35,8 @@
<string>true</string>
<key>NSCameraUsageDescription</key>
<string>Sparrow requires access to the camera in order to scan QR codes</string>
<key>NSLocalNetworkUsageDescription</key>
<string>Sparrow requires access to the local network in order to connect to your configured server</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
@@ -1,9 +1,7 @@
package com.sparrowwallet.sparrow;
import com.beust.jcommander.JCommander;
import com.google.common.base.Charsets;
import com.google.common.eventbus.Subscribe;
import com.google.common.io.ByteSource;
import com.sparrowwallet.drongo.*;
import com.sparrowwallet.drongo.crypto.*;
import com.sparrowwallet.drongo.policy.PolicyType;
@@ -633,19 +631,10 @@ public class AppController implements Initializable {
byte[] bytes = Files.readAllBytes(file.toPath());
String name = file.getName();
try {
if(Utils.isHex(bytes) || Utils.isBase64(bytes)) {
addTransactionTab(name, file, new String(bytes, StandardCharsets.UTF_8).trim());
} else {
addTransactionTab(name, file, bytes);
} catch(ParseException e) {
ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);
ByteSource byteSource = new ByteSource() {
@Override
public InputStream openStream() {
return inputStream;
}
};
String text = byteSource.asCharSource(Charsets.UTF_8).read().trim();
addTransactionTab(name, file, text);
}
} catch(IOException e) {
showErrorDialog("Error opening file", e.getMessage());
@@ -1378,7 +1367,7 @@ public class AppController implements Initializable {
public void exportWallet(ActionEvent event) {
WalletForm selectedWalletForm = getSelectedWalletForm();
if(selectedWalletForm != null) {
WalletExportDialog dlg = new WalletExportDialog(selectedWalletForm);
WalletExportDialog dlg = new WalletExportDialog(selectedWalletForm, getSelectedWalletForms());
dlg.initOwner(rootStack.getScene().getWindow());
Optional<Wallet> wallet = dlg.showAndWait();
if(wallet.isPresent()) {
@@ -1480,6 +1469,7 @@ public class AppController implements Initializable {
stage.setAlwaysOnTop(true);
stage.setAlwaysOnTop(false);
if(event.getSource() instanceof File file) {
downloadVerifierDialog.setInitialFile(file);
downloadVerifierDialog.setSignatureFile(file);
}
return;
@@ -1123,7 +1123,8 @@ public class AppServices {
public static boolean isWhirlpoolPostmixCompatible(Wallet wallet) {
return WHIRLPOOL_NETWORKS.contains(Network.get())
&& wallet.getScriptType() != ScriptType.P2TR //Taproot not yet supported
&& wallet.getKeystores().size() == 1;
&& wallet.getKeystores().size() == 1
&& wallet.getKeystores().getFirst().getWalletModel() != WalletModel.BITBOX_02; //BitBox02 does not support high account numbers
}
public static List<Wallet> addWhirlpoolWallets(Wallet decryptedWallet, String walletId, Storage storage) {
@@ -18,7 +18,7 @@ import java.util.*;
public class SparrowWallet {
public static final String APP_ID = "sparrow";
public static final String APP_NAME = "Sparrow";
public static final String APP_VERSION = "2.1.0";
public static final String APP_VERSION = "2.1.3";
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";
@@ -62,6 +62,7 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
private static final List<String> ARCHIVE_EXTENSIONS = List.of("zip", "tar.gz", "tar.bz2", "tar.xz", "rar", "7z");
private static final String SPARROW_RELEASE_PREFIX = "sparrow-";
private static final String SPARROW_RELEASE_ALT_PREFIX = "sparrow_";
private static final String SPARROW_SIGNATURE_SUFFIX = "-manifest.txt.asc";
private static final Pattern SPARROW_RELEASE_VERSION = Pattern.compile("[0-9]+(\\.[0-9]+)*");
private static final long MIN_VALID_SPARROW_RELEASE_SIZE = 10 * 1024 * 1024;
@@ -70,6 +71,7 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
private final ObjectProperty<File> manifest = new SimpleObjectProperty<>();
private final ObjectProperty<File> publicKey = new SimpleObjectProperty<>();
private final ObjectProperty<File> release = new SimpleObjectProperty<>();
private final ObjectProperty<File> initial = new SimpleObjectProperty<>();
private final BooleanProperty manifestDisabled = new SimpleBooleanProperty();
private final BooleanProperty publicKeyDisabled = new SimpleBooleanProperty();
@@ -81,7 +83,7 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
private static File lastFileParent;
public DownloadVerifierDialog(File initialSignatureFile) {
public DownloadVerifierDialog(File initialFile) {
final DialogPane dialogPane = getDialogPane();
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
dialogPane.getStylesheets().add(AppServices.class.getResource("dialog.css").toExternalForm());
@@ -223,11 +225,17 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
});
release.addListener((observable, oldValue, releaseFile) -> {
if(releaseFile != null) {
initial.set(null);
}
verify();
});
if(initialSignatureFile != null) {
javafx.application.Platform.runLater(() -> signature.set(initialSignatureFile));
if(initialFile != null) {
javafx.application.Platform.runLater(() -> {
initial.set(initialFile);
signature.set(initialFile);
});
}
}
@@ -455,7 +463,8 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
}
}
if(providedFile.getName().toLowerCase(Locale.ROOT).startsWith(SPARROW_RELEASE_PREFIX)) {
String providedName = providedFile.getName().toLowerCase(Locale.ROOT);
if(providedName.startsWith(SPARROW_RELEASE_PREFIX) || providedName.startsWith(SPARROW_RELEASE_ALT_PREFIX)) {
Matcher matcher = SPARROW_RELEASE_VERSION.matcher(providedFile.getName());
if(matcher.find()) {
String version = matcher.group();
@@ -482,6 +491,15 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
}
private File findReleaseFile(File manifestFile, Map<File, String> manifestMap) {
File initialFile = initial.get();
if(initialFile != null) {
for(File file : manifestMap.keySet()) {
if(initialFile.getName().equals(file.getName())) {
return initialFile;
}
}
}
List<String> releaseExtensions = getReleaseFileExtensions();
List<List<String>> extensionLists = List.of(releaseExtensions, DISK_IMAGE_EXTENSIONS, ARCHIVE_EXTENSIONS, List.of(""));
@@ -565,7 +583,7 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
}
}
if(name.startsWith(SPARROW_RELEASE_PREFIX) && file.length() >= MIN_VALID_SPARROW_RELEASE_SIZE) {
if((name.startsWith(SPARROW_RELEASE_PREFIX) || name.startsWith(SPARROW_RELEASE_ALT_PREFIX)) && file.length() >= MIN_VALID_SPARROW_RELEASE_SIZE) {
Matcher matcher = SPARROW_RELEASE_VERSION.matcher(name);
return matcher.find();
}
@@ -578,6 +596,10 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
signature.set(signatureFile);
}
public void setInitialFile(File initialFile) {
initial.set(initialFile);
}
private static class Header extends GridPane {
public Header() {
setMaxWidth(Double.MAX_VALUE);
@@ -825,7 +825,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
confirmationsListener.getConfirmationsProperty().unbind();
}
}
if(OsType.getCurrent() == OsType.MACOS && transactionEntry.getBlockTransaction().getHeight() > 0) {
if(OsType.getCurrent() == OsType.MACOS && transactionEntry.getBlockTransaction().getHeight() > 0 && !cell.getStyleClass().contains("label-cell")) {
cell.getStyleClass().add("number-field");
}
} else if(entry instanceof NodeEntry) {
@@ -835,7 +835,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
if(!utxoEntry.isSpendable()) {
cell.getStyleClass().add("unspendable");
}
if(OsType.getCurrent() == OsType.MACOS && utxoEntry.getHashIndex().getHeight() > 0 && !addressCell) {
if(OsType.getCurrent() == OsType.MACOS && utxoEntry.getHashIndex().getHeight() > 0 && !addressCell && !cell.getStyleClass().contains("label-cell")) {
cell.getStyleClass().add("number-field");
}
} else if(entry instanceof HashIndexEntry hashIndexEntry) {
@@ -168,7 +168,7 @@ public class FileWalletExportPane extends TitledDescriptionPane {
qrDisplayDialog = new QRDisplayDialog(RegistryType.BYTES.toString(), outputStream.toByteArray(), true);
} else if(exporter instanceof PassportMultisig || exporter instanceof KeystoneMultisig || exporter instanceof JadeMultisig) {
qrDisplayDialog = new QRDisplayDialog(RegistryType.BYTES.toString(), outputStream.toByteArray(), false);
} else if(exporter instanceof Bip129) {
} else if(exporter instanceof Bip129 || exporter instanceof WalletLabels) {
UR ur = UR.fromBytes(outputStream.toByteArray());
BBQR bbqr = new BBQR(BBQRType.UNICODE, outputStream.toByteArray());
qrDisplayDialog = new QRDisplayDialog(ur, bbqr, false, false, false);
@@ -55,7 +55,7 @@ public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane {
protected List<Node> createRightButtons() {
discoverButton = new Button("Discover Wallet");
discoverButton.setDisable(true);
discoverButton.setDefaultButton(true);
discoverButton.setDefaultButton(AppServices.onlineProperty().get());
discoverButton.managedProperty().bind(discoverButton.visibleProperty());
discoverButton.setOnAction(event -> {
discoverWallet();
@@ -66,6 +66,7 @@ public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane {
importButton = new Button("Import Wallet");
importButton.setDisable(true);
importButton.setDefaultButton(!AppServices.onlineProperty().get());
importButton.managedProperty().bind(importButton.visibleProperty());
importButton.visibleProperty().bind(discoverButton.visibleProperty().not());
importButton.setOnAction(event -> {
@@ -196,6 +197,7 @@ public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane {
HBox.setHgrow(region, Priority.SOMETIMES);
Button importMnemonicButton = new Button("Import");
importMnemonicButton.setDefaultButton(true);
importMnemonicButton.setOnAction(event -> {
showHideLink.setVisible(true);
setExpanded(false);
@@ -3,6 +3,7 @@ package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.OsType;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.bip47.PaymentCode;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.protocol.TransactionOutput;
import com.sparrowwallet.drongo.uri.BitcoinURI;
@@ -702,7 +703,9 @@ public class TransactionDiagram extends GridPane {
paymentBox.getChildren().addAll(region, amountLabel);
}
outputNodes.add(new OutputNode(paymentBox, payment.getAddress(), payment.getAmount()));
Wallet bip47Wallet = toWallet != null && toWallet.isBip47() ? toWallet : (toBip47Wallet != null && toBip47Wallet.isBip47() ? toBip47Wallet : null);
PaymentCode paymentCode = bip47Wallet == null ? null : bip47Wallet.getKeystores().getFirst().getExternalPaymentCode();
outputNodes.add(new OutputNode(paymentBox, payment.getAddress(), payment.getAmount(), paymentCode));
}
Set<Integer> seenIndexes = new HashSet<>();
@@ -766,7 +769,7 @@ public class TransactionDiagram extends GridPane {
outputsBox.getChildren().add(outputNode.outputLabel);
outputsBox.getChildren().add(createSpacer());
ContextMenu contextMenu = new LabelContextMenu(outputNode.address, outputNode.amount);
ContextMenu contextMenu = new LabelContextMenu(outputNode.address, outputNode.amount, outputNode.paymentCode);
if(!outputNode.outputLabel.getChildren().isEmpty() && outputNode.outputLabel.getChildren().get(0) instanceof Label outputLabelControl) {
outputLabelControl.setContextMenu(contextMenu);
}
@@ -1073,16 +1076,26 @@ public class TransactionDiagram extends GridPane {
public Pane outputLabel;
public Address address;
public long amount;
public PaymentCode paymentCode;
public OutputNode(Pane outputLabel, Address address, long amount) {
this(outputLabel, address, amount, null);
}
public OutputNode(Pane outputLabel, Address address, long amount, PaymentCode paymentCode) {
this.outputLabel = outputLabel;
this.address = address;
this.amount = amount;
this.paymentCode = paymentCode;
}
}
private class LabelContextMenu extends ContextMenu {
public LabelContextMenu(Address address, long value) {
this(address, value, null);
}
public LabelContextMenu(Address address, long value, PaymentCode paymentCode) {
if(address != null) {
MenuItem copyAddress = new MenuItem("Copy Address");
copyAddress.setOnAction(event -> {
@@ -1119,6 +1132,17 @@ public class TransactionDiagram extends GridPane {
Clipboard.getSystemClipboard().setContent(content);
});
getItems().addAll(copySatsValue, copyBtcValue);
if(paymentCode != null) {
MenuItem copyPaymentCode = new MenuItem("Copy Payment Code");
copyPaymentCode.setOnAction(AE -> {
hide();
ClipboardContent content = new ClipboardContent();
content.putString(paymentCode.toString());
Clipboard.getSystemClipboard().setContent(content);
});
getItems().add(copyPaymentCode);
}
}
}
}
@@ -28,7 +28,8 @@ public class UsbStatusButton extends MenuButton {
for(Device device : devices) {
MenuItem deviceItem = new MenuItem(device.getModel().toDisplayString());
if(!device.isNeedsPinSent() && (device.getModel() == WalletModel.TREZOR_1 || device.getModel() == WalletModel.TREZOR_T || device.getModel() == WalletModel.TREZOR_SAFE_3 ||
device.getModel() == WalletModel.TREZOR_SAFE_5 || device.getModel() == WalletModel.KEEPKEY || device.getModel() == WalletModel.BITBOX_02)) {
device.getModel() == WalletModel.TREZOR_SAFE_5 || device.getModel() == WalletModel.KEEPKEY || device.getModel() == WalletModel.BITBOX_02 ||
device.getModel() == WalletModel.ONEKEY_CLASSIC_1S || device.getModel() == WalletModel.ONEKEY_PRO)) {
deviceItem = new Menu(device.getModel().toDisplayString());
MenuItem toggleItem = new MenuItem("Toggle Passphrase" + (!device.getModel().externalPassphraseEntry() ? "" : (device.isNeedsPassphraseSent() ? " Off" : " On")));
toggleItem.setOnAction(event -> {
@@ -18,8 +18,8 @@ import java.util.List;
public class WalletExportDialog extends Dialog<Wallet> {
private Wallet wallet;
public WalletExportDialog(WalletForm walletForm) {
this.wallet = walletForm.getWallet();
public WalletExportDialog(WalletForm selectedWalletForm, List<WalletForm> allWalletForms) {
this.wallet = selectedWalletForm.getWallet();
EventManager.get().register(this);
setOnCloseRequest(event -> {
@@ -45,10 +45,10 @@ public class WalletExportDialog extends Dialog<Wallet> {
List<WalletExport> exporters;
if(wallet.getPolicyType() == PolicyType.SINGLE) {
exporters = List.of(new Electrum(), new ElectrumPersonalServer(), new Descriptor(), new SpecterDesktop(), new Sparrow(), new WalletLabels(), new WalletTransactions(walletForm));
exporters = List.of(new Electrum(), new ElectrumPersonalServer(), new Descriptor(), new SpecterDesktop(), new Sparrow(), new WalletLabels(allWalletForms), new WalletTransactions(selectedWalletForm));
} else if(wallet.getPolicyType() == PolicyType.MULTI) {
exporters = List.of(new Bip129(), new CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new ElectrumPersonalServer(), new KeystoneMultisig(),
new Descriptor(), new JadeMultisig(), new PassportMultisig(), new SpecterDesktop(), new BlueWalletMultisig(), new SpecterDIY(), new Sparrow(), new WalletLabels(), new WalletTransactions(walletForm));
new Descriptor(), new JadeMultisig(), new PassportMultisig(), new SpecterDesktop(), new BlueWalletMultisig(), new SpecterDIY(), new Sparrow(), new WalletLabels(allWalletForms), new WalletTransactions(selectedWalletForm));
} else {
throw new UnsupportedOperationException("Cannot export wallet with policy type " + wallet.getPolicyType());
}
@@ -1,9 +1,12 @@
package com.sparrowwallet.sparrow.io;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.OutputDescriptor;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletModel;
import com.sparrowwallet.sparrow.wallet.KeystoreController;
import java.io.*;
import java.nio.charset.StandardCharsets;
@@ -92,7 +95,7 @@ public class Descriptor implements WalletImport, WalletExport {
InputStream secondClone = new ByteArrayInputStream(baos.toByteArray());
try {
return PdfUtils.getOutputDescriptor(firstClone).toWallet();
return ensureKeyDerivations(PdfUtils.getOutputDescriptor(firstClone).toWallet());
} catch(Exception e) {
//ignore
}
@@ -100,7 +103,7 @@ public class Descriptor implements WalletImport, WalletExport {
List<String> paragraphs = getParagraphs(secondClone);
for(String paragraph : paragraphs) {
OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor(paragraph);
return descriptor.toWallet();
return ensureKeyDerivations(descriptor.toWallet());
}
throw new ImportException("Could not find an output descriptor in the file");
@@ -116,7 +119,7 @@ public class Descriptor implements WalletImport, WalletExport {
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
for(String line : reader.lines().map(String::trim).toArray(String[]::new)) {
if(line.isEmpty()) {
if(paragraph.length() > 0) {
if(!paragraph.isEmpty()) {
paragraphs.add(paragraph.toString());
paragraph.setLength(0);
}
@@ -127,13 +130,23 @@ public class Descriptor implements WalletImport, WalletExport {
}
}
if(paragraph.length() > 0) {
if(!paragraph.isEmpty()) {
paragraphs.add(paragraph.toString());
}
return paragraphs;
}
private static Wallet ensureKeyDerivations(Wallet wallet) {
for(Keystore keystore : wallet.getKeystores()) {
if(keystore.getKeyDerivation().getMasterFingerprint() == null || keystore.getKeyDerivation().getDerivationPath() == null) {
keystore.setKeyDerivation(new KeyDerivation(KeystoreController.DEFAULT_WATCH_ONLY_FINGERPRINT, wallet.getScriptType().getDefaultDerivationPath()));
}
}
return wallet;
}
@Override
public boolean isWalletImportScannable() {
return true;
@@ -54,9 +54,9 @@ public class Hwi {
Lark lark = getLark(passphrase);
isPromptActive = true;
return lark.enumerate().stream().map(Device::fromHardwareClient).toList();
} catch(Exception e) {
} catch(Throwable e) {
log.error("Error enumerating USB devices", e);
throw new ImportException("Error scanning" + (e.getMessage() == null || e.getMessage().isEmpty() ? ", check devices are ready" : ": " + e.getMessage()), e);
throw new ImportException(e.getMessage() == null || e.getMessage().isEmpty() ? "Error scanning, check devices are ready" : e.getMessage(), e);
} finally {
isPromptActive = false;
}
@@ -1,33 +1,41 @@
package com.sparrowwallet.sparrow.io;
import com.csvreader.CsvReader;
import com.google.gson.Gson;
import com.google.gson.*;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.OutputDescriptor;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.KeystoreLabelsChangedEvent;
import com.sparrowwallet.sparrow.event.WalletEntryLabelsChangedEvent;
import com.sparrowwallet.sparrow.event.WalletUtxoStatusChangedEvent;
import com.sparrowwallet.sparrow.net.ExchangeSource;
import com.sparrowwallet.sparrow.wallet.*;
import org.apache.commons.lang3.time.DateUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.nio.charset.StandardCharsets;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class WalletLabels implements WalletImport, WalletExport {
private static final Logger log = LoggerFactory.getLogger(WalletLabels.class);
private static final long ONE_DAY = 24*60*60*1000L;
private final List<WalletForm> walletForms;
public WalletLabels() {
this.walletForms = Collections.emptyList();
}
public WalletLabels(List<WalletForm> walletForms) {
this.walletForms = walletForms;
}
@@ -50,8 +58,9 @@ public class WalletLabels implements WalletImport, WalletExport {
@Override
public void exportWallet(Wallet wallet, OutputStream outputStream, String password) throws ExportException {
List<Label> labels = new ArrayList<>();
List<Wallet> allWallets = wallet.isMasterWallet() ? wallet.getAllWallets() : wallet.getMasterWallet().getAllWallets();
for(Wallet exportWallet : allWallets) {
Map<Date, Double> fiatRates = getFiatRates(walletForms);
for(WalletForm exportWalletForm : walletForms) {
Wallet exportWallet = exportWalletForm.getWallet();
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(exportWallet);
String origin = outputDescriptor.toString(true, false, false);
@@ -61,34 +70,43 @@ public class WalletLabels implements WalletImport, WalletExport {
}
}
for(BlockTransaction blkTx : exportWallet.getWalletTransactions().values()) {
if(blkTx.getLabel() != null && !blkTx.getLabel().isEmpty()) {
labels.add(new Label(Type.tx, blkTx.getHashAsString(), blkTx.getLabel(), origin, null));
Set<Sha256Hash> confirmingTxs = new HashSet<>();
WalletTransactionsEntry walletTransactionsEntry = exportWalletForm.getWalletTransactionsEntry();
for(Entry entry : walletTransactionsEntry.getChildren()) {
TransactionEntry txEntry = (TransactionEntry)entry;
BlockTransaction blkTx = txEntry.getBlockTransaction();
labels.add(new TransactionLabel(blkTx.getHashAsString(), blkTx.getLabel(), origin,
txEntry.isConfirming() ? null : blkTx.getHeight(), blkTx.getDate(),
blkTx.getFee() == null || blkTx.getFee() == 0 ? null : blkTx.getFee(), txEntry.getValue(),
getFiatValue(blkTx.getDate(), Transaction.SATOSHIS_PER_BITCOIN, fiatRates)));
if(txEntry.isConfirming()) {
confirmingTxs.add(blkTx.getHash());
}
}
for(WalletNode addressNode : exportWallet.getWalletAddresses().values()) {
if(addressNode.getLabel() != null && !addressNode.getLabel().isEmpty()) {
labels.add(new Label(Type.addr, addressNode.getAddress().toString(), addressNode.getLabel(), null, null));
}
labels.add(new AddressLabel(addressNode.getAddress().toString(), addressNode.getLabel(), origin, addressNode.getDerivationPath().substring(1),
addressNode.getTransactionOutputs().stream().flatMap(txo -> txo.isSpent() ? Stream.of(txo, txo.getSpentBy()) : Stream.of(txo))
.filter(ref -> !confirmingTxs.contains(ref.getHash())).map(BlockTransactionHash::getHeight).toList()));
}
for(BlockTransactionHashIndex txo : exportWallet.getWalletTxos().keySet()) {
String spendable = (txo.isSpent() ? null : txo.getStatus() == Status.FROZEN ? "false" : "true");
if(txo.getLabel() != null && !txo.getLabel().isEmpty()) {
labels.add(new Label(Type.output, txo.toString(), txo.getLabel(), null, spendable));
} else if(!txo.isSpent()) {
labels.add(new Label(Type.output, txo.toString(), null, null, spendable));
}
for(Map.Entry<BlockTransactionHashIndex, WalletNode> txoEntry : exportWallet.getWalletTxos().entrySet()) {
BlockTransactionHashIndex txo = txoEntry.getKey();
WalletNode addressNode = txoEntry.getValue();
Boolean spendable = (txo.isSpent() ? null : txo.getStatus() != Status.FROZEN);
labels.add(new InputOutputLabel(Type.output, txo.toString(), txo.getLabel(), origin, spendable, addressNode.getDerivationPath().substring(1), txo.getValue(),
confirmingTxs.contains(txo.getHash()) ? null : txo.getHeight(), txo.getDate(), getFiatValue(txo, fiatRates)));
if(txo.isSpent() && txo.getSpentBy().getLabel() != null && !txo.getSpentBy().getLabel().isEmpty()) {
labels.add(new Label(Type.input, txo.getSpentBy().toString(), txo.getSpentBy().getLabel(), null, null));
if(txo.isSpent()) {
BlockTransactionHashIndex txi = txo.getSpentBy();
labels.add(new InputOutputLabel(Type.input, txi.toString(), txi.getLabel(), origin, null, addressNode.getDerivationPath().substring(1), txi.getValue(),
confirmingTxs.contains(txi.getHash()) ? null : txi.getHeight(), txi.getDate(), getFiatValue(txi, fiatRates)));
}
}
}
try {
Gson gson = new Gson();
Gson gson = new GsonBuilder().registerTypeAdapter(Date.class, new GsonUTCDateAdapter()).create();
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
for(Label label : labels) {
@@ -114,7 +132,7 @@ public class WalletLabels implements WalletImport, WalletExport {
@Override
public boolean isWalletExportScannable() {
return false;
return true;
}
@Override
@@ -247,11 +265,11 @@ public class WalletLabels implements WalletImport, WalletExport {
addChangedEntry(changedWalletEntries, txioEntry);
}
if(label.type == Type.output && !reference.isSpent()) {
if("false".equalsIgnoreCase(label.spendable) && reference.getStatus() != Status.FROZEN) {
if(label.type == Type.output && !reference.isSpent() && label.spendable != null) {
if(!label.spendable && reference.getStatus() != Status.FROZEN) {
reference.setStatus(Status.FROZEN);
addChangedUtxo(changedWalletUtxoStatuses, txioEntry);
} else if("true".equalsIgnoreCase(label.spendable) && reference.getStatus() == Status.FROZEN) {
} else if(label.spendable && reference.getStatus() == Status.FROZEN) {
reference.setStatus(null);
addChangedUtxo(changedWalletUtxoStatuses, txioEntry);
}
@@ -316,7 +334,7 @@ public class WalletLabels implements WalletImport, WalletExport {
@Override
public boolean isWalletImportScannable() {
return false;
return true;
}
@Override
@@ -324,12 +342,77 @@ public class WalletLabels implements WalletImport, WalletExport {
return true;
}
private Map<Date, Double> getFiatRates(List<WalletForm> walletForms) {
ExchangeSource exchangeSource = getExchangeSource();
Currency fiatCurrency = getFiatCurrency();
Map<Date, Double> fiatRates = new HashMap<>();
if(fiatCurrency != null) {
long min = Long.MAX_VALUE;
long max = Long.MIN_VALUE;
for(WalletForm walletForm : walletForms) {
WalletTransactionsEntry walletTransactionsEntry = walletForm.getWalletTransactionsEntry();
if(!walletTransactionsEntry.getChildren().isEmpty()) {
LongSummaryStatistics stats = walletTransactionsEntry.getChildren().stream()
.map(entry -> ((TransactionEntry)entry).getBlockTransaction().getDate())
.filter(Objects::nonNull)
.collect(Collectors.summarizingLong(Date::getTime));
min = Math.min(min, stats.getMin());
max = Math.max(max, stats.getMax());
}
}
if(max > min) {
fiatRates = exchangeSource.getHistoricalExchangeRates(fiatCurrency, new Date(min - ONE_DAY), new Date(max));
}
}
return fiatRates;
}
private static ExchangeSource getExchangeSource() {
return Config.get().getExchangeSource() == null ? ExchangeSource.COINGECKO : Config.get().getExchangeSource();
}
private static Currency getFiatCurrency() {
return getExchangeSource() == ExchangeSource.NONE || !AppServices.onlineProperty().get() ? null : Config.get().getFiatCurrency();
}
private Map<Currency, BigDecimal> getFiatValue(TransactionEntry txEntry, Map<Date, Double> fiatRates) {
return getFiatValue(txEntry.getBlockTransaction().getDate(), txEntry.getValue(), fiatRates);
}
private Map<Currency, BigDecimal> getFiatValue(BlockTransactionHashIndex ref, Map<Date, Double> fiatRates) {
return getFiatValue(ref.getDate(), ref.getValue(), fiatRates);
}
private Map<Currency, BigDecimal> getFiatValue(Date date, long value, Map<Date, Double> fiatRates) {
Currency fiatCurrency = getFiatCurrency();
if(fiatCurrency != null) {
Double dayRate = null;
if(date == null) {
if(AppServices.getFiatCurrencyExchangeRate() != null) {
dayRate = AppServices.getFiatCurrencyExchangeRate().getBtcRate();
}
} else {
dayRate = fiatRates.get(DateUtils.truncate(date, Calendar.DAY_OF_MONTH));
}
if(dayRate != null) {
BigDecimal fiatValue = BigDecimal.valueOf(dayRate * value / Transaction.SATOSHIS_PER_BITCOIN);
return Map.of(fiatCurrency, fiatValue.setScale(fiatCurrency.getDefaultFractionDigits(), RoundingMode.HALF_UP));
}
}
return null;
}
private enum Type {
tx, addr, pubkey, input, output, xpub
}
private static class Label {
public Label(Type type, String ref, String label, String origin, String spendable) {
public Label(Type type, String ref, String label, String origin, Boolean spendable) {
this.type = type;
this.ref = ref;
this.label = label;
@@ -341,6 +424,74 @@ public class WalletLabels implements WalletImport, WalletExport {
String ref;
String label;
String origin;
String spendable;
Boolean spendable;
}
private static class TransactionLabel extends Label {
public TransactionLabel(String ref, String label, String origin, Integer height, Date time, Long fee, Long value, Map<Currency, BigDecimal> rate) {
super(Type.tx, ref, label, origin, null);
this.height = height;
this.time = time;
this.fee = fee;
this.value = value;
this.rate = rate;
}
Integer height;
Date time;
Long fee;
Long value;
Map<Currency, BigDecimal> rate;
}
private static class AddressLabel extends Label {
public AddressLabel(String ref, String label, String origin, String keypath, List<Integer> heights) {
super(Type.addr, ref, label, origin, null);
this.keypath = keypath;
this.heights = heights;
}
String keypath;
List<Integer> heights;
}
private static class InputOutputLabel extends Label {
public InputOutputLabel(Type type, String ref, String label, String origin, Boolean spendable, String keypath, Long value, Integer height, Date time, Map<Currency, BigDecimal> fmv) {
super(type, ref, label, origin, spendable);
this.keypath = keypath;
this.value = value;
this.height = height;
this.time = time;
this.fmv = fmv;
}
String keypath;
Long value;
Integer height;
Date time;
Map<Currency, BigDecimal> fmv;
}
public static class GsonUTCDateAdapter implements JsonSerializer<Date>, JsonDeserializer<Date> {
private final DateFormat dateFormat;
public GsonUTCDateAdapter() {
dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
}
@Override
public JsonElement serialize(Date src, java.lang.reflect.Type typeOfSrc, JsonSerializationContext context) {
return new JsonPrimitive(dateFormat.format(src));
}
@Override
public Date deserialize(JsonElement json, java.lang.reflect.Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
try {
return dateFormat.parse(json.getAsString());
} catch (ParseException e) {
throw new JsonParseException(e);
}
}
}
}
@@ -3,6 +3,7 @@ package com.sparrowwallet.sparrow.keystoreimport;
import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.Network;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.KeystoreSource;
import com.sparrowwallet.drongo.wallet.Wallet;
@@ -10,6 +11,7 @@ import com.sparrowwallet.drongo.wallet.WalletModel;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.KeystoreImportEvent;
import com.sparrowwallet.sparrow.wallet.KeystoreController;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.*;
import org.controlsfx.tools.Borders;
@@ -20,16 +22,18 @@ import java.util.List;
public class KeystoreImportDialog extends Dialog<Keystore> {
private final KeystoreImportController keystoreImportController;
private Keystore keystore;
private final ScriptType scriptType;
private final String existingLabel;
public KeystoreImportDialog(Wallet wallet) {
this(wallet, KeystoreSource.HW_USB);
}
public KeystoreImportDialog(Wallet wallet, KeystoreSource initialSource) {
this(wallet, initialSource, null, null, false);
this(wallet, initialSource, null, null, Keystore.DEFAULT_LABEL, false);
}
public KeystoreImportDialog(Wallet wallet, KeystoreSource initialSource, KeyDerivation currentDerivation, WalletModel currentModel, boolean restrictImport) {
public KeystoreImportDialog(Wallet wallet, KeystoreSource initialSource, KeyDerivation currentDerivation, WalletModel currentModel, String currentLabel, boolean restrictImport) {
EventManager.get().register(this);
setOnCloseRequest(event -> {
EventManager.get().unregister(this);
@@ -37,6 +41,8 @@ public class KeystoreImportDialog extends Dialog<Keystore> {
final DialogPane dialogPane = getDialogPane();
AppServices.setStageIcon(dialogPane.getScene().getWindow());
scriptType = wallet.getScriptType();
existingLabel = currentLabel;
try {
FXMLLoader ksiLoader = new FXMLLoader(AppServices.class.getResource("keystoreimport/keystoreimport.fxml"));
@@ -73,9 +79,10 @@ public class KeystoreImportDialog extends Dialog<Keystore> {
private Keystore getWatchOnlyKeystore() {
this.keystore = new Keystore();
keystore.setLabel(existingLabel);
keystore.setSource(KeystoreSource.SW_WATCH);
keystore.setWalletModel(WalletModel.SPARROW);
keystore.setKeyDerivation(new KeyDerivation("",""));
keystore.setKeyDerivation(new KeyDerivation(KeystoreController.DEFAULT_WATCH_ONLY_FINGERPRINT, scriptType.getDefaultDerivationPath()));
return keystore;
}
@@ -21,7 +21,7 @@ import java.util.*;
import java.util.stream.Collectors;
public enum ExchangeSource {
NONE("None") {
NONE("None", null) {
@Override
public List<Currency> getSupportedCurrencies() {
return Collections.emptyList();
@@ -37,7 +37,7 @@ public enum ExchangeSource {
return Collections.emptyMap();
}
},
COINBASE("Coinbase") {
COINBASE("Coinbase", "No historical rates") {
@Override
public List<Currency> getSupportedCurrencies() {
return getRates().data.rates.keySet().stream().filter(code -> isValidISO4217Code(code.toUpperCase(Locale.ROOT)))
@@ -121,7 +121,7 @@ public enum ExchangeSource {
return historicalRates;
}
},
COINGECKO("Coingecko") {
COINGECKO("Coingecko", "No historical rates") {
@Override
public List<Currency> getSupportedCurrencies() {
return getRates().rates.entrySet().stream().filter(rate -> "fiat".equals(rate.getValue().type) && isValidISO4217Code(rate.getKey().toUpperCase(Locale.ROOT)))
@@ -189,7 +189,7 @@ public enum ExchangeSource {
return historicalRates;
}
},
MEMPOOL_SPACE("mempool.space") {
MEMPOOL_SPACE("mempool.space", "Historical rates from Apr 2023") {
@Override
public List<Currency> getSupportedCurrencies() {
return getRates().rates.entrySet().stream().filter(price -> isValidISO4217Code(price.getKey().toUpperCase(Locale.ROOT)))
@@ -262,9 +262,11 @@ public enum ExchangeSource {
private static final Logger log = LoggerFactory.getLogger(ExchangeSource.class);
private final String name;
private final String description;
ExchangeSource(String name) {
ExchangeSource(String name, String description) {
this.name = name;
this.description = description;
}
public abstract List<Currency> getSupportedCurrencies();
@@ -282,6 +284,14 @@ public enum ExchangeSource {
}
}
public String getName() {
return name;
}
public String getDescription() {
return description;
}
@Override
public String toString() {
return name;
@@ -129,7 +129,7 @@ public enum Protocol {
public abstract CloseableTransport getTransport(HostAndPort server, File serverCert, HostAndPort proxy) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException;
public HostAndPort getServerHostAndPort(String url) {
String lessProtocol = url.substring(this.toUrlString().length());
String lessProtocol = url.endsWith(":t") || url.endsWith(":s") ? url.substring(0, url.length() - 2) : url.substring(this.toUrlString().length());
int pathStart = lessProtocol.indexOf('/');
return HostAndPort.fromString(pathStart < 0 ? lessProtocol : lessProtocol.substring(0, pathStart));
}
@@ -178,10 +178,10 @@ public enum Protocol {
}
public static Protocol getProtocol(String url) {
if(url.startsWith("tcp://")) {
if(url.startsWith("tcp://") || url.endsWith(":t")) {
return TCP;
}
if(url.startsWith("ssl://")) {
if(url.startsWith("ssl://") || url.endsWith(":s")) {
return SSL;
}
if(url.startsWith("http://")) {
@@ -19,6 +19,7 @@ import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.util.StringConverter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -158,6 +159,9 @@ public class GeneralSettingsController extends SettingsDetailController {
config.setExchangeSource(exchangeSource.getValue());
}
exchangeSource.setButtonCell(new ExchangeSourceButtonCell());
exchangeSource.setCellFactory(_ -> new ExchangeSourceListCell());
exchangeSource.valueProperty().addListener((observable, oldValue, source) -> {
config.setExchangeSource(source);
updateCurrencies(source);
@@ -253,4 +257,32 @@ public class GeneralSettingsController extends SettingsDetailController {
fiatCurrency.valueProperty().addListener(fiatCurrencyListener);
}
private static class ExchangeSourceButtonCell extends ListCell<ExchangeSource> {
@Override
protected void updateItem(ExchangeSource exchangeSource, boolean empty) {
super.updateItem(exchangeSource, empty);
if(exchangeSource == null || empty) {
setText("");
} else {
setText(exchangeSource.getName());
}
}
}
private static class ExchangeSourceListCell extends ListCell<ExchangeSource> {
@Override
protected void updateItem(ExchangeSource exchangeSource, boolean empty) {
super.updateItem(exchangeSource, empty);
if(exchangeSource == null || empty) {
setText("");
} else {
String text = exchangeSource.getName();
if(exchangeSource.getDescription() != null && !exchangeSource.getDescription().isEmpty()) {
text += " (" + exchangeSource.getDescription() + ")";
}
setText(text);
}
}
}
}
@@ -9,6 +9,7 @@ import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.psbt.PSBTInput;
import com.sparrowwallet.drongo.uri.BitcoinURI;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.hummingbird.UR;
import com.sparrowwallet.hummingbird.registry.CryptoPSBT;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
@@ -227,6 +228,9 @@ public class HeadersController extends TransactionFormController implements Init
@FXML
private Button broadcastButton;
@FXML
private Button showTransactionButton;
@FXML
private Button saveFinalButton;
@@ -461,6 +465,8 @@ public class HeadersController extends TransactionFormController implements Init
broadcastProgressBar.visibleProperty().bind(signaturesProgressBar.visibleProperty().not());
broadcastButton.managedProperty().bind(broadcastButton.visibleProperty());
showTransactionButton.managedProperty().bind(showTransactionButton.visibleProperty());
showTransactionButton.visibleProperty().bind(broadcastButton.visibleProperty().not());
saveFinalButton.managedProperty().bind(saveFinalButton.visibleProperty());
saveFinalButton.visibleProperty().bind(broadcastButton.visibleProperty().not());
broadcastButton.visibleProperty().bind(AppServices.onlineProperty());
@@ -1255,6 +1261,21 @@ public class HeadersController extends TransactionFormController implements Init
broadcastTransactionService.start();
}
public void showTransaction(ActionEvent event) {
try {
Transaction transaction = headersForm.getPsbt().extractTransaction();
byte[] txBytes = transaction.bitcoinSerialize();
UR ur = UR.fromBytes(txBytes);
BBQR bbqr = new BBQR(BBQRType.TXN, txBytes);
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(ur, bbqr, false, false, false);
qrDisplayDialog.initOwner(showTransactionButton.getScene().getWindow());
qrDisplayDialog.showAndWait();
} catch (Exception exception) {
log.error("Error creating UR", exception);
AppServices.showErrorDialog("Error displaying transaction QR code", exception.getMessage());
}
}
public void saveFinalTransaction(ActionEvent event) {
Stage window = new Stage();
@@ -36,10 +36,7 @@ import tornadofx.control.Field;
import javax.smartcardio.CardException;
import java.net.URL;
import java.util.Base64;
import java.util.Locale;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.*;
import java.util.stream.Collectors;
import static com.sparrowwallet.sparrow.io.CardApi.isReaderAvailable;
@@ -256,6 +253,7 @@ public class KeystoreController extends WalletFormController implements Initiali
launchImportDialog(keystoreSource);
} else {
fingerprint.setText(DEFAULT_WATCH_ONLY_FINGERPRINT);
derivation.setText(getWalletForm().getWallet().getScriptType().getDefaultDerivationPath());
selectSourcePane.setVisible(false);
}
}
@@ -370,7 +368,8 @@ public class KeystoreController extends WalletFormController implements Initiali
boolean restrictImport = keystore.getSource() != KeystoreSource.SW_WATCH && keystoreSourceToggleGroup.getToggles().stream().anyMatch(toggle -> ((ToggleButton)toggle).isDisabled());
KeyDerivation currentDerivation = keystore.getKeyDerivation();
WalletModel currentModel = keystore.getWalletModel();
KeystoreImportDialog dlg = new KeystoreImportDialog(getWalletForm().getWallet(), initialSource, currentDerivation, currentModel, restrictImport);
String currentLabel = keystore.getLabel();
KeystoreImportDialog dlg = new KeystoreImportDialog(getWalletForm().getWallet(), initialSource, currentDerivation, currentModel, currentLabel, restrictImport);
dlg.initOwner(selectSourcePane.getScene().getWindow());
Optional<Keystore> result = dlg.showAndWait();
if(result.isPresent()) {
@@ -385,7 +384,12 @@ public class KeystoreController extends WalletFormController implements Initiali
return;
}
}
walletForm.getWallet().makeLabelsUnique(importedKeystore);
if(!keystore.getLabel().equals(importedKeystore.getLabel())) {
List<Keystore> changedKeystores = walletForm.getWallet().makeLabelsUnique(importedKeystore);
if(!changedKeystores.isEmpty()) {
EventManager.get().post(new SettingsChangedEvent(walletForm.getWallet(), SettingsChangedEvent.Type.KEYSTORE_LABEL));
}
}
keystore.setSource(importedKeystore.getSource());
keystore.setWalletModel(importedKeystore.getWalletModel());
keystore.setLabel(importedKeystore.getLabel());
@@ -395,7 +399,7 @@ public class KeystoreController extends WalletFormController implements Initiali
keystore.setSeed(importedKeystore.getSeed());
keystore.setBip47ExtendedPrivateKey(importedKeystore.getBip47ExtendedPrivateKey());
updateType(true);
updateType(keystore.isValid());
label.setText(keystore.getLabel());
fingerprint.setText(keystore.getKeyDerivation().getMasterFingerprint());
derivation.setText(keystore.getKeyDerivation().getDerivationPath());
@@ -652,15 +656,31 @@ public class KeystoreController extends WalletFormController implements Initiali
@Subscribe
public void update(SettingsChangedEvent event) {
if(walletForm.getWallet().equals(event.getWallet()) && event.getType().equals(SettingsChangedEvent.Type.SCRIPT_TYPE)) {
derivation.setPromptText(event.getWallet().getScriptType().getDefaultDerivationPath());
if(derivation.getText() != null && !derivation.getText().isEmpty()) {
String derivationPath = derivation.getText();
derivation.setText(derivationPath + " ");
derivation.setText(derivationPath);
if(walletForm.getWallet().equals(event.getWallet())) {
if(event.getType().equals(SettingsChangedEvent.Type.SCRIPT_TYPE)) {
if(keystore.getSource() == KeystoreSource.SW_WATCH && derivation.getPromptText().equals(derivation.getText())) {
derivation.setText(event.getWallet().getScriptType().getDefaultDerivationPath());
}
derivation.setPromptText(event.getWallet().getScriptType().getDefaultDerivationPath());
if(derivation.getText() != null && !derivation.getText().isEmpty()) {
String derivationPath = derivation.getText();
derivation.setText(derivationPath + " ");
derivation.setText(derivationPath);
}
if(keystore.getExtendedPublicKey() != null) {
setXpubContext(keystore.getExtendedPublicKey());
}
} else if(event.getType().equals(SettingsChangedEvent.Type.KEYSTORE_LABEL)) {
if(!keystore.getLabel().equals(label.getText())) {
label.setText(keystore.getLabel());
}
}
if(keystore.getExtendedPublicKey() != null) {
setXpubContext(keystore.getExtendedPublicKey());
if(event.getType().equals(SettingsChangedEvent.Type.KEYSTORE_LABEL) || event.getType().equals(SettingsChangedEvent.Type.KEYSTORE_FINGERPRINT) ||
event.getType().equals(SettingsChangedEvent.Type.KEYSTORE_DERIVATION) || event.getType().equals(SettingsChangedEvent.Type.KEYSTORE_XPUB)) {
if(keystore.getSource() == KeystoreSource.SW_WATCH) {
exportButton.setVisible(keystore.isValid() && getWalletForm().getWallet().getPolicyType() == PolicyType.MULTI);
}
}
}
}
@@ -591,7 +591,7 @@ public class SettingsController extends WalletFormController implements Initiali
}
if(walletForm instanceof SettingsWalletForm settingsWalletForm) {
WalletExportDialog dlg = new WalletExportDialog(settingsWalletForm.getAppWalletForm());
WalletExportDialog dlg = new WalletExportDialog(settingsWalletForm.getAppWalletForm(), List.of(settingsWalletForm.getAppWalletForm()));
dlg.initOwner(export.getScene().getWindow());
dlg.showAndWait();
} else {
@@ -52,7 +52,7 @@
<ComboBox fx:id="fiatCurrency" />
</Field>
<Field text="Exchange rate source:">
<ComboBox fx:id="exchangeSource">
<ComboBox fx:id="exchangeSource" prefWidth="160">
<items>
<FXCollections fx:factory="observableArrayList">
<ExchangeSource fx:constant="NONE" />
@@ -285,6 +285,11 @@
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="20" icon="SATELLITE_DISH" />
</graphic>
</Button>
<Button fx:id="showTransactionButton" HBox.hgrow="ALWAYS" text="Show Transaction as QR" contentDisplay="TOP" wrapText="true" textAlignment="CENTER" onAction="#showTransaction">
<graphic>
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="20" icon="QRCODE" />
</graphic>
</Button>
<Button fx:id="saveFinalButton" defaultButton="true" HBox.hgrow="ALWAYS" text="Save Final Transaction" contentDisplay="TOP" wrapText="true" textAlignment="CENTER" onAction="#saveFinalTransaction">
<graphic>
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="20" icon="ARROW_DOWN" />
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="15px" height="15px" viewBox="0 0 15 15" version="1.1">
<g id="surface1">
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(92.941177%,92.941177%,92.941177%);fill-opacity:1;" d="M 7.5 12.898438 C 11.226562 12.898438 12.898438 11.226562 12.898438 7.5 C 12.898438 3.773438 11.226562 2.101562 7.5 2.101562 C 3.773438 2.101562 2.101562 3.773438 2.101562 7.5 C 2.101562 11.226562 3.773438 12.898438 7.5 12.898438 Z M 7.5 12.898438 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941177%,12.941177%,12.941177%);fill-opacity:1;" d="M 6.484375 4.390625 L 7.988281 4.390625 L 7.988281 6.867188 L 7.054688 6.867188 L 7.054688 5.1875 L 6.222656 5.1875 Z M 6.484375 4.390625 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941177%,12.941177%,12.941177%);fill-opacity:1;" d="M 7.5 10.609375 C 8.445312 10.609375 9.214844 9.84375 9.214844 8.898438 C 9.214844 7.949219 8.445312 7.183594 7.5 7.183594 C 6.554688 7.183594 5.785156 7.949219 5.785156 8.898438 C 5.785156 9.84375 6.554688 10.609375 7.5 10.609375 Z M 7.5 10.609375 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(92.941177%,92.941177%,92.941177%);fill-opacity:1;" d="M 7.5 9.832031 C 8.015625 9.832031 8.433594 9.414062 8.433594 8.898438 C 8.433594 8.378906 8.015625 7.960938 7.5 7.960938 C 6.984375 7.960938 6.566406 8.378906 6.566406 8.898438 C 6.566406 9.414062 6.984375 9.832031 7.5 9.832031 Z M 7.5 9.832031 "/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+9
View File
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="15px" height="15px" viewBox="0 0 15 15" version="1.1">
<g id="surface1">
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(20%,20%,20%);fill-opacity:1;" d="M 7.5 12.898438 C 11.226562 12.898438 12.898438 11.226562 12.898438 7.5 C 12.898438 3.773438 11.226562 2.101562 7.5 2.101562 C 3.773438 2.101562 2.101562 3.773438 2.101562 7.5 C 2.101562 11.226562 3.773438 12.898438 7.5 12.898438 Z M 7.5 12.898438 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(92.941177%,92.941177%,92.941177%);fill-opacity:1;" d="M 6.484375 4.390625 L 7.988281 4.390625 L 7.988281 6.867188 L 7.054688 6.867188 L 7.054688 5.1875 L 6.222656 5.1875 Z M 6.484375 4.390625 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(92.941177%,92.941177%,92.941177%);fill-opacity:1;" d="M 7.5 10.609375 C 8.445312 10.609375 9.214844 9.84375 9.214844 8.898438 C 9.214844 7.949219 8.445312 7.183594 7.5 7.183594 C 6.554688 7.183594 5.785156 7.949219 5.785156 8.898438 C 5.785156 9.84375 6.554688 10.609375 7.5 10.609375 Z M 7.5 10.609375 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(20%,20%,20%);fill-opacity:1;" d="M 7.5 9.832031 C 8.015625 9.832031 8.433594 9.414062 8.433594 8.898438 C 8.433594 8.378906 8.015625 7.960938 7.5 7.960938 C 6.984375 7.960938 6.566406 8.378906 6.566406 8.898438 C 6.566406 9.414062 6.984375 9.832031 7.5 9.832031 Z M 7.5 9.832031 "/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

+1
View File
@@ -13,6 +13,7 @@
<logger name="org.flywaydb.core.internal.command.DbValidate" level="WARN" />
<logger name="org.flywaydb.core.internal.command.DbMigrate" level="WARN" />
<logger name="org.flywaydb.core.internal.command.DbClean" level="ERROR" />
<logger name="org.flywaydb.core.internal.command.clean.CleanExecutor" level="ERROR" />
<logger name="org.flywaydb.core.internal.database.base.BaseDatabaseType" level="WARN" />
<logger name="org.flywaydb.core.internal.database.base.Schema" level="WARN" />
<logger name="org.flywaydb.core.internal.schemahistory.JdbcTableSchemaHistory" level="WARN" />