Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb40d991bb | ||
|
|
5fe6a7196a | ||
|
|
f06b859c82 | ||
|
|
80dd59928e | ||
|
|
f22f76464a | ||
|
|
dfe1f16495 | ||
|
|
90a9030ecb | ||
|
|
4ab33a373c | ||
|
|
10e751d6e1 | ||
|
|
5f40669af7 | ||
|
|
9fc096569a | ||
|
|
41636f7152 | ||
|
|
fc5d48de6f | ||
|
|
2a7f14a4ed | ||
|
|
1cb6778502 | ||
|
|
7f254e763d | ||
|
|
e0ff42b6a4 | ||
|
|
88fc8f5017 | ||
|
|
d7072928de | ||
|
|
0cc9ddba05 | ||
|
|
e3799cd0a8 | ||
|
|
ea6b30326e | ||
|
|
38768885e2 | ||
|
|
c360177c31 | ||
|
|
e88ea0bac1 | ||
|
|
a66b36c59c | ||
|
|
fb3b674b65 | ||
|
|
eff0e201f3 | ||
|
|
58d10cbba4 | ||
|
|
29bce8a9bc |
@@ -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:
|
||||
|
||||
@@ -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,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')
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 6.6 KiB |