Compare commits

..
30 Commits
Author SHA1 Message Date
Craig Raw fb40d991bb replace default textinput dialog with custom textfield dialog 2023-03-01 08:14:25 +02:00
Craig Raw 5fe6a7196a use uppercase to encode pdf output descriptor qr 2023-02-28 16:52:42 +02:00
Craig Raw f06b859c82 extend request timeout for paynym api 2023-02-28 14:37:46 +02:00
Craig Raw 80dd59928e upgrade jlink plugin to support java 19 2023-02-28 14:21:57 +02:00
Craig Raw f22f76464a add option ot disable block explorer 2023-02-28 13:59:45 +02:00
Craig Raw dfe1f16495 configure a block explorer url, and open a txid in the configured block explorer 2023-02-28 13:19:24 +02:00
Craig Raw 90a9030ecb allow crypto-output qr scanning from wallet import dialog 2023-02-27 14:03:12 +02:00
Craig Raw 4ab33a373c reduce log level for no card reader errors 2023-02-27 12:36:41 +02:00
Craig Raw 10e751d6e1 upgrade to hwi 2.2.1 2023-02-27 12:18:50 +02:00
Craig Raw 5f40669af7 request confirmation before enabling use of a bip39 passphrase 2023-02-27 09:11:06 +02:00
Craig Raw 9fc096569a add minfeerate parameter to bip78 payjoin urls 2023-02-23 12:53:12 +02:00
Craig Raw 41636f7152 support encrypted bip129 wallet imports 2023-02-23 12:17:41 +02:00
Craig Raw fc5d48de6f bip129 round 2 support (wallet import and export) 2023-02-23 12:02:06 +02:00
Craig Raw 2a7f14a4ed bip129 round 1 support with optional signing of bsms keystore exports 2023-02-22 10:22:04 +02:00
Craig Raw 1cb6778502 bump required java version to 18 2023-02-20 11:56:34 +02:00
Craig Raw 7f254e763d fix keystore encryption issue when changing the password on a wallet with freshly added accounts 2023-02-17 09:10:40 +02:00
Craig Raw e0ff42b6a4 terminal: add lock menu item to all wallets with a password 2023-02-17 08:37:10 +02:00
craigrawandGitHub 88fc8f5017 Merge pull request #843 from secondl1ght/master
add StartupWMClass to linux .desktop file
2023-02-15 08:00:36 +02:00
secondl1ghtandGitHub d7072928de add StartupWMClass to linux .desktop file 2023-02-14 21:22:41 -07:00
Craig Raw 0cc9ddba05 read and throw hwi error stream if stdout empty 2023-02-14 09:57:23 +02:00
Craig Raw e3799cd0a8 fix error receiving a stowaway to postmix by reverting to master wallet 2023-02-13 18:27:21 +02:00
Craig Raw ea6b30326e fix taproot signature hash for single | anyonecanpay 2023-02-13 17:35:17 +02:00
Craig Raw 38768885e2 show hwi signature verification errors, display strings encoded into scripts 2023-02-13 16:36:33 +02:00
Craig Raw c360177c31 use default ports for bitcoin core if absent, fix ux on changing port for an aliased server 2023-02-12 13:06:21 +02:00
Craig Raw e88ea0bac1 improve ux of bip39 wallet discovery on bitcoin core 2023-02-12 11:48:46 +02:00
Craig Raw a66b36c59c cormorant: switch bitcoind client from named to array parameters to support btc-rpc-proxy 2023-02-11 14:28:10 +02:00
Craig Raw fb3b674b65 recompile secp256k1 on osx 10.13.6 2023-02-10 09:23:02 +02:00
Craig Raw eff0e201f3 cormorant: only use proxy when connecting to onion addresses 2023-02-10 09:12:09 +02:00
Craig Raw 58d10cbba4 v1.7.3 2023-02-09 18:30:28 +02:00
Craig Raw 29bce8a9bc revert to ubuntu 20.04 runner 2023-02-09 17:34:19 +02:00
79 changed files with 1221 additions and 164 deletions
+1 -1
View File
@@ -7,7 +7,7 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [windows-latest, ubuntu-latest, macos-latest]
os: [windows-2022, ubuntu-20.04, macos-12]
steps:
- uses: actions/checkout@v3
with:
+2 -2
View File
@@ -16,7 +16,7 @@ or for those without SSH credentials:
`git clone --recursive https://github.com/sparrowwallet/sparrow.git`
In order to build, Sparrow requires Java 17 or higher to be installed.
In order to build, Sparrow requires Java 18 or higher to be installed.
The release binaries are built with [Eclipse Temurin 18.0.1+10](https://github.com/adoptium/temurin18-binaries/releases/tag/jdk-18.0.1%2B10).
Other packages may also be necessary to build depending on the platform. On Debian/Ubuntu systems:
@@ -44,7 +44,7 @@ If you prefer to run Sparrow directly from source, it can be launched from withi
`./sparrow`
Java 17 or higher must be installed.
Java 18 or higher must be installed.
## Configuration
+4 -4
View File
@@ -4,10 +4,10 @@ plugins {
id 'application'
id 'org-openjfx-javafxplugin'
id 'extra-java-module-info'
id 'org.beryx.jlink' version '2.25.0'
id 'org.beryx.jlink' version '2.26.0'
}
def sparrowVersion = '1.7.2'
def sparrowVersion = '1.7.3'
def os = org.gradle.internal.os.OperatingSystem.current()
def osName = os.getFamilyName()
if(os.macOsX) {
@@ -103,7 +103,7 @@ dependencies {
implementation('org.slf4j:jul-to-slf4j:1.7.30') {
exclude group: 'org.slf4j'
}
implementation('com.sparrowwallet.nightjar:nightjar:0.2.34')
implementation('com.sparrowwallet.nightjar:nightjar:0.2.35')
implementation('io.reactivex.rxjava2:rxjava:2.2.15')
implementation('io.reactivex.rxjava2:rxjavafx:2.2.2')
implementation('org.apache.commons:commons-lang3:3.7')
@@ -498,7 +498,7 @@ extraJavaModuleInfo {
exports('co.nstant.in.cbor.model')
exports('co.nstant.in.cbor.builder')
}
module('nightjar-0.2.34.jar', 'com.sparrowwallet.nightjar', '0.2.34') {
module('nightjar-0.2.35.jar', 'com.sparrowwallet.nightjar', '0.2.35') {
requires('com.google.common')
requires('net.sourceforge.streamsupport')
requires('org.slf4j')
+1 -1
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.2"
GIT_TAG="1.7.3"
```
The project can then be initially cloned as follows:
+1 -1
Submodule drongo updated: d48054ac6b...0f78efc373
@@ -6,4 +6,5 @@ Icon=/opt/sparrow/lib/Sparrow.png
Terminal=false
Type=Application
Categories=Finance;Network;
MimeType=application/psbt;application/bitcoin-transaction;x-scheme-handler/bitcoin;x-scheme-handler/auth47;x-scheme-handler/lightning
MimeType=application/psbt;application/bitcoin-transaction;x-scheme-handler/bitcoin;x-scheme-handler/auth47;x-scheme-handler/lightning
StartupWMClass=Sparrow
+1 -1
View File
@@ -21,7 +21,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.7.2</string>
<string>1.7.3</string>
<key>CFBundleSignature</key>
<string>????</string>
<!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories -->
@@ -1073,7 +1073,18 @@ public class AppController implements Initializable {
Optional<Wallet> optionalWallet = dlg.showAndWait();
if(optionalWallet.isPresent()) {
Wallet wallet = optionalWallet.get();
if(selectedWalletForms.isEmpty() || wallet != selectedWalletForms.get(0).getWallet()) {
List<WalletTabData> walletTabData = getOpenWalletTabData();
List<ExtendedKey> xpubs = wallet.getKeystores().stream().map(Keystore::getExtendedPublicKey).collect(Collectors.toList());
Optional<WalletForm> optNewWalletForm = walletTabData.stream()
.map(WalletTabData::getWalletForm)
.filter(wf -> wf.getSettingsWalletForm() != null && wf.getSettingsWalletForm().getWallet().getPolicyType() == PolicyType.MULTI &&
wf.getSettingsWalletForm().getWallet().getScriptType() == wallet.getScriptType() && !wf.getSettingsWalletForm().getWallet().isValid() &&
wf.getSettingsWalletForm().getWallet().getKeystores().stream().map(Keystore::getExtendedPublicKey).anyMatch(xpubs::contains)).findFirst();
if(optNewWalletForm.isPresent()) {
EventManager.get().post(new ExistingWalletImportedEvent(optNewWalletForm.get().getWalletId(), wallet));
selectTab(optNewWalletForm.get().getWallet());
} else if(selectedWalletForms.isEmpty() || wallet != selectedWalletForms.get(0).getWallet()) {
addImportedWallet(wallet);
}
}
@@ -1117,7 +1128,7 @@ public class AppController implements Initializable {
}
private void addImportedWallet(Wallet wallet) {
WalletNameDialog nameDlg = new WalletNameDialog(wallet.getName());
WalletNameDialog nameDlg = new WalletNameDialog(wallet.getName(), true, wallet.getBirthDate());
Optional<WalletNameDialog.NameAndBirthDate> optNameAndBirthDate = nameDlg.showAndWait();
if(optNameAndBirthDate.isPresent()) {
WalletNameDialog.NameAndBirthDate nameAndBirthDate = optNameAndBirthDate.get();
@@ -811,6 +811,24 @@ public class AppServices {
}
}
public static void openBlockExplorer(String txid) {
if(Config.get().isBlockExplorerDisabled()) {
return;
}
Server blockExplorer = Config.get().getBlockExplorer() == null ? BlockExplorer.MEMPOOL_SPACE.getServer() : Config.get().getBlockExplorer();
String url = blockExplorer.getUrl();
if(url.contains("{0}")) {
url = url.replace("{0}", txid);
} else {
if(Network.get() != Network.MAINNET) {
url += "/" + Network.get().getName();
}
url += "/tx/" + txid;
}
AppServices.get().getApplication().getHostServices().showDocument(url);
}
static void parseFileUriArguments(List<String> fileUriArguments) {
for(String fileUri : fileUriArguments) {
try {
@@ -25,7 +25,7 @@ public abstract class BaseController {
scriptArea.setMouseOverTextDelay(Duration.ofMillis(150));
scriptArea.addEventHandler(MouseOverTextEvent.MOUSE_OVER_TEXT_BEGIN, e -> {
TwoDimensional.Position position = scriptArea.getParagraph(0).getStyleSpans().offsetToPosition(e.getCharacterIndex(), Backward);
if(position.getMajor() % 2 == 0) {
if(position.getMajor() % 2 == 0 && scriptArea.getScript().getChunks().size() > position.getMajor() / 2) {
ScriptChunk hoverChunk = scriptArea.getScript().getChunks().get(position.getMajor()/2);
if(!hoverChunk.isOpCode()) {
Point2D pos = e.getScreenPosition();
@@ -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.2";
public static final String APP_VERSION = "1.7.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";
@@ -537,6 +537,13 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
getItems().add(createCpfp);
}
MenuItem openBlockExplorer = new MenuItem("Open in Block Explorer");
openBlockExplorer.setOnAction(AE -> {
hide();
AppServices.openBlockExplorer(blockTransaction.getHashAsString());
});
getItems().add(openBlockExplorer);
MenuItem copyTxid = new MenuItem("Copy Transaction ID");
copyTxid.setOnAction(AE -> {
hide();
@@ -558,6 +565,12 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
EventManager.get().post(new ViewTransactionEvent(this.getOwnerWindow(), blockTransaction));
});
MenuItem openBlockExplorer = new MenuItem("Open in Block Explorer");
openBlockExplorer.setOnAction(AE -> {
hide();
AppServices.openBlockExplorer(blockTransaction.getHashAsString());
});
MenuItem copyDate = new MenuItem("Copy Date");
copyDate.setOnAction(AE -> {
hide();
@@ -582,7 +595,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
Clipboard.getSystemClipboard().setContent(content);
});
getItems().addAll(viewTransaction, copyDate, copyTxid, copyHeight);
getItems().addAll(viewTransaction, openBlockExplorer, copyDate, copyTxid, copyHeight);
}
}
@@ -162,8 +162,8 @@ public abstract class FileImportPane extends TitledDescriptionPane {
setError("Import Error", e.getMessage());
}
} else if(result.outputDescriptor != null) {
wallets = List.of(result.outputDescriptor.toKeystoreWallet(null));
try {
wallets = List.of(result.outputDescriptor.toWallet());
importFile(importer.getName(), null, null);
} catch(ImportException e) {
log.error("Error importing QR", e);
@@ -193,6 +193,10 @@ public abstract class FileImportPane extends TitledDescriptionPane {
}
}
protected List<Wallet> getScannedWallets() {
return wallets;
}
protected Keystore getScannedKeystore(ScriptType scriptType) throws ImportException {
if(wallets != null) {
for(Wallet wallet : wallets) {
@@ -215,7 +219,7 @@ public abstract class FileImportPane extends TitledDescriptionPane {
private Node getPasswordEntry(File file) {
CustomPasswordField passwordField = new ViewPasswordField();
passwordField.setPromptText("Wallet password");
passwordField.setPromptText("Password");
password.bind(passwordField.textProperty());
HBox.setHgrow(passwordField, Priority.ALWAYS);
@@ -0,0 +1,168 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.KeystoreSource;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.hummingbird.registry.RegistryType;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.KeystoreExportEvent;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.*;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Control;
import javafx.scene.control.ToggleButton;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import org.controlsfx.control.SegmentedButton;
import org.controlsfx.glyphfont.Glyph;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Optional;
public class FileKeystoreExportPane extends TitledDescriptionPane {
private final Keystore keystore;
private final KeystoreFileExport exporter;
private final boolean scannable;
private final boolean file;
public FileKeystoreExportPane(Keystore keystore, KeystoreFileExport exporter) {
super(exporter.getName(), "Keystore export", exporter.getKeystoreExportDescription(), "image/" + exporter.getWalletModel().getType() + ".png");
this.keystore = keystore;
this.exporter = exporter;
this.scannable = exporter.isKeystoreExportScannable();
this.file = exporter.isKeystoreExportFile();
buttonBox.getChildren().clear();
buttonBox.getChildren().add(createButton());
}
@Override
protected Control createButton() {
if(scannable && file) {
ToggleButton showButton = new ToggleButton("Show...");
Glyph cameraGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CAMERA);
cameraGlyph.setFontSize(12);
showButton.setGraphic(cameraGlyph);
showButton.setOnAction(event -> {
showButton.setSelected(false);
exportQR();
});
ToggleButton fileButton = new ToggleButton("Export File...");
fileButton.setAlignment(Pos.CENTER_RIGHT);
fileButton.setOnAction(event -> {
fileButton.setSelected(false);
exportFile();
});
SegmentedButton segmentedButton = new SegmentedButton();
segmentedButton.getButtons().addAll(showButton, fileButton);
return segmentedButton;
} else if(scannable) {
Button showButton = new Button("Show...");
Glyph cameraGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CAMERA);
cameraGlyph.setFontSize(12);
showButton.setGraphic(cameraGlyph);
showButton.setOnAction(event -> {
exportQR();
});
return showButton;
} else {
Button exportButton = new Button("Export File...");
exportButton.setAlignment(Pos.CENTER_RIGHT);
exportButton.setOnAction(event -> {
exportFile();
});
return exportButton;
}
}
private void exportQR() {
exportKeystore(null, keystore);
}
private void exportFile() {
Stage window = new Stage();
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Export " + exporter.getWalletModel().toDisplayString() + " File");
String extension = exporter.getExportFileExtension(keystore);
String fileName = keystore.getLabel();
fileChooser.setInitialFileName(fileName + (extension == null || extension.isEmpty() ? "" : "." + extension));
AppServices.moveToActiveWindowScreen(window, 800, 450);
File file = fileChooser.showSaveDialog(window);
if(file != null) {
exportKeystore(file, keystore);
}
}
private void exportKeystore(File file, Keystore exportKeystore) {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
exporter.exportKeystore(exportKeystore, baos);
if(exporter.requiresSignature()) {
String message = baos.toString(StandardCharsets.UTF_8);
if(keystore.getSource() == KeystoreSource.HW_USB || keystore.getWalletModel().isCard()) {
TextAreaDialog dialog = new TextAreaDialog(message, false);
dialog.setTitle("Sign " + exporter.getName() + " Export");
dialog.getDialogPane().setHeaderText("The following text needs to be signed by the device.\nClick OK to continue.");
dialog.showAndWait();
Wallet wallet = new Wallet();
wallet.setScriptType(ScriptType.P2PKH);
wallet.getKeystores().add(keystore);
List<String> operationFingerprints = List.of(keystore.getKeyDerivation().getMasterFingerprint());
DeviceSignMessageDialog deviceSignMessageDialog = new DeviceSignMessageDialog(operationFingerprints, wallet, message, keystore.getKeyDerivation());
Optional<String> optSignature = deviceSignMessageDialog.showAndWait();
if(optSignature.isPresent()) {
exporter.addSignature(keystore, optSignature.get(), baos);
}
} else if(keystore.getSource() == KeystoreSource.SW_SEED) {
String signature = keystore.getExtendedPrivateKey().getKey().signMessage(message, ScriptType.P2PKH);
exporter.addSignature(keystore, signature, baos);
} else {
Optional<ButtonType> optButtonType = AppServices.showWarningDialog("Cannot sign export",
"Signing the " + exporter.getName() + " export with " + keystore.getWalletModel().toDisplayString() + " is not supported." +
"Proceed without signing?", ButtonType.NO, ButtonType.YES);
if(optButtonType.isPresent() && optButtonType.get() == ButtonType.NO) {
throw new RuntimeException("Export aborted due to lack of device message signing support.");
}
}
}
if(file != null) {
try(OutputStream outputStream = new FileOutputStream(file)) {
outputStream.write(baos.toByteArray());
EventManager.get().post(new KeystoreExportEvent(exportKeystore));
}
} else {
QRDisplayDialog qrDisplayDialog;
if(exporter instanceof Bip129) {
qrDisplayDialog = new QRDisplayDialog(RegistryType.BYTES.toString(), baos.toByteArray(), false);
} else {
qrDisplayDialog = new QRDisplayDialog(baos.toString(StandardCharsets.UTF_8));
}
qrDisplayDialog.showAndWait();
}
} catch(Exception e) {
String errorMessage = e.getMessage();
if(e.getCause() != null && e.getCause().getMessage() != null && !e.getCause().getMessage().isEmpty()) {
errorMessage = e.getCause().getMessage();
}
setError("Export Error", errorMessage);
}
}
}
@@ -92,9 +92,11 @@ public class FileWalletExportPane extends TitledDescriptionPane {
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Export " + exporter.getWalletModel().toDisplayString() + " File");
String extension = exporter.getExportFileExtension(wallet);
String fileName = wallet.getFullName() + "-" + exporter.getWalletModel().toDisplayString().toLowerCase(Locale.ROOT).replace(" ", "");
String walletModel = exporter.getWalletModel().toDisplayString().toLowerCase(Locale.ROOT).replace(" ", "");
String postfix = walletModel.equals(extension) ? "" : "-" + walletModel;
String fileName = wallet.getFullName() + postfix;
if(exporter.exportsAllWallets()) {
fileName = wallet.getMasterName();
fileName = wallet.getMasterName() + postfix;
}
fileChooser.setInitialFileName(fileName + (extension == null || extension.isEmpty() ? "" : "." + extension));
@@ -148,7 +150,7 @@ public class FileWalletExportPane extends TitledDescriptionPane {
QRDisplayDialog qrDisplayDialog;
if(exporter instanceof CoboVaultMultisig) {
qrDisplayDialog = new QRDisplayDialog(RegistryType.BYTES.toString(), outputStream.toByteArray(), true);
} else if(exporter instanceof PassportMultisig || exporter instanceof KeystoneMultisig || exporter instanceof JadeMultisig) {
} else if(exporter instanceof PassportMultisig || exporter instanceof KeystoneMultisig || exporter instanceof JadeMultisig || exporter instanceof Bip129) {
qrDisplayDialog = new QRDisplayDialog(RegistryType.BYTES.toString(), outputStream.toByteArray(), false);
} else {
qrDisplayDialog = new QRDisplayDialog(outputStream.toString(StandardCharsets.UTF_8));
@@ -18,7 +18,13 @@ public class FileWalletImportPane extends FileImportPane {
@Override
protected void importFile(String fileName, InputStream inputStream, String password) throws ImportException {
Wallet wallet = importer.importWallet(inputStream, password);
Wallet wallet;
if(getScannedWallets() != null && !getScannedWallets().isEmpty()) {
wallet = getScannedWallets().iterator().next();
} else {
wallet = importer.importWallet(inputStream, password);
}
if(wallet.getName() == null) {
wallet.setName(fileName);
}
@@ -0,0 +1,66 @@
package com.sparrowwallet.sparrow.control;
import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.KeystoreExportEvent;
import com.sparrowwallet.sparrow.io.*;
import javafx.scene.control.*;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.StackPane;
import java.util.Comparator;
import java.util.List;
public class KeystoreExportDialog extends Dialog<Keystore> {
public KeystoreExportDialog(Keystore keystore) {
EventManager.get().register(this);
setOnCloseRequest(event -> {
EventManager.get().unregister(this);
});
final DialogPane dialogPane = getDialogPane();
AppServices.setStageIcon(dialogPane.getScene().getWindow());
StackPane stackPane = new StackPane();
dialogPane.setContent(stackPane);
AnchorPane anchorPane = new AnchorPane();
stackPane.getChildren().add(anchorPane);
ScrollPane scrollPane = new ScrollPane();
scrollPane.setPrefHeight(200);
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
anchorPane.getChildren().add(scrollPane);
scrollPane.setFitToWidth(true);
AnchorPane.setLeftAnchor(scrollPane, 0.0);
AnchorPane.setRightAnchor(scrollPane, 0.0);
List<KeystoreFileExport> exporters = List.of(new Bip129());
Accordion exportAccordion = new Accordion();
for(KeystoreFileExport exporter : exporters) {
if(!exporter.isDeprecated() || Config.get().isShowDeprecatedImportExport()) {
FileKeystoreExportPane exportPane = new FileKeystoreExportPane(keystore, exporter);
exportAccordion.getPanes().add(exportPane);
}
}
exportAccordion.getPanes().sort(Comparator.comparing(o -> ((TitledDescriptionPane) o).getTitle()));
scrollPane.setContent(exportAccordion);
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
dialogPane.getButtonTypes().addAll(cancelButtonType);
dialogPane.setPrefWidth(500);
dialogPane.setPrefHeight(280);
AppServices.moveToActiveWindowScreen(this);
setResultConverter(dialogButton -> dialogButton != cancelButtonType ? keystore : null);
}
@Subscribe
public void keystoreExported(KeystoreExportEvent event) {
setResult(event.getKeystore());
}
}
@@ -24,7 +24,8 @@ public class MnemonicKeystoreDisplayPane extends MnemonicKeystorePane {
showWordList(keystore.getSeed());
}
protected Node getMnemonicWordsEntry(int numWords) {
@Override
protected Node getMnemonicWordsEntry(int numWords, boolean editPassphrase) {
VBox vBox = new VBox();
vBox.setSpacing(10);
@@ -223,7 +223,7 @@ public class MnemonicKeystoreImportPane extends MnemonicKeystorePane {
private void confirmBackup() {
setDescription("Confirm backup by re-entering words");
showHideLink.setVisible(false);
setContent(getMnemonicWordsEntry(wordEntriesProperty.get().size()));
setContent(getMnemonicWordsEntry(wordEntriesProperty.get().size(), false));
setExpanded(true);
backButton.setVisible(true);
generateButton.setVisible(false);
@@ -106,7 +106,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
protected void showWordList(DeterministicSeed seed) {
List<String> words = seed.getMnemonicCode();
setContent(getMnemonicWordsEntry(words.size()));
setContent(getMnemonicWordsEntry(words.size(), true));
setExpanded(true);
for(int i = 0; i < wordsPane.getChildren().size(); i++) {
@@ -119,11 +119,11 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
protected void enterMnemonic(int numWords) {
setDescription("Generate new or enter existing");
showHideLink.setVisible(false);
setContent(getMnemonicWordsEntry(numWords));
setContent(getMnemonicWordsEntry(numWords, true));
setExpanded(true);
}
protected Node getMnemonicWordsEntry(int numWords) {
protected Node getMnemonicWordsEntry(int numWords, boolean editPassphrase) {
VBox vBox = new VBox();
vBox.setSpacing(10);
@@ -152,7 +152,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
vBox.getChildren().add(wordsPane);
PassphraseEntry passphraseEntry = new PassphraseEntry();
PassphraseEntry passphraseEntry = new PassphraseEntry(editPassphrase);
wordEntries.get(wordEntries.size() - 1).setNextField(passphraseEntry.getEditor());
passphraseEntry.setPadding(new Insets(0, 26, 10, 10));
vBox.getChildren().add(passphraseEntry);
@@ -344,23 +344,50 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
}
protected class PassphraseEntry extends HBox {
private final CustomTextField passphraseField;
private final TextField passphraseField;
public PassphraseEntry() {
public PassphraseEntry(boolean editable) {
super();
setAlignment(Pos.CENTER_LEFT);
setSpacing(10);
Label usePassphraseLabel = new Label("Use passphrase?");
usePassphraseLabel.managedProperty().bind(usePassphraseLabel.visibleProperty());
CheckBox usePassphraseCheckbox = new CheckBox(" ");
usePassphraseCheckbox.setDisable(!editable);
usePassphraseCheckbox.managedProperty().bind(usePassphraseCheckbox.visibleProperty());
usePassphraseLabel.visibleProperty().bind(usePassphraseCheckbox.visibleProperty());
usePassphraseCheckbox.setVisible(passphraseProperty.isEmpty().get());
usePassphraseCheckbox.selectedProperty().addListener((observable, oldValue, newValue) -> {
if(newValue) {
Optional<ButtonType> optType = AppServices.showWarningDialog("Add a passphrase?", "A passphrase is not a password! " +
"Any variation entered in future loads a valid wallet, but with different addresses.\n\n" +
"This feature provides optional added security for advanced users only. Are you sure?", ButtonType.NO, ButtonType.YES);
if(optType.isPresent() && optType.get() == ButtonType.YES) {
usePassphraseCheckbox.setVisible(false);
} else {
usePassphraseCheckbox.setSelected(false);
}
}
});
Label passphraseLabel = new Label("Passphrase:");
passphraseField = (CustomTextField) TextFields.createClearableTextField();
passphraseLabel.managedProperty().bind(passphraseLabel.visibleProperty());
passphraseField = new TextField();
passphraseField.setPromptText(passphraseProperty.isEmpty().get() ? "Leave blank for none" : "");
passphraseField.setText(passphraseProperty.get());
passphraseField.setDisable(!editable);
passphraseField.managedProperty().bind(passphraseField.visibleProperty());
passphraseLabel.visibleProperty().bind(passphraseField.visibleProperty());
passphraseField.visibleProperty().bind(usePassphraseCheckbox.visibleProperty().not());
passphraseProperty.bind(passphraseField.textProperty());
passphraseField.setPromptText("Leave blank for none");
HelpLabel helpLabel = new HelpLabel();
helpLabel.setPrefHeight(28);
helpLabel.setStyle("-fx-padding: 0 0 0 0");
helpLabel.setHelpText("A passphrase provides optional added security - it is not stored so it must be remembered!");
helpLabel.setHelpText("Advanced feature: a passphrase provides optional added security, but it is not stored so it must be remembered!");
getChildren().addAll(passphraseLabel, passphraseField, helpLabel);
getChildren().addAll(usePassphraseLabel, usePassphraseCheckbox, passphraseLabel, passphraseField, helpLabel);
}
public TextField getEditor() {
@@ -10,9 +10,11 @@ import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.WalletImportEvent;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.io.ImportException;
import com.sparrowwallet.sparrow.io.KeystoreMnemonicImport;
import com.sparrowwallet.sparrow.net.ElectrumServer;
import com.sparrowwallet.sparrow.net.ServerType;
import javafx.collections.FXCollections;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
@@ -135,7 +137,10 @@ public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane {
EventManager.get().post(new WalletImportEvent(optWallet.get()));
} else {
discoverButton.setDisable(false);
Optional<ButtonType> optButtonType = AppServices.showErrorDialog("No existing wallet found", "Could not find a wallet with existing transactions using this mnemonic. Import this wallet anyway?", ButtonType.NO, ButtonType.YES);
Optional<ButtonType> optButtonType = AppServices.showErrorDialog("No existing wallet found",
Config.get().getServerType() == ServerType.BITCOIN_CORE ? "The configured server type is Bitcoin Core, which does not support wallet discovery.\n\n" +
"You can however import this wallet and scan the blockchain by supplying a start date. Do you want to import this wallet?" :
"Could not find a wallet with existing transactions using this mnemonic. Import this wallet anyway?", ButtonType.NO, ButtonType.YES);
if(optButtonType.isPresent() && optButtonType.get() == ButtonType.YES) {
setContent(getScriptTypeEntry());
setExpanded(true);
@@ -183,7 +188,7 @@ public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane {
scriptTypeComboBox.setMaxWidth(170);
HelpLabel helpLabel = new HelpLabel();
helpLabel.setHelpText("P2WPKH is a Native Segwit type and is usually the best choice for new wallets.\nP2SH-P2WPKH is a Wrapped Segwit type and is a reasonable choice for the widest compatibility.\nP2PKH is a Legacy type and should be avoided for new wallets.\nFor existing wallets, be sure to choose the type that matches the wallet you are importing.");
helpLabel.setHelpText("Native Segwit is usually the best choice for new wallets.\nTaproot is a new type useful for specific needs.\nNested Segwit and Legacy are useful for recovering older wallets.\nFor existing wallets, be sure to choose the type that matches the wallet you are importing.");
fieldBox.getChildren().addAll(scriptTypeComboBox, helpLabel);
Region region = new Region();
@@ -53,6 +53,7 @@ import java.util.*;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
@SuppressWarnings("deprecation")
@@ -542,7 +543,12 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
private KeyDerivation getKeyDerivation(CryptoKeypath cryptoKeypath) {
if(cryptoKeypath != null) {
return new KeyDerivation(Utils.bytesToHex(cryptoKeypath.getSourceFingerprint()), cryptoKeypath.getPath());
if(cryptoKeypath.getComponents().stream().anyMatch(PathComponent::isWildcard)) {
throw new IllegalArgumentException("Wildcard derivation paths are not supported");
}
List<ChildNumber> path = cryptoKeypath.getComponents().stream().map(comp -> new ChildNumber(comp.getIndex(), comp.isHardened())).collect(Collectors.toList());
return new KeyDerivation(Utils.bytesToHex(cryptoKeypath.getSourceFingerprint()), KeyDerivation.writePath(path));
}
return null;
@@ -58,6 +58,8 @@ public class ScriptArea extends CodeArea {
append("<signature" + signatureCount++ + ">", "script-signature");
} else if(chunk.isPubKey()) {
append("<pubkey" + pubKeyCount++ + ">", "script-pubkey");
} else if(chunk.isString()) {
append(chunk.toString(), "script-other");
} else if(chunk.isScript()) {
Script nestedScript = chunk.getScript();
if (nestedScript.equals(redeemScript)) {
@@ -0,0 +1,73 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.sparrow.AppServices;
import javafx.application.Platform;
import javafx.beans.NamedArg;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class TextfieldDialog extends Dialog<String> {
private static final Logger log = LoggerFactory.getLogger(TextAreaDialog.class);
private final TextField textField;
private final String defaultValue;
public TextfieldDialog() {
this("");
}
public TextfieldDialog(String defaultValue) {
this(defaultValue, true);
}
public TextfieldDialog(@NamedArg("defaultValue") String defaultValue, @NamedArg("editable") boolean editable) {
final DialogPane dialogPane = getDialogPane();
setDialogPane(dialogPane);
Image image = new Image("/image/sparrow-small.png");
dialogPane.setGraphic(new ImageView(image));
HBox hbox = new HBox();
this.textField = new TextField(defaultValue);
this.textField.setMaxWidth(Double.MAX_VALUE);
this.textField.setEditable(editable);
hbox.getChildren().add(textField);
HBox.setHgrow(this.textField, Priority.ALWAYS);
this.defaultValue = defaultValue;
dialogPane.setContent(hbox);
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
AppServices.setStageIcon(dialogPane.getScene().getWindow());
dialogPane.getStyleClass().add("text-input-dialog");
dialogPane.getButtonTypes().add(ButtonType.OK);
if(editable) {
dialogPane.getButtonTypes().add(ButtonType.CANCEL);
}
Platform.runLater(textField::requestFocus);
setResultConverter((dialogButton) -> {
ButtonBar.ButtonData data = dialogButton == null ? null : dialogButton.getButtonData();
return data == ButtonBar.ButtonData.OK_DONE ? textField.getText() : null;
});
dialogPane.setPrefWidth(600);
dialogPane.setPrefHeight(230);
AppServices.moveToActiveWindowScreen(this);
}
public final TextField getEditor() {
return textField;
}
public final String getDefaultValue() {
return defaultValue;
}
}
@@ -44,7 +44,7 @@ public class WalletExportDialog extends Dialog<Wallet> {
if(wallet.getPolicyType() == PolicyType.SINGLE) {
exporters = List.of(new Electrum(), new ElectrumPersonalServer(), new Descriptor(), new SpecterDesktop(), new Sparrow(), new WalletLabels());
} else if(wallet.getPolicyType() == PolicyType.MULTI) {
exporters = List.of(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());
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());
} else {
throw new UnsupportedOperationException("Cannot export wallet with policy type " + wallet.getPolicyType());
}
@@ -59,7 +59,7 @@ public class WalletImportDialog extends Dialog<Wallet> {
}
}
List<WalletImport> walletImporters = new ArrayList<>(List.of(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()));
if(!selectedWalletForms.isEmpty()) {
walletImporters.add(new WalletLabels(selectedWalletForms));
}
@@ -34,6 +34,14 @@ public class WalletNameDialog extends Dialog<WalletNameDialog.NameAndBirthDate>
}
public WalletNameDialog(String initialName) {
this(initialName, false);
}
public WalletNameDialog(String initialName, boolean hasExistingTransactions) {
this(initialName, hasExistingTransactions, null);
}
public WalletNameDialog(String initialName, boolean hasExistingTransactions, Date startDate) {
final DialogPane dialogPane = getDialogPane();
AppServices.setStageIcon(dialogPane.getScene().getWindow());
boolean requestBirthDate = (Config.get().getServerType() == null || Config.get().getServerType() == ServerType.BITCOIN_CORE);
@@ -91,6 +99,12 @@ public class WalletNameDialog extends Dialog<WalletNameDialog.NameAndBirthDate>
if(requestBirthDate) {
content.getChildren().add(existingBox);
if(hasExistingTransactions) {
existingCheck.setSelected(true);
}
if(startDate != null) {
existingPicker.setValue(startDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDate());
}
}
dialogPane.setContent(content);
@@ -0,0 +1,21 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet;
public class ExistingWalletImportedEvent {
private final String existingWalletId;
private final Wallet importedWallet;
public ExistingWalletImportedEvent(String existingWalletId, Wallet importedWallet) {
this.existingWalletId = existingWalletId;
this.importedWallet = importedWallet;
}
public String getExistingWalletId() {
return existingWalletId;
}
public Wallet getImportedWallet() {
return importedWallet;
}
}
@@ -0,0 +1,15 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Keystore;
public class KeystoreExportEvent {
private final Keystore keystore;
public KeystoreExportEvent(Keystore keystore) {
this.keystore = keystore;
}
public Keystore getKeystore() {
return keystore;
}
}
@@ -0,0 +1,24 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.io.Storage;
public class NewChildWalletSavedEvent {
private final Storage storage;
private final Wallet masterWallet;
private final Wallet childWallet;
public NewChildWalletSavedEvent(Storage storage, Wallet masterWallet, Wallet childWallet) {
this.storage = storage;
this.masterWallet = masterWallet;
this.childWallet = childWallet;
}
public String getMasterWalletId() {
return storage.getWalletId(masterWallet);
}
public Wallet getChildWallet() {
return childWallet;
}
}
@@ -76,6 +76,7 @@ public class FontAwesome5 extends GlyphFont {
TOGGLE_OFF('\uf204'),
TOGGLE_ON('\uf205'),
TOOLS('\uf7d9'),
UP_RIGHT_FROM_SQUARE('\uf35d'),
UNDO('\uf0e2'),
USER('\uf007'),
USER_FRIENDS('\uf500'),
@@ -0,0 +1,253 @@
package com.sparrowwallet.sparrow.io;
import com.google.common.io.CharStreams;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.OutputDescriptor;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.crypto.Pbkdf2KeyDeriver;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.wallet.*;
import javax.crypto.*;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.*;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.util.Arrays;
import java.util.List;
public class Bip129 implements KeystoreFileExport, KeystoreFileImport, WalletExport, WalletImport {
@Override
public String getName() {
return "BSMS";
}
@Override
public WalletModel getWalletModel() {
return WalletModel.BSMS;
}
@Override
public String getKeystoreExportDescription() {
return "Exports the keystore in the Bitcoin Secure Multisig Setup (BSMS) format.";
}
@Override
public void exportKeystore(Keystore keystore, OutputStream outputStream) throws ExportException {
if(!keystore.isValid()) {
throw new ExportException("Invalid keystore");
}
try {
String record = "BSMS 1.0\n00\n[" +
keystore.getKeyDerivation().toString() +
"]" +
keystore.getExtendedPublicKey().toString() +
"\n" +
keystore.getLabel();
outputStream.write(record.getBytes(StandardCharsets.UTF_8));
} catch(Exception e) {
throw new ExportException("Error writing BSMS file", e);
}
}
@Override
public boolean requiresSignature() {
//Due to poor vendor support of multiline message signing at the xpub derivation path, signing BSMS keystore exports is configurable (default false)
return Config.get().isSignBsmsExports();
}
@Override
public void addSignature(Keystore keystore, String signature, OutputStream outputStream) throws ExportException {
try {
String append = "\n" + signature;
outputStream.write(append.getBytes(StandardCharsets.UTF_8));
} catch(Exception e) {
throw new ExportException("Error writing BSMS file", e);
}
}
@Override
public String getExportFileExtension(Keystore keystore) {
return "bsms";
}
@Override
public boolean isKeystoreExportScannable() {
return true;
}
@Override
public boolean isEncrypted(File file) {
try {
try(BufferedReader reader = new BufferedReader(new FileReader(file, StandardCharsets.UTF_8))) {
String text = CharStreams.toString(reader);
return Utils.isHex(text.trim());
}
} catch(Exception e) {
return false;
}
}
@Override
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
try {
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
if(password != null) {
reader = decryptImport(password, reader);
}
String header = reader.readLine();
String token = reader.readLine();
String descriptor = reader.readLine();
String label = reader.readLine();
String signature = reader.readLine();
return getKeystore(header, token, descriptor, label, signature);
} catch(MnemonicException.MnemonicWordException e) {
throw new ImportException("Error importing BSMS: Invalid mnemonic word " + e.badWord, e);
} catch(MnemonicException.MnemonicChecksumException e) {
throw new ImportException("Error importing BSMS: Invalid mnemonic checksum", e);
} catch(Exception e) {
throw new ImportException("Error importing BSMS", e);
}
}
private BufferedReader decryptImport(String password, BufferedReader streamReader) throws Exception {
byte[] token;
if((password.length() == 16 || password.length() == 32) && Utils.isHex(password)) {
token = Utils.hexToBytes(password);
} else if(Utils.isNumber(password)) {
BigInteger bi = new BigInteger(password);
token = Utils.bigIntegerToBytes(bi, bi.toByteArray().length >= 16 ? 16 : 8);
} else if(password.split(" ").length == 6 || password.split(" ").length == 12) {
List<String> mnemonicWords = Arrays.asList(password.split(" "));
token = Bip39MnemonicCode.INSTANCE.toEntropy(mnemonicWords);
} else {
throw new ImportException("Provided password needs to be in hexadecimal, decimal or mnemonic format.");
}
String hex = CharStreams.toString(streamReader).trim();
byte[] data = Utils.hexToBytes(hex);
byte[] mac = Arrays.copyOfRange(data, 0, 32);
byte[] iv = Arrays.copyOfRange(mac, 0, 16);
byte[] ciphertext = Arrays.copyOfRange(data, 32, data.length);
Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
Pbkdf2KeyDeriver pbkdf2KeyDeriver = new Pbkdf2KeyDeriver(token, 2048, 256);
byte[] key = pbkdf2KeyDeriver.deriveKey("No SPOF").getKeyBytes();
Key keySpec = new SecretKeySpec(key, "AES");
IvParameterSpec ivSpec = new IvParameterSpec(iv);
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
byte[] plaintext = cipher.doFinal(ciphertext);
String plaintextString = new String(plaintext, StandardCharsets.UTF_8);
SecretKeySpec secretKeySpec = new SecretKeySpec(Sha256Hash.hash(key), "HmacSHA256");
Mac hmac = Mac.getInstance("HmacSHA256");
hmac.init(secretKeySpec);
String macData = Utils.bytesToHex(token) + plaintextString;
byte[] calculatedMac = hmac.doFinal(macData.getBytes(StandardCharsets.UTF_8));
if(!Arrays.equals(mac, calculatedMac)) {
throw new ImportException("Message digest authentication failed.");
}
return new BufferedReader(new StringReader(plaintextString));
}
private Keystore getKeystore(String header, String token, String descriptor, String label, String signature) throws ImportException {
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor("sh(" + descriptor + ")");
Wallet wallet = outputDescriptor.toWallet();
Keystore keystore = wallet.getKeystores().get(0);
keystore.setLabel(label);
if(signature != null) {
try {
String message = header + "\n" + token + "\n" + descriptor + "\n" + label;
keystore.getExtendedPublicKey().getKey().verifyMessage(message, signature);
} catch(SignatureException e) {
throw new ImportException("Signature did not match provided public key", e);
}
}
return keystore;
}
@Override
public String getKeystoreImportDescription(int account) {
return "Imports a keystore that was exported using the Bitcoin Secure Multisig Setup (BSMS) format.";
}
@Override
public boolean isKeystoreImportScannable() {
return true;
}
@Override
public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException {
try {
String record = "BSMS 1.0\n" +
OutputDescriptor.getOutputDescriptor(wallet) +
"\n/0/*,/1/*\n" +
wallet.getNode(KeyPurpose.RECEIVE).getChildren().iterator().next().getAddress();
outputStream.write(record.getBytes(StandardCharsets.UTF_8));
} catch(Exception e) {
throw new ExportException("Error exporting BSMS format", e);
}
}
@Override
public String getWalletExportDescription() {
return "Exports a multisig wallet in the Bitcoin Secure Multisig Setup (BSMS) format for import by other signers in the quorum.";
}
@Override
public String getExportFileExtension(Wallet wallet) {
return "bsms";
}
@Override
public boolean isWalletExportScannable() {
return true;
}
@Override
public boolean walletExportRequiresDecryption() {
return false;
}
@Override
public String getWalletImportDescription() {
return "Imports a multisig wallet in the Bitcoin Secure Multisig Setup (BSMS) format that has been created by another signer in the quorum.";
}
@Override
public Wallet importWallet(InputStream inputStream, String password) throws ImportException {
try {
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
if(password != null) {
reader = decryptImport(password, reader);
}
String header = reader.readLine();
String descriptor = reader.readLine();
String paths = reader.readLine();
String address = reader.readLine();
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(descriptor);
return outputDescriptor.toWallet();
} catch(Exception e) {
throw new ImportException("Error importing BSMS format", e);
}
}
@Override
public boolean isWalletImportScannable() {
return true;
}
}
@@ -99,6 +99,8 @@ public abstract class CardApi {
Throwable cause = Throwables.getRootCause(e);
if(cause.getMessage().equals("SCARD_E_NO_SERVICE")) {
recoverNoService();
} else if(cause.getMessage().equals("SCARD_E_NO_READERS_AVAILABLE")) {
log.info("Error detecting card terminals", e);
} else {
log.error("Error detecting card terminals", e);
}
@@ -30,6 +30,7 @@ public class Config {
private Mode mode;
private BitcoinUnit bitcoinUnit;
private UnitFormat unitFormat;
private Server blockExplorer;
private FeeRatesSource feeRatesSource;
private FeeRatesSelection feeRatesSelection;
private OptimizationStrategy sendOptimizationStrategy;
@@ -48,6 +49,7 @@ public class Config {
private boolean showLoadingLog = true;
private boolean showAddressTransactionCount = false;
private boolean showDeprecatedImportExport = false;
private boolean signBsmsExports = false;
private boolean preventSleep = false;
private List<File> recentWalletFiles;
private Integer keyDerivationPeriod;
@@ -149,6 +151,19 @@ public class Config {
flush();
}
public boolean isBlockExplorerDisabled() {
return BlockExplorer.NONE.getServer().equals(blockExplorer);
}
public Server getBlockExplorer() {
return blockExplorer;
}
public void setBlockExplorer(Server blockExplorer) {
this.blockExplorer = blockExplorer;
flush();
}
public FeeRatesSource getFeeRatesSource() {
return feeRatesSource;
}
@@ -315,6 +330,15 @@ public class Config {
flush();
}
public boolean isSignBsmsExports() {
return signBsmsExports;
}
public void setSignBsmsExports(boolean signBsmsExports) {
this.signBsmsExports = signBsmsExports;
flush();
}
public boolean isPreventSleep() {
return preventSleep;
}
@@ -136,6 +136,6 @@ public class Descriptor implements WalletImport, WalletExport {
@Override
public boolean isWalletImportScannable() {
return false;
return true;
}
}
@@ -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.0";
private static final String HWI_VERSION = "2.2.1";
private static final String HWI_VERSION_DIR = HWI_VERSION_PREFIX + HWI_VERSION;
private static boolean isPromptActive = false;
@@ -278,7 +278,7 @@ public class Hwi {
} catch(IOException e) {
throw new SignTransactionException("Could not sign PSBT", e);
} catch(PSBTParseException e) {
throw new SignTransactionException("Could not parse signed PSBT", e);
throw new SignTransactionException("Could not parse signed PSBT" + (e.getMessage() != null ? ": " + e.getMessage() : ""), e);
} finally {
isPromptActive = false;
}
@@ -290,9 +290,7 @@ public class Hwi {
try {
ProcessBuilder processBuilder = new ProcessBuilder(command);
process = processBuilder.start();
try(InputStreamReader reader = new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8)) {
return CharStreams.toString(reader);
}
return getProcessOutput(process);
} finally {
deleteExtractionOnFailure(process, start);
}
@@ -303,29 +301,54 @@ public class Hwi {
Process process = null;
try {
List<String> processArguments = new ArrayList<>(arguments);
processArguments.add("--stdin");
boolean useStdin = Arrays.stream(commandArguments).noneMatch(arg -> arg.contains("\n"));
if(useStdin) {
processArguments.add("--stdin");
} else {
processArguments.add(command.toString());
processArguments.addAll(Arrays.asList(commandArguments));
}
ProcessBuilder processBuilder = new ProcessBuilder(processArguments);
process = processBuilder.start();
try(BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(process.getOutputStream(), StandardCharsets.UTF_8))) {
writer.write(command.toString());
for(String commandArgument : commandArguments) {
writer.write(" \"");
writer.write(commandArgument.replace("\\", "\\\\").replace("\"", "\\\""));
writer.write("\"");
if(useStdin) {
try(BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(process.getOutputStream(), StandardCharsets.UTF_8))) {
writer.write(command.toString());
for(String commandArgument : commandArguments) {
writer.write(" \"");
writer.write(commandArgument.replace("\\", "\\\\").replace("\"", "\\\""));
writer.write("\"");
}
writer.flush();
}
writer.flush();
}
try(InputStreamReader reader = new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8)) {
return CharStreams.toString(reader);
}
return getProcessOutput(process);
} finally {
deleteExtractionOnFailure(process, start);
}
}
private String getProcessOutput(Process process) throws IOException {
String output;
try(InputStreamReader reader = new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8)) {
output = CharStreams.toString(reader);
}
if(output.isEmpty() && process.getErrorStream() != null) {
try(InputStreamReader reader = new InputStreamReader(process.getErrorStream(), StandardCharsets.UTF_8)) {
String errorOutput = CharStreams.toString(reader);
if(!errorOutput.isEmpty()) {
throw new IOException(errorOutput);
}
}
}
return output;
}
private synchronized File getHwiExecutable(Command command) {
File hwiExecutable = Config.get().getHwi();
if(hwiExecutable != null && hwiExecutable.exists()) {
@@ -0,0 +1,5 @@
package com.sparrowwallet.sparrow.io;
public interface KeystoreExport extends ImportExport {
String getKeystoreExportDescription();
}
@@ -0,0 +1,16 @@
package com.sparrowwallet.sparrow.io;
import com.sparrowwallet.drongo.wallet.Keystore;
import java.io.OutputStream;
public interface KeystoreFileExport extends KeystoreExport {
void exportKeystore(Keystore keystore, OutputStream outputStream) throws ExportException;
boolean requiresSignature();
void addSignature(Keystore keystore, String signature, OutputStream outputStream) throws ExportException;
String getExportFileExtension(Keystore keystore);
boolean isKeystoreExportScannable();
default boolean isKeystoreExportFile() {
return true;
}
}
@@ -25,6 +25,7 @@ import org.slf4j.LoggerFactory;
import java.awt.*;
import java.io.*;
import java.util.Locale;
import java.util.Scanner;
public class PdfUtils {
@@ -99,7 +100,7 @@ public class PdfUtils {
private static javafx.scene.image.Image getQrCode(String fragment) throws IOException, WriterException {
QRCodeWriter qrCodeWriter = new QRCodeWriter();
BitMatrix qrMatrix = qrCodeWriter.encode(fragment, BarcodeFormat.QR_CODE, QR_WIDTH, QR_HEIGHT);
BitMatrix qrMatrix = qrCodeWriter.encode(fragment.toUpperCase(Locale.ROOT), BarcodeFormat.QR_CODE, QR_WIDTH, QR_HEIGHT);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
MatrixToImageWriter.writeToStream(qrMatrix, "PNG", baos, new MatrixToImageConfig());
@@ -70,6 +70,14 @@ public class Server {
return url + (alias == null ? "" : "|" + alias);
}
public boolean portEquals(String port) {
if(port == null) {
return !getHostAndPort().hasPort();
}
return port.equals(getHostAndPort().hasPort() ? Integer.toString(getHostAndPort().getPort()) : "");
}
public static Server fromString(String server) {
String[] parts = server.split("\\|");
if(parts.length >= 2) {
@@ -23,10 +23,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.smartcardio.CardException;
import java.util.Base64;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.*;
public class CkCardApi extends CardApi {
private static final Logger log = LoggerFactory.getLogger(CkCardApi.class);
@@ -227,8 +224,18 @@ public class CkCardApi extends CardApi {
}
String signMessage(String message, ScriptType scriptType, List<ChildNumber> derivation) throws CardException {
List<ChildNumber> keystoreDerivation = derivation.subList(0, derivation.size() - 2);
List<ChildNumber> subPathDerivation = derivation.subList(derivation.size() - 2, derivation.size());
List<ChildNumber> keystoreDerivation;
List<ChildNumber> subPathDerivation;
Optional<ChildNumber> firstUnhardened = derivation.stream().filter(cn -> !cn.isHardened()).findFirst();
if(firstUnhardened.isPresent()) {
int index = derivation.indexOf(firstUnhardened.get());
keystoreDerivation = derivation.subList(0, index);
subPathDerivation = derivation.subList(index, derivation.size());
} else {
keystoreDerivation = derivation;
subPathDerivation = Collections.emptyList();
}
Keystore cardKeystore = getKeystore();
KeyDerivation cardKeyDerivation = cardKeystore.getKeyDerivation();
@@ -239,9 +246,15 @@ public class CkCardApi extends CardApi {
signingKeystore = getKeystore();
}
WalletNode addressNode = new WalletNode(KeyDerivation.writePath(subPathDerivation));
ECKey addressPubKey = signingKeystore.getPubKey(addressNode);
return addressPubKey.signMessage(message, scriptType, hash -> {
ECKey signingPubKey;
if(subPathDerivation.isEmpty()) {
signingPubKey = signingKeystore.getExtendedPublicKey().getKey();
} else {
WalletNode addressNode = new WalletNode(KeyDerivation.writePath(subPathDerivation));
signingPubKey = signingKeystore.getPubKey(addressNode);
}
return signingPubKey.signMessage(message, scriptType, hash -> {
try {
CardSign cardSign = cardProtocol.sign(cvc, subPathDerivation, hash);
return cardSign.getSignature();
@@ -28,7 +28,7 @@ public class HwAirgappedController extends KeystoreImportDetailController {
if(getMasterController().getWallet().getPolicyType().equals(PolicyType.SINGLE)) {
fileImporters = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig(), new Jade(), new KeystoneSinglesig(), new PassportSinglesig(), new SeedSigner(), new GordianSeedTool(), new SpecterDIY());
} else if(getMasterController().getWallet().getPolicyType().equals(PolicyType.MULTI)) {
fileImporters = List.of(new ColdcardMultisig(), new CoboVaultMultisig(), new Jade(), new KeystoneMultisig(), new PassportMultisig(), new SeedSigner(), new GordianSeedTool(), new SpecterDIY());
fileImporters = List.of(new Bip129(), new ColdcardMultisig(), new CoboVaultMultisig(), new Jade(), new KeystoneMultisig(), new PassportMultisig(), new SeedSigner(), new GordianSeedTool(), new SpecterDIY());
}
for(KeystoreFileImport importer : fileImporters) {
@@ -0,0 +1,19 @@
package com.sparrowwallet.sparrow.net;
import com.sparrowwallet.sparrow.io.Server;
public enum BlockExplorer {
MEMPOOL_SPACE("https://mempool.space"),
BLOCKSTREAM_INFO("https://blockstream.info"),
NONE("http://none");
private final Server server;
BlockExplorer(String url) {
this.server = new Server(url);
}
public Server getServer() {
return server;
}
}
@@ -146,6 +146,9 @@ public class Bwt {
Config config = Config.get();
if(config.getCoreServer() != null) {
bwtConfig.bitcoindUrl = config.getCoreServer().getUrl();
if(!config.getCoreServer().getHostAndPort().hasPort()) {
bwtConfig.bitcoindUrl = config.getCoreServer().getUrl() + ":" + Network.get().getDefaultPort();
}
try {
Protocol protocol = config.getCoreServer().getProtocol();
HostAndPort hostAndPort = protocol.getServerHostAndPort(bwtConfig.bitcoindUrl);
@@ -444,7 +444,7 @@ public class BitcoindClient {
private String getTransaction(String txid) {
try {
return getBitcoindService().getTransaction(txid, false).get("hex").toString();
return getBitcoindService().getTransaction(txid, true, false).get("hex").toString();
} catch(JsonRpcException e) {
return getBitcoindService().getRawTransaction(txid, false).toString();
}
@@ -1,5 +1,7 @@
package com.sparrowwallet.sparrow.net.cormorant.bitcoind;
import com.github.arteam.simplejsonrpc.client.JsonRpcParams;
import com.github.arteam.simplejsonrpc.client.ParamsType;
import com.github.arteam.simplejsonrpc.core.annotation.JsonRpcMethod;
import com.github.arteam.simplejsonrpc.core.annotation.JsonRpcOptional;
import com.github.arteam.simplejsonrpc.core.annotation.JsonRpcParam;
@@ -9,6 +11,7 @@ import java.util.List;
import java.util.Map;
@JsonRpcService
@JsonRpcParams(ParamsType.ARRAY)
public interface BitcoindClientService {
@JsonRpcMethod("uptime")
long uptime();
@@ -44,7 +47,7 @@ public interface BitcoindClientService {
Object getRawTransaction(@JsonRpcParam("txid") String txid, @JsonRpcParam("verbose") boolean verbose);
@JsonRpcMethod("gettransaction")
Map<String, Object> getTransaction(@JsonRpcParam("txid") String txid, @JsonRpcParam("verbose") boolean verbose);
Map<String, Object> getTransaction(@JsonRpcParam("txid") String txid, @JsonRpcParam("include_watchonly") boolean includeWatchOnly, @JsonRpcParam("verbose") boolean verbose);
@JsonRpcMethod("getmempoolentry")
MempoolEntry getMempoolEntry(@JsonRpcParam("txid") String txid);
@@ -4,6 +4,7 @@ import com.github.arteam.simplejsonrpc.client.Transport;
import com.sparrowwallet.drongo.Network;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.io.Server;
import com.sparrowwallet.sparrow.net.Protocol;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -23,6 +24,7 @@ public class BitcoindTransport implements Transport {
private static final Logger log = LoggerFactory.getLogger(BitcoindTransport.class);
public static final String COOKIE_FILENAME = ".cookie";
private final Server bitcoindServer;
private URL bitcoindUrl;
private File cookieFile;
private Long cookieFileTimestamp;
@@ -39,8 +41,13 @@ public class BitcoindTransport implements Transport {
}
private BitcoindTransport(Server bitcoindServer, String bitcoindWallet) {
this.bitcoindServer = bitcoindServer;
try {
this.bitcoindUrl = new URL(bitcoindServer.getUrl() + "/wallet/" + bitcoindWallet);
String serverUrl = bitcoindServer.getUrl();
if(!bitcoindServer.getHostAndPort().hasPort()) {
serverUrl += ":" + Network.get().getDefaultPort();
}
this.bitcoindUrl = new URL(serverUrl + "/wallet/" + bitcoindWallet);
} catch(MalformedURLException e) {
log.error("Malformed Bitcoin Core RPC URL", e);
}
@@ -49,7 +56,7 @@ public class BitcoindTransport implements Transport {
@Override
public String pass(String request) throws IOException {
Proxy proxy = AppServices.getProxy();
HttpURLConnection connection = proxy == null ? (HttpURLConnection)bitcoindUrl.openConnection() : (HttpURLConnection)bitcoindUrl.openConnection(proxy);
HttpURLConnection connection = proxy != null && Protocol.isOnionAddress(bitcoindServer) ? (HttpURLConnection)bitcoindUrl.openConnection(proxy) : (HttpURLConnection)bitcoindUrl.openConnection();
if(connection instanceof HttpsURLConnection httpsURLConnection) {
SSLSocketFactory sslSocketFactory = getTrustAllSocketFactory();
@@ -150,7 +150,7 @@ public class ElectrumServerService {
return bitcoindClient.getBitcoindService().getRawTransaction(tx_hash, true);
} catch(JsonRpcException e) {
try {
Map<String, Object> txInfo = bitcoindClient.getBitcoindService().getTransaction(tx_hash, true);
Map<String, Object> txInfo = bitcoindClient.getBitcoindService().getTransaction(tx_hash, true, true);
Object decoded = txInfo.get("decoded");
if(decoded instanceof Map<?, ?>) {
Map<String, Object> decodedMap = (Map<String, Object>)decoded;
@@ -172,7 +172,7 @@ public class ElectrumServerService {
}
} else {
try {
return bitcoindClient.getBitcoindService().getTransaction(tx_hash, false).get("hex");
return bitcoindClient.getBitcoindService().getTransaction(tx_hash, true, false).get("hex");
} catch(JsonRpcException e) {
try {
return bitcoindClient.getBitcoindService().getRawTransaction(tx_hash, false);
@@ -66,7 +66,7 @@ public class Payjoin {
try {
String base64Psbt = psbt.getPublicCopy().toBase64String();
String appendQuery = "v=1";
String appendQuery = "v=1&minfeerate=" + AppServices.getMinimumRelayFeeRate();
int changeOutputIndex = getChangeOutputIndex();
long maxAdditionalFeeContribution = 0;
if(changeOutputIndex > -1) {
@@ -441,6 +441,7 @@ public class PayNymController {
if(!storage.isPersisted(addedWallet)) {
try {
storage.saveWallet(addedWallet);
EventManager.get().post(new NewChildWalletSavedEvent(storage, masterWallet, addedWallet));
} catch(Exception e) {
log.error("Error saving wallet", e);
AppServices.showErrorDialog("Error saving wallet " + addedWallet.getName(), e.getMessage());
@@ -33,7 +33,7 @@ public class PayNymService {
private final JavaHttpClientService httpClientService;
public PayNymService(HostAndPort torProxy) {
this.httpClientService = new JavaHttpClientService(torProxy);
this.httpClientService = new JavaHttpClientService(torProxy, 120000);
}
public Observable<Map<String, Object>> createPayNym(Wallet wallet) {
@@ -1,32 +1,44 @@
package com.sparrowwallet.sparrow.preferences;
import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.TextfieldDialog;
import com.sparrowwallet.sparrow.control.UnlabeledToggleSwitch;
import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.io.Server;
import com.sparrowwallet.sparrow.net.BlockExplorer;
import com.sparrowwallet.sparrow.net.ExchangeSource;
import com.sparrowwallet.sparrow.net.FeeRatesSource;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.scene.control.ComboBox;
import javafx.util.StringConverter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Arrays;
import java.util.Currency;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
public class GeneralPreferencesController extends PreferencesDetailController {
private static final Logger log = LoggerFactory.getLogger(GeneralPreferencesController.class);
@FXML
private ComboBox<BitcoinUnit> bitcoinUnit;
private static final Server CUSTOM_BLOCK_EXPLORER = new Server("http://custom.block.explorer");
@FXML
private ComboBox<FeeRatesSource> feeRatesSource;
@FXML
private ComboBox<Server> blockExplorers;
@FXML
private ComboBox<Currency> fiatCurrency;
@@ -63,17 +75,6 @@ public class GeneralPreferencesController extends PreferencesDetailController {
@Override
public void initializeView(Config config) {
if(config.getBitcoinUnit() != null) {
bitcoinUnit.setValue(config.getBitcoinUnit());
} else {
bitcoinUnit.setValue(BitcoinUnit.AUTO);
}
bitcoinUnit.valueProperty().addListener((observable, oldValue, newValue) -> {
config.setBitcoinUnit(newValue);
EventManager.get().post(new BitcoinUnitChangedEvent(newValue));
});
if(config.getFeeRatesSource() != null) {
feeRatesSource.setValue(config.getFeeRatesSource());
} else {
@@ -86,6 +87,62 @@ public class GeneralPreferencesController extends PreferencesDetailController {
EventManager.get().post(new FeeRatesSourceChangedEvent(newValue));
});
blockExplorers.setItems(getBlockExplorerList());
blockExplorers.setConverter(new StringConverter<>() {
@Override
public String toString(Server server) {
if(server == null || server == BlockExplorer.NONE.getServer()) {
return "None";
}
if(server == CUSTOM_BLOCK_EXPLORER) {
return "Custom...";
}
return server.getHost();
}
@Override
public Server fromString(String string) {
return null;
}
});
blockExplorers.valueProperty().addListener((observable, oldValue, newValue) -> {
if(newValue != null) {
if(newValue == CUSTOM_BLOCK_EXPLORER) {
TextfieldDialog textfieldDialog = new TextfieldDialog();
textfieldDialog.setTitle("Enter Block Explorer URL");
textfieldDialog.setHeaderText("Enter the URL of the block explorer.\n\nIf present, the characters {0} will be replaced with the txid.\nFor example, https://localhost or https://localhost/tx/{0}\n");
textfieldDialog.getEditor().setPromptText("https://localhost");
Optional<String> optUrl = textfieldDialog.showAndWait();
if(optUrl.isPresent() && !optUrl.get().isEmpty()) {
try {
Server server = getBlockExplorer(optUrl.get());
config.setBlockExplorer(server);
Platform.runLater(() -> {
blockExplorers.getSelectionModel().select(-1);
blockExplorers.setItems(getBlockExplorerList());
blockExplorers.setValue(Config.get().getBlockExplorer());
});
} catch(Exception e) {
AppServices.showErrorDialog("Invalid URL", "The URL " + optUrl.get() + " is not valid.");
blockExplorers.setValue(oldValue);
}
} else {
blockExplorers.setValue(oldValue);
}
} else {
Config.get().setBlockExplorer(newValue);
}
}
});
if(config.getBlockExplorer() != null) {
blockExplorers.setValue(config.getBlockExplorer());
} else {
blockExplorers.getSelectionModel().select(0);
}
if(config.getExchangeSource() != null) {
exchangeSource.setValue(config.getExchangeSource());
} else {
@@ -134,6 +191,23 @@ public class GeneralPreferencesController extends PreferencesDetailController {
});
}
private static Server getBlockExplorer(String serverUrl) {
String url = serverUrl.trim();
if(url.endsWith("/")) {
url = url.substring(0, url.length() - 1);
}
return new Server(url);
}
private ObservableList<Server> getBlockExplorerList() {
List<Server> servers = Arrays.stream(BlockExplorer.values()).map(BlockExplorer::getServer).collect(Collectors.toList());
if(Config.get().getBlockExplorer() != null && !servers.contains(Config.get().getBlockExplorer())) {
servers.add(Config.get().getBlockExplorer());
}
servers.add(CUSTOM_BLOCK_EXPLORER);
return FXCollections.observableList(servers);
}
private void updateCurrencies(ExchangeSource exchangeSource) {
ExchangeSource.CurrenciesService currenciesService = new ExchangeSource.CurrenciesService(exchangeSource);
currenciesService.setOnSucceeded(event -> {
@@ -243,6 +243,7 @@ public class ServerPreferencesController extends PreferencesDetailController {
proxyHost.textProperty().addListener(getProxyListener(config));
proxyPort.textProperty().addListener(getProxyListener(config));
corePort.setPromptText("e.g. " + Network.get().getDefaultPort());
coreDataDirField.managedProperty().bind(coreDataDirField.visibleProperty());
coreUserPassField.managedProperty().bind(coreUserPassField.visibleProperty());
coreUserPassField.visibleProperty().bind(coreDataDirField.visibleProperty().not());
@@ -734,6 +735,10 @@ public class ServerPreferencesController extends PreferencesDetailController {
private ChangeListener<String> getBitcoinCoreListener(Config config) {
return (observable, oldValue, newValue) -> {
Server existingServer = config.getRecentCoreServers().stream().filter(server -> coreHost.getText().equals(server.getAlias())).findFirst().orElse(null);
if(existingServer != null && !existingServer.portEquals(corePort.getText())) {
coreHost.setText(existingServer.getHost());
existingServer = null;
}
coreHost.setLeft(existingServer == null ? null : getGlyph(FontAwesome5.Glyph.TAG, null));
setCoreServerInConfig(config);
};
@@ -769,6 +774,10 @@ public class ServerPreferencesController extends PreferencesDetailController {
private ChangeListener<String> getElectrumServerListener(Config config) {
return (observable, oldValue, newValue) -> {
Server existingServer = config.getRecentElectrumServers().stream().filter(server -> electrumHost.getText().equals(server.getAlias())).findFirst().orElse(null);
if(existingServer != null && !existingServer.portEquals(electrumPort.getText())) {
electrumHost.setText(existingServer.getHost());
existingServer = null;
}
electrumHost.setLeft(existingServer == null ? null : getGlyph(FontAwesome5.Glyph.TAG, null));
setElectrumServerInConfig(config);
};
@@ -8,13 +8,9 @@ import com.samourai.wallet.cahoots.Cahoots;
import com.samourai.wallet.cahoots.CahootsType;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.psbt.PSBTParseException;
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
import com.sparrowwallet.drongo.wallet.Status;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletNode;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.control.*;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.paynym.PayNymDialog;
import com.sparrowwallet.sparrow.paynym.PayNymService;
import io.reactivex.rxjavafx.schedulers.JavaFxScheduler;
@@ -298,10 +294,20 @@ public class CounterpartyController extends SorobanController {
sorobanProgressLabel.setText("Creating mix transaction...");
Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId);
Map<BlockTransactionHashIndex, WalletNode> walletUtxos = wallet.getWalletUtxos();
Wallet counterpartyWallet;
if(counterpartyCahootsWallet.getAccount() == StandardAccount.WHIRLPOOL_POSTMIX.getAccountNumber() && cahootsType == CahootsType.STOWAWAY
&& !counterpartyCahootsWallet.getWallet().isMasterWallet()) {
//Counterparty cannot participate in Stowaway using Postmix wallet, switch to master wallet
counterpartyWallet = counterpartyCahootsWallet.getWallet().getMasterWallet();
counterpartyCahootsWallet = soroban.getCahootsWallet(counterpartyWallet, 1);
} else {
counterpartyWallet = wallet;
}
Map<BlockTransactionHashIndex, WalletNode> walletUtxos = counterpartyWallet.getWalletUtxos();
for(Map.Entry<BlockTransactionHashIndex, WalletNode> entry : walletUtxos.entrySet()) {
if(entry.getKey().getStatus() != Status.FROZEN) {
counterpartyCahootsWallet.addUtxo(entry.getValue(), wallet.getWalletTransaction(entry.getKey().getHash()), (int)entry.getKey().getIndex());
counterpartyCahootsWallet.addUtxo(entry.getValue(), counterpartyWallet.getWalletTransaction(entry.getKey().getHash()), (int)entry.getKey().getIndex());
}
}
@@ -325,7 +331,7 @@ public class CounterpartyController extends SorobanController {
Transaction transaction = getTransaction(cahoots);
if(transaction != null) {
transactionProperty.set(transaction);
updateTransactionDiagram(transactionDiagram, wallet, null, transaction);
updateTransactionDiagram(transactionDiagram, counterpartyWallet, null, transaction);
next();
}
} catch(PSBTParseException e) {
@@ -62,6 +62,10 @@ public class SparrowCahootsWallet extends SimpleCahootsWallet {
addUtxo(account, cahootsUtxo);
}
public Wallet getWallet() {
return wallet;
}
public int getAccount() {
return account;
}
@@ -1,5 +1,6 @@
package com.sparrowwallet.sparrow.terminal.wallet;
import com.google.common.eventbus.Subscribe;
import com.googlecode.lanterna.TerminalSize;
import com.googlecode.lanterna.gui2.*;
import com.googlecode.lanterna.gui2.dialogs.TextInputDialogBuilder;
@@ -21,8 +22,8 @@ import com.sparrowwallet.sparrow.io.Storage;
import com.sparrowwallet.sparrow.io.StorageException;
import com.sparrowwallet.sparrow.terminal.SparrowTerminal;
import com.sparrowwallet.sparrow.wallet.Function;
import com.sparrowwallet.sparrow.wallet.SettingsWalletForm;
import com.sparrowwallet.sparrow.wallet.WalletForm;
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolServices;
import javafx.application.Platform;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -276,6 +277,13 @@ public class SettingsDialog extends WalletDialog {
return lines;
}
@Subscribe
public void newChildWalletSaved(NewChildWalletSavedEvent event) {
if(event.getMasterWalletId().equals(getWalletForm().getMasterWalletId())) {
((SettingsWalletForm)getWalletForm()).childWalletSaved(event.getChildWallet());
}
}
public enum PasswordRequirement {
UPDATE_NEW("Add a password to the wallet?\nLeave empty for no password:", "No Password"),
UPDATE_EMPTY("This wallet has no password.\nAdd a password to the wallet?\nLeave empty for no password:", "No Password"),
@@ -4,6 +4,7 @@ import com.googlecode.lanterna.TerminalSize;
import com.googlecode.lanterna.gui2.*;
import com.googlecode.lanterna.gui2.dialogs.DialogWindow;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.io.Storage;
import com.sparrowwallet.sparrow.terminal.SparrowTerminal;
import com.sparrowwallet.sparrow.wallet.Function;
@@ -48,7 +49,7 @@ public class WalletActionsDialog extends DialogWindow {
SettingsDialog settingsDialog = getWalletData().getSettingsDialog();
settingsDialog.showDialog(SparrowTerminal.get().getGui());
});
if(getWalletData().getWalletForm().getWallet().isEncrypted()) {
if(!Storage.NO_PASSWORD_KEY.equals(getWalletData().getWalletForm().getStorage().getEncryptionPubKey())) {
actions.addItem("Lock", () -> {
close();
SparrowTerminal.get().lockWallet(getWalletData().getWalletForm().getStorage());
@@ -16,10 +16,7 @@ import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.CurrencyRate;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.UnitFormat;
import com.sparrowwallet.sparrow.event.ChildWalletsAddedEvent;
import com.sparrowwallet.sparrow.event.StorageEvent;
import com.sparrowwallet.sparrow.event.TimedEvent;
import com.sparrowwallet.sparrow.event.WalletHistoryClearedEvent;
import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.io.Storage;
import com.sparrowwallet.sparrow.terminal.SparrowTerminal;
@@ -151,6 +148,7 @@ public class WalletDialog extends DialogWindow {
if(!storage.isPersisted(childWallet)) {
try {
storage.saveWallet(childWallet);
EventManager.get().post(new NewChildWalletSavedEvent(storage, masterWallet, childWallet));
} catch(Exception e) {
log.error("Error saving wallet", e);
showErrorDialog("Error saving wallet " + childWallet.getName(), e.getMessage());
@@ -16,6 +16,7 @@ import com.sparrowwallet.sparrow.control.*;
import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5Brands;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.io.Device;
import com.sparrowwallet.sparrow.net.ElectrumServer;
import com.sparrowwallet.sparrow.io.Storage;
@@ -78,6 +79,12 @@ public class HeadersController extends TransactionFormController implements Init
@FXML
private IdLabel id;
@FXML
private ToggleButton copyTxid;
@FXML
private ToggleButton openBlockExplorer;
@FXML
private TransactionDiagram transactionDiagram;
@@ -806,11 +813,13 @@ public class HeadersController extends TransactionFormController implements Init
addStyleClass(size, UNFINALIZED_TXID_CLASS);
addStyleClass(virtualSize, UNFINALIZED_TXID_CLASS);
addStyleClass(feeRate, UNFINALIZED_TXID_CLASS);
openBlockExplorer.setDisable(true);
} else {
id.getStyleClass().remove(UNFINALIZED_TXID_CLASS);
size.getStyleClass().remove(UNFINALIZED_TXID_CLASS);
virtualSize.getStyleClass().remove(UNFINALIZED_TXID_CLASS);
feeRate.getStyleClass().remove(UNFINALIZED_TXID_CLASS);
openBlockExplorer.setDisable(Config.get().isBlockExplorerDisabled());
}
}
@@ -821,11 +830,18 @@ public class HeadersController extends TransactionFormController implements Init
}
public void copyId(ActionEvent event) {
copyTxid.setSelected(false);
ClipboardContent content = new ClipboardContent();
content.putString(headersForm.getTransaction().calculateTxId(false).toString());
Clipboard.getSystemClipboard().setContent(content);
}
public void openInBlockExplorer(ActionEvent event) {
openBlockExplorer.setSelected(false);
String txid = headersForm.getTransaction().calculateTxId(false).toString();
AppServices.openBlockExplorer(txid);
}
public void setLocktimeToCurrentHeight(ActionEvent event) {
if(AppServices.getCurrentBlockHeight() != null && locktimeBlock.isEditable()) {
locktimeBlock.getValueFactory().setValue(AppServices.getCurrentBlockHeight());
@@ -2,6 +2,7 @@ package com.sparrowwallet.sparrow.wallet;
import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.drongo.*;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
@@ -56,6 +57,9 @@ public class KeystoreController extends WalletFormController implements Initiali
@FXML
private Label type;
@FXML
private Button exportButton;
@FXML
private Button viewSeedButton;
@@ -129,6 +133,7 @@ public class KeystoreController extends WalletFormController implements Initiali
}
}
exportButton.managedProperty().bind(exportButton.visibleProperty());
viewSeedButton.managedProperty().bind(viewSeedButton.visibleProperty());
viewKeyButton.managedProperty().bind(viewKeyButton.visibleProperty());
changePinButton.managedProperty().bind(changePinButton.visibleProperty());
@@ -136,7 +141,7 @@ public class KeystoreController extends WalletFormController implements Initiali
displayXpubQR.managedProperty().bind(displayXpubQR.visibleProperty());
displayXpubQR.visibleProperty().bind(scanXpubQR.visibleProperty().not());
updateType();
updateType(keystore.isValid() && !getWalletForm().getWallet().isValid());
label.setText(keystore.getLabel());
@@ -280,9 +285,10 @@ public class KeystoreController extends WalletFormController implements Initiali
));
}
private void updateType() {
private void updateType(boolean showExport) {
type.setText(getTypeLabel(keystore));
type.setGraphic(getTypeIcon(keystore));
exportButton.setVisible(showExport && getWalletForm().getWallet().getPolicyType() == PolicyType.MULTI);
viewSeedButton.setVisible(keystore.getSource() == KeystoreSource.SW_SEED && keystore.hasSeed());
viewKeyButton.setVisible(keystore.getSource() == KeystoreSource.SW_SEED && keystore.hasMasterPrivateExtendedKey());
changePinButton.setVisible(keystore.getWalletModel().isCard());
@@ -309,9 +315,9 @@ public class KeystoreController extends WalletFormController implements Initiali
private String getTypeLabel(Keystore keystore) {
switch (keystore.getSource()) {
case HW_USB:
return "Connected Hardware Wallet (" + keystore.getWalletModel().toDisplayString() + ")";
return "Connected Wallet (" + keystore.getWalletModel().toDisplayString() + ")";
case HW_AIRGAPPED:
return "Airgapped Hardware Wallet (" + keystore.getWalletModel().toDisplayString() + ")";
return "Airgapped Wallet (" + keystore.getWalletModel().toDisplayString() + ")";
case SW_SEED:
return "Software Wallet";
case SW_WATCH:
@@ -348,7 +354,7 @@ public class KeystoreController extends WalletFormController implements Initiali
}
private void launchImportDialog(KeystoreSource initialSource) {
boolean restrictSource = keystoreSourceToggleGroup.getToggles().stream().anyMatch(toggle -> ((ToggleButton)toggle).isDisabled());
boolean restrictSource = keystore.getSource() != KeystoreSource.SW_WATCH && keystoreSourceToggleGroup.getToggles().stream().anyMatch(toggle -> ((ToggleButton)toggle).isDisabled());
KeyDerivation requiredDerivation = restrictSource ? keystore.getKeyDerivation() : null;
WalletModel requiredModel = restrictSource ? keystore.getWalletModel() : null;
KeystoreImportDialog dlg = new KeystoreImportDialog(getWalletForm().getWallet(), initialSource, requiredDerivation, requiredModel, restrictSource);
@@ -367,7 +373,7 @@ public class KeystoreController extends WalletFormController implements Initiali
keystore.setSeed(importedKeystore.getSeed());
keystore.setBip47ExtendedPrivateKey(importedKeystore.getBip47ExtendedPrivateKey());
updateType();
updateType(true);
label.setText(keystore.getLabel());
fingerprint.setText(keystore.getKeyDerivation().getMasterFingerprint());
derivation.setText(keystore.getKeyDerivation().getDerivationPath());
@@ -381,6 +387,11 @@ public class KeystoreController extends WalletFormController implements Initiali
}
}
public void export(ActionEvent event) {
KeystoreExportDialog keystoreExportDialog = new KeystoreExportDialog(keystore);
keystoreExportDialog.showAndWait();
}
public void showPrivate(ActionEvent event) {
int keystoreIndex = getWalletForm().getWallet().getKeystores().indexOf(keystore);
Wallet copy = getWalletForm().getWallet().copy();
@@ -614,4 +625,11 @@ public class KeystoreController extends WalletFormController implements Initiali
}
}
}
@Subscribe
public void walletSettingsChanged(WalletSettingsChangedEvent event) {
if(event.getWalletId().equals(walletForm.getWalletId())) {
exportButton.setVisible(false);
}
}
}
@@ -1196,6 +1196,7 @@ public class SendController extends WalletFormController implements Initializabl
if(!storage.isPersisted(childWallet)) {
try {
storage.saveWallet(childWallet);
EventManager.get().post(new NewChildWalletSavedEvent(storage, masterWallet, childWallet));
} catch(Exception e) {
AppServices.showErrorDialog("Error saving wallet " + childWallet.getName(), e.getMessage());
}
@@ -1381,6 +1382,7 @@ public class SendController extends WalletFormController implements Initializabl
if(!storage.isPersisted(addedWallet)) {
try {
storage.saveWallet(addedWallet);
EventManager.get().post(new NewChildWalletSavedEvent(storage, masterWallet, addedWallet));
} catch(Exception e) {
log.error("Error saving wallet", e);
AppServices.showErrorDialog("Error saving wallet " + addedWallet.getName(), e.getMessage());
@@ -416,24 +416,28 @@ public class SettingsController extends WalletFormController implements Initiali
try {
OutputDescriptor editedOutputDescriptor = OutputDescriptor.getOutputDescriptor(text.trim().replace("\\", ""));
Wallet editedWallet = editedOutputDescriptor.toWallet();
editedWallet.setName(getWalletForm().getWallet().getName());
editedWallet.setBirthDate(getWalletForm().getWallet().getBirthDate());
editedWallet.setGapLimit(getWalletForm().getWallet().getGapLimit());
editedWallet.setWatchLast(getWalletForm().getWallet().getWatchLast());
keystoreTabs.getTabs().removeAll(keystoreTabs.getTabs());
totalKeystores.unbind();
totalKeystores.setValue(0);
walletForm.setWallet(editedWallet);
initialising = true;
setFieldsFromWallet(editedWallet);
EventManager.get().post(new SettingsChangedEvent(editedWallet, SettingsChangedEvent.Type.POLICY));
replaceWallet(editedWallet);
} catch(Exception e) {
AppServices.showErrorDialog("Invalid output descriptor", e.getMessage());
}
}
private void replaceWallet(Wallet editedWallet) {
editedWallet.setName(getWalletForm().getWallet().getName());
editedWallet.setBirthDate(getWalletForm().getWallet().getBirthDate());
editedWallet.setGapLimit(getWalletForm().getWallet().getGapLimit());
editedWallet.setWatchLast(getWalletForm().getWallet().getWatchLast());
editedWallet.setMasterWallet(getWalletForm().getWallet().getMasterWallet());
keystoreTabs.getTabs().removeAll(keystoreTabs.getTabs());
totalKeystores.unbind();
totalKeystores.setValue(0);
walletForm.setWallet(editedWallet);
initialising = true;
setFieldsFromWallet(editedWallet);
EventManager.get().post(new SettingsChangedEvent(editedWallet, SettingsChangedEvent.Type.POLICY));
}
public void showDescriptor(ActionEvent event) {
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(walletForm.getWallet(), KeyPurpose.DEFAULT_PURPOSES, null);
String outputDescriptorString = outputDescriptor.toString(walletForm.getWallet().isValid());
@@ -631,6 +635,7 @@ public class SettingsController extends WalletFormController implements Initiali
if(!storage.isPersisted(childWallet)) {
try {
storage.saveWallet(childWallet);
EventManager.get().post(new NewChildWalletSavedEvent(storage, masterWallet, childWallet));
} catch(Exception e) {
log.error("Error saving wallet", e);
AppServices.showErrorDialog("Error saving wallet " + childWallet.getName(), e.getMessage());
@@ -718,6 +723,31 @@ public class SettingsController extends WalletFormController implements Initiali
}
}
@Subscribe
public void newChildWalletSaved(NewChildWalletSavedEvent event) {
if(event.getMasterWalletId().equals(walletForm.getMasterWalletId())) {
((SettingsWalletForm)walletForm).childWalletSaved(event.getChildWallet());
}
}
@Subscribe
public void existingWalletImported(ExistingWalletImportedEvent event) {
if(event.getExistingWalletId().equals(getWalletForm().getWalletId())) {
List<Keystore> importedKeystores = event.getImportedWallet().getKeystores();
List<Keystore> nonWatchKeystores = walletForm.getWallet().getKeystores().stream().filter(k -> k.isValid() && k.getSource() != KeystoreSource.SW_WATCH).collect(Collectors.toList());
for(Keystore nonWatchKeystore : nonWatchKeystores) {
Optional<Keystore> optReplacedKeystore = importedKeystores.stream().filter(k -> nonWatchKeystore.getExtendedPublicKey().equals(k.getExtendedPublicKey())).findFirst();
if(optReplacedKeystore.isPresent()) {
int index = importedKeystores.indexOf(optReplacedKeystore.get());
importedKeystores.remove(index);
importedKeystores.add(index, nonWatchKeystore);
}
}
replaceWallet(event.getImportedWallet());
}
}
private void saveWallet(boolean changePassword, boolean suggestChangePassword) {
ECKey existingPubKey = walletForm.getStorage().getEncryptionPubKey();
@@ -242,4 +242,15 @@ public class SettingsWalletForm extends WalletForm {
return changedKeystores;
}
public void childWalletSaved(Wallet childWallet) {
//Update the child wallets for the master wallet of the local walletCopy to ensure all KeystoreEncryptionChangedEvents are posted
Wallet masterWalletCopy = walletCopy.isMasterWallet() ? walletCopy : walletCopy.getMasterWallet();
Wallet childWalletCopy = masterWalletCopy.getChildWallet(childWallet.getName());
if(childWalletCopy == null) {
childWalletCopy = childWallet.copy();
childWalletCopy.setMasterWallet(masterWalletCopy);
masterWalletCopy.getChildWallets().add(childWallet.copy());
}
}
}
@@ -103,6 +103,7 @@ public class WalletController extends WalletFormController implements Initializa
WalletForm walletForm = getWalletForm();
if(function.equals(Function.SETTINGS)) {
walletForm = new SettingsWalletForm(getWalletForm().getStorage(), getWalletForm().getWallet());
getWalletForm().setSettingsWalletForm(walletForm);
}
controller.setWalletForm(walletForm);
@@ -41,6 +41,7 @@ public class WalletForm {
private final PublishSubject<WalletNode> refreshNodesSubject;
private WalletForm settingsWalletForm;
private final List<WalletForm> nestedWalletForms = new ArrayList<>();
private WalletTransactionsEntry walletTransactionsEntry;
@@ -96,6 +97,14 @@ public class WalletForm {
throw new UnsupportedOperationException("Only SettingsWalletForm supports setWallet");
}
public WalletForm getSettingsWalletForm() {
return settingsWalletForm;
}
void setSettingsWalletForm(WalletForm settingsWalletForm) {
this.settingsWalletForm = settingsWalletForm;
}
public List<WalletForm> getNestedWalletForms() {
return nestedWalletForms;
}
@@ -189,6 +198,7 @@ public class WalletForm {
if(!storage.isPersisted(addedWallet)) {
try {
storage.saveWallet(addedWallet);
EventManager.get().post(new NewChildWalletSavedEvent(storage, wallet, addedWallet));
} catch(Exception e) {
log.error("Error saving wallet", e);
AppServices.showErrorDialog("Error saving wallet " + addedWallet.getName(), e.getMessage());
@@ -11,7 +11,6 @@
<?import tornadofx.control.Field?>
<?import javafx.collections.FXCollections?>
<?import javafx.geometry.Insets?>
<?import com.sparrowwallet.drongo.BitcoinUnit?>
<?import com.sparrowwallet.sparrow.net.ExchangeSource?>
<?import com.sparrowwallet.sparrow.control.UnlabeledToggleSwitch?>
<?import com.sparrowwallet.sparrow.control.HelpLabel?>
@@ -30,18 +29,6 @@
<Form GridPane.columnIndex="0" GridPane.rowIndex="0">
<Fieldset inputGrow="SOMETIMES" text="Bitcoin" styleClass="wideLabelFieldSet">
<Field text="Display unit:">
<ComboBox fx:id="bitcoinUnit">
<items>
<FXCollections fx:factory="observableArrayList">
<BitcoinUnit fx:constant="AUTO" />
<BitcoinUnit fx:constant="BTC" />
<BitcoinUnit fx:constant="SATOSHIS" />
</FXCollections>
</items>
</ComboBox>
<HelpLabel helpText="Display unit for bitcoin amounts.\nAuto displays amounts over 1 BTC in BTC, and amounts under that in satoshis."/>
</Field>
<Field text="Fee rates source:">
<ComboBox fx:id="feeRatesSource">
<items>
@@ -54,6 +41,10 @@
</ComboBox>
<HelpLabel helpText="Recommended fee rates can be sourced from your connected Electrum server, or an external source."/>
</Field>
<Field text="Block explorer:">
<ComboBox fx:id="blockExplorers" />
<HelpLabel helpText="An external block explorer can be configured to open a transaction from any transaction tab."/>
</Field>
</Fieldset>
<Fieldset inputGrow="SOMETIMES" text="Fiat" styleClass="wideLabelFieldSet">
<Field text="Currency:">
@@ -45,11 +45,26 @@
<Fieldset text="Transaction" inputGrow="SOMETIMES" wrapWidth="620">
<Field text="Txid:" styleClass="label-button">
<IdLabel fx:id="id"/>
<Button maxWidth="25" minWidth="-Infinity" prefWidth="30" text="Cy" onAction="#copyId">
<graphic>
<Glyph fontFamily="FontAwesome" icon="COPY" prefWidth="15" />
</graphic>
</Button>
<SegmentedButton>
<buttons>
<ToggleButton fx:id="copyTxid" onAction="#copyId">
<graphic>
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="COPY" />
</graphic>
<tooltip>
<Tooltip text="Copy the transaction id to the clipboard"/>
</tooltip>
</ToggleButton>
<ToggleButton fx:id="openBlockExplorer" onAction="#openInBlockExplorer">
<graphic>
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="UP_RIGHT_FROM_SQUARE" />
</graphic>
<tooltip>
<Tooltip text="Open transaction in the configured block explorer"/>
</tooltip>
</ToggleButton>
</buttons>
</SegmentedButton>
</Field>
</Fieldset>
</Form>
@@ -24,30 +24,37 @@
<Fieldset inputGrow="SOMETIMES" text="">
<Field text="Type:">
<Label fx:id="type" graphicTextGap="5"/>
<Button fx:id="viewSeedButton" text="View Seed..." graphicTextGap="5" onAction="#showPrivate">
<graphic>
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="KEY" />
</graphic>
<tooltip>
<Tooltip text="View mnemonic seed words"/>
</tooltip>
</Button>
<Button fx:id="viewKeyButton" text="View Key..." graphicTextGap="5" onAction="#showPrivate">
<graphic>
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="KEY" />
</graphic>
<tooltip>
<Tooltip text="View master private key"/>
</tooltip>
</Button>
<Button fx:id="changePinButton" text="Change Pin..." graphicTextGap="5" onAction="#changeCardPin">
<graphic>
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="WIFI" />
</graphic>
<tooltip>
<Tooltip text="Change the PIN of current card"/>
</tooltip>
</Button>
<HBox spacing="10" alignment="CENTER_LEFT">
<Button fx:id="viewSeedButton" text="View Seed..." graphicTextGap="5" onAction="#showPrivate">
<graphic>
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="KEY" />
</graphic>
<tooltip>
<Tooltip text="View mnemonic seed words"/>
</tooltip>
</Button>
<Button fx:id="viewKeyButton" text="View Key..." graphicTextGap="5" onAction="#showPrivate">
<graphic>
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="KEY" />
</graphic>
<tooltip>
<Tooltip text="View master private key"/>
</tooltip>
</Button>
<Button fx:id="changePinButton" text="Change Pin..." graphicTextGap="5" onAction="#changeCardPin">
<graphic>
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="WIFI" />
</graphic>
<tooltip>
<Tooltip text="Change the PIN of current card"/>
</tooltip>
</Button>
<Button fx:id="exportButton" text="Export..." onAction="#export">
<tooltip>
<Tooltip text="Export this keystore as a signer for a multisig wallet"/>
</tooltip>
</Button>
</HBox>
<Pane HBox.hgrow="ALWAYS" />
<Button fx:id="importButton" text="Import..." graphicTextGap="5" onAction="#importKeystore">
<graphic>
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.