mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2026-07-31 20:16:15 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
685fef6b76 | ||
|
|
9a4eb22396 | ||
|
|
57eb3a77e7 | ||
|
|
dcc63046bd | ||
|
|
8388a7fed5 | ||
|
|
d5830399b7 | ||
|
|
38b8fa3b86 | ||
|
|
6f26c7e138 | ||
|
|
9e869ae69b | ||
|
|
4a7b0b20ad | ||
|
|
8fc971c07c | ||
|
|
cb884d97cb | ||
|
|
942db9df48 | ||
|
|
193f88b88f | ||
|
|
6785bccf0e | ||
|
|
579b9a685b | ||
|
|
57b3fd99bc | ||
|
|
c3ae98f3d1 | ||
|
|
d635815607 | ||
|
|
e524396aaf | ||
|
|
08edc04c6d | ||
|
|
e88d6265b4 | ||
|
|
5d91f033c0 | ||
|
|
771bd1545c | ||
|
|
f65fec66bb | ||
|
|
9c6dbeec69 | ||
|
|
563af71ed2 | ||
|
|
a4579c953a | ||
|
|
6a3c3de67c | ||
|
|
f8fa929166 | ||
|
|
6aaf532051 | ||
|
|
fb59bdfff4 | ||
|
|
c8aa1b0a19 | ||
|
|
11a201b3f5 | ||
|
|
b74741bccb | ||
|
|
b8739ace4f | ||
|
|
cd674ee9be | ||
|
|
8b3ed8171b | ||
|
|
28ee16279c | ||
|
|
ff43db0842 | ||
|
|
8ddac9acba | ||
|
|
0c2554da72 | ||
|
|
1274d56fce | ||
|
|
8d9e253f93 | ||
|
|
9166160008 | ||
|
|
35eb8fcdf9 | ||
|
|
d34c8b8bde | ||
|
|
80a079cbee | ||
|
|
5d181d1abb | ||
|
|
6fbd332b4d | ||
|
|
211e5952aa | ||
|
|
4078c61d6b | ||
|
|
8b7d1e6888 | ||
|
|
7c6daf2e11 | ||
|
|
e1f405d886 | ||
|
|
1d73956853 | ||
|
|
205f78151a | ||
|
|
5853eb01f5 | ||
|
|
1a5e71f852 | ||
|
|
7053e7b225 | ||
|
|
5bee26a8cc | ||
|
|
b9caff352b | ||
|
|
73089ce99c | ||
|
|
34080ae8b3 | ||
|
|
307c7d53f8 | ||
|
|
a6d2a6563b | ||
|
|
577e570c99 | ||
|
|
06411d4e2f | ||
|
|
3e287bfb2d |
@@ -32,8 +32,16 @@ jobs:
|
||||
restore-keys: ${{ runner.os }}-gradle
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew jpackage
|
||||
- name: Package zip distribution
|
||||
if: ${{ runner.os == 'Windows' }}
|
||||
run: ./gradlew packageZipDistribution
|
||||
- name: Package tar distribution
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
run: ./gradlew packageTarDistribution
|
||||
- name: Upload Artifacts
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: Sparrow Build - ${{ runner.os }}
|
||||
path: build/jpackage/*
|
||||
path: |
|
||||
build/jpackage/*
|
||||
!build/jpackage/Sparrow/
|
||||
@@ -57,7 +57,7 @@ As a fallback, the network (mainnet, testnet, regtest or signet) can also be set
|
||||
|
||||
A final fallback which can be useful when running the Sparrow binary is to create a file called ``network-testnet`` in the Sparrow home folder (see below) to configure the testnet network.
|
||||
|
||||
Note that if you are connecting to an Electrum server when using testnet, that server will need to running on testnet configuration as well.
|
||||
Note that if you are connecting to an Electrum server when using testnet, that server will need to be running on testnet configuration as well.
|
||||
|
||||
When not explicitly configured using the command line argument above, Sparrow stores its mainnet config file, log file and wallets in a home folder location appropriate to the operating system:
|
||||
|
||||
|
||||
+29
-4
@@ -5,7 +5,7 @@ plugins {
|
||||
id 'org.beryx.jlink' version '2.22.0'
|
||||
}
|
||||
|
||||
def sparrowVersion = '1.3.0'
|
||||
def sparrowVersion = '1.3.2'
|
||||
def os = org.gradle.internal.os.OperatingSystem.current()
|
||||
def osName = os.getFamilyName()
|
||||
if(os.macOsX) {
|
||||
@@ -20,7 +20,7 @@ repositories {
|
||||
maven { url 'https://oss.sonatype.org/content/groups/public' }
|
||||
maven { url 'https://mymavenrepo.com/repo/29EACwkkGcoOKnbx3bxN/' }
|
||||
maven { url 'https://jitpack.io' }
|
||||
maven { url 'http://maven.openimaj.org/' }
|
||||
maven { url 'https://maven.ecs.soton.ac.uk/content/groups/maven.openimaj.org/' }
|
||||
}
|
||||
|
||||
tasks.withType(AbstractArchiveTask) {
|
||||
@@ -51,7 +51,7 @@ dependencies {
|
||||
implementation('com.github.arteam:simple-json-rpc-server:1.0') {
|
||||
exclude group: 'org.slf4j'
|
||||
}
|
||||
implementation('com.sparrowwallet:hummingbird:1.5.2')
|
||||
implementation('com.sparrowwallet:hummingbird:1.5.5')
|
||||
implementation('com.nativelibs4java:bridj:0.7-20140918-3') {
|
||||
exclude group: 'com.google.android.tools', module: 'dx'
|
||||
}
|
||||
@@ -71,6 +71,9 @@ dependencies {
|
||||
}
|
||||
implementation('dev.bwt:bwt-jni:0.1.7')
|
||||
implementation('net.sourceforge.javacsv:javacsv:2.0')
|
||||
implementation('org.slf4j:jul-to-slf4j:1.7.30') {
|
||||
exclude group: 'org.slf4j'
|
||||
}
|
||||
testImplementation('junit:junit:4.12')
|
||||
}
|
||||
|
||||
@@ -112,7 +115,7 @@ run {
|
||||
"--add-opens=java.base/java.net=com.sparrowwallet.sparrow"]
|
||||
|
||||
if(os.macOsX) {
|
||||
applicationDefaultJvmArgs += ["-Xdock:name=Sparrow", "-Xdock:icon=/Users/scy/git/sparrow/src/main/resources/sparrow.png",
|
||||
applicationDefaultJvmArgs += ["-Xdock:name=Sparrow", "-Xdock:icon=/Users/scy/git/sparrow/src/main/resources/sparrow-large.png",
|
||||
"--add-opens=javafx.graphics/com.sun.glass.ui.mac=centerdevice.nsmenufx"]
|
||||
}
|
||||
}
|
||||
@@ -178,3 +181,25 @@ jlink {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
task removeGroupWritePermission(type: Exec) {
|
||||
commandLine 'chmod', '-R', 'g-w', "$buildDir/jpackage/Sparrow"
|
||||
}
|
||||
|
||||
task packageZipDistribution(type: Zip) {
|
||||
archiveFileName = "Sparrow-${sparrowVersion}.zip"
|
||||
destinationDirectory = file("$buildDir/jpackage")
|
||||
from("$buildDir/jpackage/") {
|
||||
include "Sparrow/**"
|
||||
}
|
||||
}
|
||||
|
||||
task packageTarDistribution(type: Tar) {
|
||||
dependsOn removeGroupWritePermission
|
||||
archiveFileName = "sparrow-${sparrowVersion}.tar.gz"
|
||||
destinationDirectory = file("$buildDir/jpackage")
|
||||
compression = Compression.GZIP
|
||||
from("$buildDir/jpackage/") {
|
||||
include "Sparrow/**"
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
Submodule drongo updated: e974c3f422...49654b7c82
@@ -21,7 +21,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.3.0</string>
|
||||
<string>1.3.2</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories -->
|
||||
|
||||
@@ -21,6 +21,7 @@ import com.sparrowwallet.drongo.psbt.PSBTParseException;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.sparrow.control.*;
|
||||
import com.sparrowwallet.sparrow.event.*;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.io.*;
|
||||
import com.sparrowwallet.sparrow.net.ElectrumServer;
|
||||
import com.sparrowwallet.sparrow.net.ServerType;
|
||||
@@ -49,10 +50,15 @@ import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.input.*;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.stage.*;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.StageStyle;
|
||||
import javafx.stage.Window;
|
||||
import javafx.util.Duration;
|
||||
import org.controlsfx.control.Notifications;
|
||||
import org.controlsfx.control.StatusBar;
|
||||
import org.controlsfx.glyphfont.FontAwesome;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -61,7 +67,6 @@ import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.ParseException;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.sparrowwallet.sparrow.AppServices.*;
|
||||
@@ -70,16 +75,29 @@ public class AppController implements Initializable {
|
||||
private static final Logger log = LoggerFactory.getLogger(AppController.class);
|
||||
|
||||
public static final String DRAG_OVER_CLASS = "drag-over";
|
||||
public static final double TAB_LABEL_GRAPHIC_OPACITY_INACTIVE = 0.8;
|
||||
public static final double TAB_LABEL_GRAPHIC_OPACITY_ACTIVE = 0.95;
|
||||
public static final String LOADING_TRANSACTIONS_MESSAGE = "Loading wallet, select Transactions tab to view...";
|
||||
public static final String CONNECTION_FAILED_PREFIX = "Connection failed: ";
|
||||
|
||||
@FXML
|
||||
private MenuItem saveTransaction;
|
||||
|
||||
@FXML
|
||||
private Menu savePSBT;
|
||||
|
||||
@FXML
|
||||
private MenuItem exportWallet;
|
||||
|
||||
@FXML
|
||||
private Menu fileMenu;
|
||||
|
||||
@FXML
|
||||
private Menu viewMenu;
|
||||
|
||||
@FXML
|
||||
private Menu toolsMenu;
|
||||
|
||||
@FXML
|
||||
private Menu helpMenu;
|
||||
|
||||
@@ -98,9 +116,15 @@ public class AppController implements Initializable {
|
||||
@FXML
|
||||
private CheckMenuItem hideEmptyUsedAddresses;
|
||||
|
||||
@FXML
|
||||
private CheckMenuItem useHdCameraResolution;
|
||||
|
||||
@FXML
|
||||
private CheckMenuItem showTxHex;
|
||||
|
||||
@FXML
|
||||
private MenuItem minimizeToTray;
|
||||
|
||||
@FXML
|
||||
private MenuItem refreshWallet;
|
||||
|
||||
@@ -128,7 +152,7 @@ public class AppController implements Initializable {
|
||||
}
|
||||
|
||||
void initializeView() {
|
||||
setOsxApplicationMenu();
|
||||
setPlatformApplicationMenu();
|
||||
|
||||
rootStack.setOnDragOver(event -> {
|
||||
if(event.getGestureSource() != rootStack && event.getDragboard().hasFiles()) {
|
||||
@@ -163,7 +187,11 @@ public class AppController implements Initializable {
|
||||
});
|
||||
|
||||
tabs.getSelectionModel().selectedItemProperty().addListener((observable, old_val, selectedTab) -> {
|
||||
tabs.getTabs().forEach(tab -> ((Label)tab.getGraphic()).getGraphic().setOpacity(TAB_LABEL_GRAPHIC_OPACITY_INACTIVE));
|
||||
if(selectedTab != null) {
|
||||
Label tabLabel = (Label)selectedTab.getGraphic();
|
||||
tabLabel.getGraphic().setOpacity(TAB_LABEL_GRAPHIC_OPACITY_ACTIVE);
|
||||
|
||||
TabData tabData = (TabData)selectedTab.getUserData();
|
||||
if(tabData.getType() == TabData.TabType.TRANSACTION) {
|
||||
EventManager.get().post(new TransactionTabSelectedEvent(selectedTab));
|
||||
@@ -228,7 +256,9 @@ public class AppController implements Initializable {
|
||||
|
||||
openWalletsInNewWindows.setSelected(Config.get().isOpenWalletsInNewWindows());
|
||||
hideEmptyUsedAddresses.setSelected(Config.get().isHideEmptyUsedAddresses());
|
||||
useHdCameraResolution.setSelected(Config.get().isHdCapture());
|
||||
showTxHex.setSelected(Config.get().isShowTransactionHex());
|
||||
savePSBT.visibleProperty().bind(saveTransaction.visibleProperty().not());
|
||||
exportWallet.setDisable(true);
|
||||
refreshWallet.disableProperty().bind(Bindings.or(exportWallet.disableProperty(), Bindings.or(serverToggle.disableProperty(), AppServices.onlineProperty().not())));
|
||||
|
||||
@@ -246,8 +276,9 @@ public class AppController implements Initializable {
|
||||
openTransactionIdItem.disableProperty().bind(onlineProperty().not());
|
||||
}
|
||||
|
||||
private void setOsxApplicationMenu() {
|
||||
if(org.controlsfx.tools.Platform.getCurrent() == org.controlsfx.tools.Platform.OSX) {
|
||||
private void setPlatformApplicationMenu() {
|
||||
org.controlsfx.tools.Platform platform = org.controlsfx.tools.Platform.getCurrent();
|
||||
if(platform == org.controlsfx.tools.Platform.OSX) {
|
||||
MenuToolkit tk = MenuToolkit.toolkit();
|
||||
MenuItem preferences = new MenuItem("Preferences...");
|
||||
preferences.setOnAction(this::openPreferences);
|
||||
@@ -259,7 +290,14 @@ public class AppController implements Initializable {
|
||||
tk.setApplicationMenu(defaultApplicationMenu);
|
||||
|
||||
fileMenu.getItems().removeIf(item -> item.getStyleClass().contains("osxHide"));
|
||||
toolsMenu.getItems().removeIf(item -> item.getStyleClass().contains("osxHide"));
|
||||
helpMenu.getItems().removeIf(item -> item.getStyleClass().contains("osxHide"));
|
||||
} else if(platform == org.controlsfx.tools.Platform.WINDOWS) {
|
||||
toolsMenu.getItems().removeIf(item -> item.getStyleClass().contains("windowsHide"));
|
||||
}
|
||||
|
||||
if(platform == org.controlsfx.tools.Platform.UNIX || !TrayManager.isSupported()) {
|
||||
viewMenu.getItems().remove(minimizeToTray);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -313,6 +351,24 @@ public class AppController implements Initializable {
|
||||
return null;
|
||||
}
|
||||
|
||||
public void installUdevRules(ActionEvent event) {
|
||||
Hwi.EnumerateService enumerateService = new Hwi.EnumerateService(null);
|
||||
enumerateService.setOnSucceeded(workerStateEvent -> {
|
||||
Platform.runLater(this::showInstallUdevMessage);
|
||||
});
|
||||
enumerateService.setOnFailed(workerStateEvent -> {
|
||||
Platform.runLater(this::showInstallUdevMessage);
|
||||
});
|
||||
enumerateService.start();
|
||||
}
|
||||
|
||||
public void showInstallUdevMessage() {
|
||||
TextAreaDialog dialog = new TextAreaDialog("sudo " + Config.get().getHwi().getAbsolutePath() + " installudevrules");
|
||||
dialog.setTitle("Install Udev Rules");
|
||||
dialog.getDialogPane().setHeaderText("Installing udev rules ensures devices can connect over USB.\nThis command requires root privileges.\nOpen a shell and enter the following:");
|
||||
dialog.showAndWait();
|
||||
}
|
||||
|
||||
public void openTransactionFromFile(ActionEvent event) {
|
||||
Stage window = new Stage();
|
||||
|
||||
@@ -446,45 +502,27 @@ public class AppController implements Initializable {
|
||||
TransactionTabData transactionTabData = (TransactionTabData)tabData;
|
||||
Transaction transaction = transactionTabData.getTransaction();
|
||||
|
||||
//Save a transaction if the PSBT is null or transaction has already been extracted, otherwise save PSBT
|
||||
//The PSBT's transaction is not altered with transaction extraction, but the extracted transaction is stored in TransactionData
|
||||
boolean saveTx = (transactionTabData.getPsbt() == null || transactionTabData.getPsbt().getTransaction() != transaction);
|
||||
|
||||
Stage window = new Stage();
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle("Save " + (saveTx ? "Transaction" : "PSBT"));
|
||||
fileChooser.setTitle("Save Transaction");
|
||||
|
||||
String fileName = selectedTab.getText();
|
||||
String fileName = ((Label)selectedTab.getGraphic()).getText();
|
||||
if(fileName != null && !fileName.isEmpty()) {
|
||||
if(transactionTabData.getPsbt() != null) {
|
||||
if(!fileName.endsWith(".psbt")) {
|
||||
fileName += ".psbt";
|
||||
}
|
||||
} else if(!fileName.endsWith(".txn")) {
|
||||
fileName += ".txn";
|
||||
}
|
||||
if(fileName.endsWith(".psbt")) {
|
||||
fileName = fileName.substring(0, fileName.length() - ".psbt".length());
|
||||
}
|
||||
|
||||
if(saveTx && fileName.endsWith(".psbt")) {
|
||||
fileName = fileName.replace(".psbt", "") + ".txn";
|
||||
}
|
||||
if(!fileName.endsWith(".txn")) {
|
||||
fileName += ".txn";
|
||||
}
|
||||
|
||||
fileChooser.setInitialFileName(fileName);
|
||||
fileChooser.setInitialFileName(fileName);
|
||||
}
|
||||
|
||||
File file = fileChooser.showSaveDialog(window);
|
||||
if(file != null) {
|
||||
if(!saveTx && !file.getName().toLowerCase().endsWith(".psbt")) {
|
||||
file = new File(file.getAbsolutePath() + ".psbt");
|
||||
}
|
||||
|
||||
try {
|
||||
try(PrintWriter writer = new PrintWriter(file, StandardCharsets.UTF_8)) {
|
||||
if(saveTx) {
|
||||
writer.print(Utils.bytesToHex(transaction.bitcoinSerialize()));
|
||||
} else {
|
||||
writer.print(transactionTabData.getPsbt().toBase64String());
|
||||
}
|
||||
}
|
||||
try(PrintWriter writer = new PrintWriter(file, StandardCharsets.UTF_8)) {
|
||||
writer.print(Utils.bytesToHex(transaction.bitcoinSerialize()));
|
||||
} catch(IOException e) {
|
||||
log.error("Error saving transaction", e);
|
||||
AppServices.showErrorDialog("Error saving transaction", "Cannot write to " + file.getAbsolutePath());
|
||||
@@ -493,6 +531,58 @@ public class AppController implements Initializable {
|
||||
}
|
||||
}
|
||||
|
||||
public void savePSBTBinary(ActionEvent event) {
|
||||
savePSBT(false);
|
||||
}
|
||||
|
||||
public void savePSBTText(ActionEvent event) {
|
||||
savePSBT(true);
|
||||
}
|
||||
|
||||
public void savePSBT(boolean asText) {
|
||||
Tab selectedTab = tabs.getSelectionModel().getSelectedItem();
|
||||
TabData tabData = (TabData)selectedTab.getUserData();
|
||||
if(tabData.getType() == TabData.TabType.TRANSACTION) {
|
||||
TransactionTabData transactionTabData = (TransactionTabData)tabData;
|
||||
|
||||
Stage window = new Stage();
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle("Save PSBT");
|
||||
|
||||
String fileName = ((Label)selectedTab.getGraphic()).getText();
|
||||
if(fileName != null && !fileName.isEmpty()) {
|
||||
if(!fileName.endsWith(".psbt")) {
|
||||
fileName += ".psbt";
|
||||
}
|
||||
|
||||
if(asText) {
|
||||
fileName += ".txt";
|
||||
}
|
||||
|
||||
fileChooser.setInitialFileName(fileName);
|
||||
}
|
||||
|
||||
File file = fileChooser.showSaveDialog(window);
|
||||
if(file != null) {
|
||||
if(!asText && !file.getName().toLowerCase().endsWith(".psbt")) {
|
||||
file = new File(file.getAbsolutePath() + ".psbt");
|
||||
}
|
||||
|
||||
try(FileOutputStream outputStream = new FileOutputStream(file)) {
|
||||
if(asText) {
|
||||
PrintWriter writer = new PrintWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
|
||||
writer.print(transactionTabData.getPsbt().toBase64String());
|
||||
writer.flush();
|
||||
} else {
|
||||
outputStream.write(transactionTabData.getPsbt().serialize());
|
||||
}
|
||||
} catch(IOException e) {
|
||||
log.error("Error saving PSBT", e);
|
||||
AppServices.showErrorDialog("Error saving PSBT", "Cannot write to " + file.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public List<WalletTabData> getOpenWalletTabData() {
|
||||
List<WalletTabData> openWalletTabData = new ArrayList<>();
|
||||
@@ -553,6 +643,11 @@ public class AppController implements Initializable {
|
||||
EventManager.get().post(new HideEmptyUsedAddressesStatusEvent(item.isSelected()));
|
||||
}
|
||||
|
||||
public void useHdCameraResolution(ActionEvent event) {
|
||||
CheckMenuItem item = (CheckMenuItem)event.getSource();
|
||||
Config.get().setHdCapture(item.isSelected());
|
||||
}
|
||||
|
||||
public void showTxHex(ActionEvent event) {
|
||||
CheckMenuItem item = (CheckMenuItem)event.getSource();
|
||||
Config.get().setShowTransactionHex(item.isSelected());
|
||||
@@ -802,7 +897,7 @@ public class AppController implements Initializable {
|
||||
}
|
||||
|
||||
Storage storage = new Storage(Storage.getWalletFile(wallet.getName()));
|
||||
WalletPasswordDialog dlg = new WalletPasswordDialog(WalletPasswordDialog.PasswordRequirement.UPDATE_NEW);
|
||||
WalletPasswordDialog dlg = new WalletPasswordDialog(wallet.getName(), WalletPasswordDialog.PasswordRequirement.UPDATE_NEW);
|
||||
Optional<SecureString> password = dlg.showAndWait();
|
||||
if(password.isPresent()) {
|
||||
if(password.get().length() == 0) {
|
||||
@@ -877,6 +972,10 @@ public class AppController implements Initializable {
|
||||
messageSignDialog.showAndWait();
|
||||
}
|
||||
|
||||
public void minimizeToTray(ActionEvent event) {
|
||||
AppServices.get().minimizeStage((Stage)tabs.getScene().getWindow());
|
||||
}
|
||||
|
||||
public void refreshWallet(ActionEvent event) {
|
||||
Tab selectedTab = tabs.getSelectionModel().getSelectedItem();
|
||||
TabData tabData = (TabData)selectedTab.getUserData();
|
||||
@@ -886,7 +985,7 @@ public class AppController implements Initializable {
|
||||
Wallet pastWallet = wallet.copy();
|
||||
walletTabData.getStorage().backupTempWallet();
|
||||
wallet.clearHistory();
|
||||
EventManager.get().post(new WalletSettingsChangedEvent(wallet, pastWallet, walletTabData.getStorage().getWalletFile()));
|
||||
EventManager.get().post(new WalletAddressesChangedEvent(wallet, pastWallet, walletTabData.getStorage().getWalletFile()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -920,7 +1019,14 @@ public class AppController implements Initializable {
|
||||
if(!name.equals(wallet.getName())) {
|
||||
wallet.setName(name);
|
||||
}
|
||||
Tab tab = new Tab(name);
|
||||
Tab tab = new Tab("");
|
||||
Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.WALLET);
|
||||
glyph.setFontSize(10.0);
|
||||
glyph.setOpacity(TAB_LABEL_GRAPHIC_OPACITY_ACTIVE);
|
||||
Label tabLabel = new Label(name);
|
||||
tabLabel.setGraphic(glyph);
|
||||
tabLabel.setGraphicTextGap(5.0);
|
||||
tab.setGraphic(tabLabel);
|
||||
tab.setContextMenu(getTabContextMenu(tab));
|
||||
tab.setClosable(true);
|
||||
FXMLLoader walletLoader = new FXMLLoader(getClass().getResource("wallet/wallet.fxml"));
|
||||
@@ -1022,7 +1128,7 @@ public class AppController implements Initializable {
|
||||
//As per BIP174, combine PSBTs with matching transactions so long as they are not yet finalized
|
||||
transactionTabData.getPsbt().combine(psbt);
|
||||
if(name != null && !name.isEmpty()) {
|
||||
tab.setText(name);
|
||||
((Label)tab.getGraphic()).setText(name);
|
||||
}
|
||||
|
||||
EventManager.get().post(new PSBTCombinedEvent(transactionTabData.getPsbt()));
|
||||
@@ -1037,7 +1143,7 @@ public class AppController implements Initializable {
|
||||
}
|
||||
|
||||
if(name != null && !name.isEmpty()) {
|
||||
tab.setText(name);
|
||||
((Label)tab.getGraphic()).setText(name);
|
||||
}
|
||||
|
||||
EventManager.get().post(new PSBTFinalizedEvent(transactionTabData.getPsbt()));
|
||||
@@ -1057,7 +1163,14 @@ public class AppController implements Initializable {
|
||||
tabName = "[" + transaction.getTxId().toString().substring(0, 6) + "]";
|
||||
}
|
||||
|
||||
Tab tab = new Tab(tabName);
|
||||
Tab tab = new Tab("");
|
||||
Glyph glyph = new Glyph("FontAwesome", FontAwesome.Glyph.SEND);
|
||||
glyph.setFontSize(10.0);
|
||||
glyph.setOpacity(TAB_LABEL_GRAPHIC_OPACITY_ACTIVE);
|
||||
Label tabLabel = new Label(tabName);
|
||||
tabLabel.setGraphic(glyph);
|
||||
tabLabel.setGraphicTextGap(5.0);
|
||||
tab.setGraphic(tabLabel);
|
||||
tab.setContextMenu(getTabContextMenu(tab));
|
||||
tab.setClosable(true);
|
||||
FXMLLoader transactionLoader = new FXMLLoader(getClass().getResource("transaction/transaction.fxml"));
|
||||
@@ -1138,6 +1251,57 @@ public class AppController implements Initializable {
|
||||
EventManager.get().post(new ThemeChangedEvent(selectedTheme));
|
||||
}
|
||||
|
||||
private void tabLabelStartAnimation(Wallet wallet) {
|
||||
tabs.getTabs().stream().filter(tab -> tab.getUserData() instanceof WalletTabData && ((WalletTabData)tab.getUserData()).getWallet() == wallet).forEach(this::tabLabelStartAnimation);
|
||||
}
|
||||
|
||||
private void tabLabelStartAnimation(Transaction transaction) {
|
||||
tabs.getTabs().stream().filter(tab -> tab.getUserData() instanceof TransactionTabData && ((TransactionTabData)tab.getUserData()).getTransaction().getTxId().equals(transaction.getTxId())).forEach(this::tabLabelStartAnimation);
|
||||
}
|
||||
|
||||
private void tabLabelStartAnimation(Tab tab) {
|
||||
Label tabLabel = (Label) tab.getGraphic();
|
||||
if(tabLabel.getUserData() == null) {
|
||||
FadeTransition fadeTransition = new FadeTransition(Duration.millis(1000), tabLabel.getGraphic());
|
||||
fadeTransition.setFromValue(tabLabel.getGraphic().getOpacity());
|
||||
fadeTransition.setToValue(0.1);
|
||||
fadeTransition.setAutoReverse(true);
|
||||
fadeTransition.setCycleCount(Animation.INDEFINITE);
|
||||
fadeTransition.play();
|
||||
tabLabel.setUserData(fadeTransition);
|
||||
}
|
||||
}
|
||||
|
||||
private void tabLabelAddFailure(Tab tab) {
|
||||
Label tabLabel = (Label)tab.getGraphic();
|
||||
if(!tabLabel.getStyleClass().contains("failure")) {
|
||||
tabLabel.getGraphic().getStyleClass().add("failure");
|
||||
}
|
||||
}
|
||||
|
||||
private void tabLabelStopAnimation(Wallet wallet) {
|
||||
tabs.getTabs().stream().filter(tab -> tab.getUserData() instanceof WalletTabData && ((WalletTabData)tab.getUserData()).getWallet() == wallet).forEach(this::tabLabelStopAnimation);
|
||||
}
|
||||
|
||||
private void tabLabelStopAnimation(Transaction transaction) {
|
||||
tabs.getTabs().stream().filter(tab -> tab.getUserData() instanceof TransactionTabData && ((TransactionTabData)tab.getUserData()).getTransaction().getTxId().equals(transaction.getTxId())).forEach(this::tabLabelStopAnimation);
|
||||
}
|
||||
|
||||
private void tabLabelStopAnimation(Tab tab) {
|
||||
Label tabLabel = (Label) tab.getGraphic();
|
||||
if(tabLabel.getUserData() != null) {
|
||||
FadeTransition fadeTransition = (FadeTransition)tabLabel.getUserData();
|
||||
fadeTransition.stop();
|
||||
tabLabel.setUserData(null);
|
||||
tabLabel.getGraphic().setOpacity(tab.isSelected() ? TAB_LABEL_GRAPHIC_OPACITY_ACTIVE : TAB_LABEL_GRAPHIC_OPACITY_INACTIVE);
|
||||
}
|
||||
}
|
||||
|
||||
private void tabLabelRemoveFailure(Tab tab) {
|
||||
Label tabLabel = (Label)tab.getGraphic();
|
||||
tabLabel.getGraphic().getStyleClass().remove("failure");
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void themeChanged(ThemeChangedEvent event) {
|
||||
String darkCss = getClass().getResource("darktheme.css").toExternalForm();
|
||||
@@ -1167,15 +1331,19 @@ public class AppController implements Initializable {
|
||||
if(event instanceof TransactionTabSelectedEvent) {
|
||||
TransactionTabSelectedEvent txTabEvent = (TransactionTabSelectedEvent)event;
|
||||
TransactionTabData transactionTabData = txTabEvent.getTransactionTabData();
|
||||
saveTransaction.setDisable(false);
|
||||
saveTransaction.setText("Save " + (transactionTabData.getPsbt() == null || transactionTabData.getPsbt().getTransaction() != transactionTabData.getTransaction() ? "Transaction..." : "PSBT..."));
|
||||
if(transactionTabData.getPsbt() == null || transactionTabData.getPsbt().getTransaction() != transactionTabData.getTransaction()) {
|
||||
saveTransaction.setVisible(true);
|
||||
saveTransaction.setDisable(false);
|
||||
} else {
|
||||
saveTransaction.setVisible(false);
|
||||
}
|
||||
exportWallet.setDisable(true);
|
||||
showTxHex.setDisable(false);
|
||||
} else if(event instanceof WalletTabSelectedEvent) {
|
||||
WalletTabSelectedEvent walletTabEvent = (WalletTabSelectedEvent)event;
|
||||
WalletTabData walletTabData = walletTabEvent.getWalletTabData();
|
||||
saveTransaction.setVisible(true);
|
||||
saveTransaction.setDisable(true);
|
||||
saveTransaction.setText("Save Transaction...");
|
||||
exportWallet.setDisable(walletTabData.getWallet() == null || !walletTabData.getWallet().isValid());
|
||||
showTxHex.setDisable(true);
|
||||
}
|
||||
@@ -1189,14 +1357,15 @@ public class AppController implements Initializable {
|
||||
if(tabData instanceof TransactionTabData) {
|
||||
TransactionTabData transactionTabData = (TransactionTabData)tabData;
|
||||
if(transactionTabData.getTransaction() == event.getFinalTransaction()) {
|
||||
saveTransaction.setText("Save Transaction...");
|
||||
saveTransaction.setVisible(true);
|
||||
saveTransaction.setDisable(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletSettingsChanged(WalletSettingsChangedEvent event) {
|
||||
public void walletAddressesChanged(WalletAddressesChangedEvent event) {
|
||||
Tab tab = tabs.getSelectionModel().getSelectedItem();
|
||||
TabData tabData = (TabData)tab.getUserData();
|
||||
if(tabData instanceof WalletTabData) {
|
||||
@@ -1282,6 +1451,7 @@ public class AppController implements Initializable {
|
||||
@Subscribe
|
||||
public void versionUpdated(VersionUpdatedEvent event) {
|
||||
Hyperlink versionUpdateLabel = new Hyperlink("Sparrow " + event.getVersion() + " available");
|
||||
versionUpdateLabel.getStyleClass().add("version-hyperlink");
|
||||
versionUpdateLabel.setOnAction(event1 -> {
|
||||
AppServices.get().getApplication().getHostServices().showDocument("https://www.sparrowwallet.com/download");
|
||||
});
|
||||
@@ -1350,8 +1520,7 @@ public class AppController implements Initializable {
|
||||
|
||||
@Subscribe
|
||||
public void connectionFailed(ConnectionFailedEvent event) {
|
||||
String reason = event.getException().getCause() != null ? event.getException().getCause().getMessage() : event.getException().getMessage();
|
||||
String status = "Connection failed: " + reason;
|
||||
String status = CONNECTION_FAILED_PREFIX + event.getMessage();
|
||||
statusUpdated(new StatusEvent(status));
|
||||
}
|
||||
|
||||
@@ -1364,7 +1533,7 @@ public class AppController implements Initializable {
|
||||
@Subscribe
|
||||
public void disconnection(DisconnectionEvent event) {
|
||||
serverToggle.setDisable(false);
|
||||
if(!AppServices.isConnecting() && !AppServices.isConnected() && !statusBar.getText().startsWith("Connection error")) {
|
||||
if(!AppServices.isConnecting() && !AppServices.isConnected() && !statusBar.getText().startsWith(CONNECTION_FAILED_PREFIX)) {
|
||||
statusUpdated(new StatusEvent("Disconnected"));
|
||||
}
|
||||
if(statusTimeline == null || statusTimeline.getStatus() != Animation.Status.RUNNING) {
|
||||
@@ -1375,7 +1544,7 @@ public class AppController implements Initializable {
|
||||
@Subscribe
|
||||
public void walletTabsClosed(WalletTabsClosedEvent event) {
|
||||
if(event.getClosedWalletTabData().stream().map(WalletTabData::getWallet).anyMatch(loadingWallets::remove) && loadingWallets.isEmpty()) {
|
||||
if(statusBar.getText().equals("Loading transactions...")) {
|
||||
if(statusBar.getText().equals(LOADING_TRANSACTIONS_MESSAGE)) {
|
||||
statusBar.setText("");
|
||||
}
|
||||
if(statusTimeline == null || statusTimeline.getStatus() != Animation.Status.RUNNING) {
|
||||
@@ -1384,30 +1553,50 @@ public class AppController implements Initializable {
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void transactionReferences(TransactionReferencesEvent event) {
|
||||
if(AppServices.isConnected() && event instanceof TransactionReferencesStartedEvent) {
|
||||
tabLabelStartAnimation(event.getTransaction());
|
||||
} else {
|
||||
tabLabelStopAnimation(event.getTransaction());
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletHistoryStarted(WalletHistoryStartedEvent event) {
|
||||
if(AppServices.isConnected() && getOpenWallets().containsKey(event.getWallet())) {
|
||||
statusUpdated(new StatusEvent("Loading transactions...", 120));
|
||||
if(statusTimeline == null || statusTimeline.getStatus() != Animation.Status.RUNNING) {
|
||||
statusBar.setProgress(-1);
|
||||
loadingWallets.add(event.getWallet());
|
||||
if(event.getWalletNode() == null && event.getWallet().getTransactions().isEmpty()) {
|
||||
statusUpdated(new StatusEvent(LOADING_TRANSACTIONS_MESSAGE, 120));
|
||||
if(statusTimeline == null || statusTimeline.getStatus() != Animation.Status.RUNNING) {
|
||||
statusBar.setProgress(-1);
|
||||
loadingWallets.add(event.getWallet());
|
||||
}
|
||||
}
|
||||
tabLabelStartAnimation(event.getWallet());
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletHistoryFinished(WalletHistoryFinishedEvent event) {
|
||||
if(getOpenWallets().containsKey(event.getWallet())) {
|
||||
if(statusBar.getText().equals("Loading transactions...")) {
|
||||
if(statusBar.getText().equals(LOADING_TRANSACTIONS_MESSAGE)) {
|
||||
statusBar.setText("");
|
||||
}
|
||||
if(statusTimeline == null || statusTimeline.getStatus() != Animation.Status.RUNNING) {
|
||||
statusBar.setProgress(0);
|
||||
}
|
||||
tabLabelStopAnimation(event.getWallet());
|
||||
loadingWallets.remove(event.getWallet());
|
||||
tabs.getTabs().stream().filter(tab -> tab.getUserData() instanceof WalletTabData && ((WalletTabData)tab.getUserData()).getWallet() == event.getWallet()).forEach(this::tabLabelRemoveFailure);
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletHistoryFailed(WalletHistoryFailedEvent event) {
|
||||
walletHistoryFinished(new WalletHistoryFinishedEvent(event.getWallet()));
|
||||
tabs.getTabs().stream().filter(tab -> tab.getUserData() instanceof WalletTabData && ((WalletTabData) tab.getUserData()).getWallet() == event.getWallet()).forEach(this::tabLabelAddFailure);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void bwtBootStatus(BwtBootStatusEvent event) {
|
||||
serverToggle.setDisable(true);
|
||||
@@ -1467,6 +1656,12 @@ public class AppController implements Initializable {
|
||||
statusUpdated(new StatusEvent(event.getStatus()));
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void torExternalStatus(TorExternalStatusEvent event) {
|
||||
serverToggle.setDisable(false);
|
||||
statusUpdated(new StatusEvent(event.getStatus()));
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void newBlock(NewBlockEvent event) {
|
||||
setServerToggleTooltip(event.getHeight());
|
||||
|
||||
@@ -2,12 +2,14 @@ package com.sparrowwallet.sparrow;
|
||||
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.google.common.net.HostAndPort;
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
import com.sparrowwallet.drongo.uri.BitcoinURI;
|
||||
import com.sparrowwallet.drongo.wallet.KeystoreSource;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.control.TrayManager;
|
||||
import com.sparrowwallet.sparrow.event.*;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.io.Device;
|
||||
@@ -63,6 +65,8 @@ public class AppServices {
|
||||
|
||||
private final Map<Window, List<WalletTabData>> walletWindows = new LinkedHashMap<>();
|
||||
|
||||
private TrayManager trayManager;
|
||||
|
||||
private static final BooleanProperty onlineProperty = new SimpleBooleanProperty(false);
|
||||
|
||||
private ExchangeSource.RatesService ratesService;
|
||||
@@ -93,7 +97,7 @@ public class AppServices {
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean online) {
|
||||
if(online) {
|
||||
if(Config.get().requiresTor() && !isTorRunning()) {
|
||||
if(Config.get().requiresInternalTor() && !isTorRunning()) {
|
||||
torService.start();
|
||||
} else {
|
||||
restartServices();
|
||||
@@ -121,7 +125,7 @@ public class AppServices {
|
||||
onlineProperty.addListener(onlineServicesListener);
|
||||
|
||||
if(config.getMode() == Mode.ONLINE) {
|
||||
if(config.requiresTor()) {
|
||||
if(config.requiresInternalTor()) {
|
||||
torService.start();
|
||||
} else {
|
||||
restartServices();
|
||||
@@ -139,7 +143,7 @@ public class AppServices {
|
||||
restartService(ratesService);
|
||||
}
|
||||
|
||||
if(config.isCheckNewVersions()) {
|
||||
if(config.isCheckNewVersions() && Network.get() == Network.MAINNET) {
|
||||
restartService(versionCheckService);
|
||||
}
|
||||
}
|
||||
@@ -207,6 +211,37 @@ public class AppServices {
|
||||
connectionService.setRestartOnFailure(false);
|
||||
}
|
||||
|
||||
if(failEvent.getSource().getException() instanceof TlsServerException && failEvent.getSource().getException().getCause() != null) {
|
||||
TlsServerException tlsServerException = (TlsServerException)failEvent.getSource().getException();
|
||||
connectionService.setRestartOnFailure(false);
|
||||
if(tlsServerException.getCause().getMessage().contains("PKIX path building failed")) {
|
||||
File crtFile = Config.get().getElectrumServerCert();
|
||||
if(crtFile != null) {
|
||||
AppServices.showErrorDialog("SSL Handshake Failed", "The configured server certificate at " + crtFile.getAbsolutePath() + " did not match the certificate provided by the server at " + tlsServerException.getServer().getHost() + "." +
|
||||
"\n\nThis may indicate a man-in-the-middle attack!" +
|
||||
"\n\nChange the configured server certificate if you would like to proceed.");
|
||||
} else {
|
||||
crtFile = Storage.getCertificateFile(tlsServerException.getServer().getHost());
|
||||
if(crtFile != null) {
|
||||
Optional<ButtonType> optButton = AppServices.showErrorDialog("SSL Handshake Failed", "The certificate provided by the server at " + tlsServerException.getServer().getHost() + " appears to have changed." +
|
||||
"\n\nThis may indicate a man-in-the-middle attack!" +
|
||||
"\n\nDo you still want to proceed?", ButtonType.NO, ButtonType.YES);
|
||||
if(optButton.isPresent() && optButton.get() == ButtonType.YES) {
|
||||
crtFile.delete();
|
||||
Platform.runLater(() -> restartService(connectionService));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(failEvent.getSource().getException() instanceof ProxyServerException && Config.get().isUseProxy() && Config.get().requiresTor()) {
|
||||
Config.get().setUseProxy(false);
|
||||
Platform.runLater(() -> restartService(torService));
|
||||
return;
|
||||
}
|
||||
|
||||
onlineProperty.removeListener(onlineServicesListener);
|
||||
onlineProperty.setValue(false);
|
||||
onlineProperty.addListener(onlineServicesListener);
|
||||
@@ -288,6 +323,23 @@ public class AppServices {
|
||||
EventManager.get().post(new TorReadyStatusEvent());
|
||||
});
|
||||
torService.setOnFailed(workerStateEvent -> {
|
||||
Throwable exception = workerStateEvent.getSource().getException();
|
||||
if(exception instanceof TorServerAlreadyBoundException) {
|
||||
String proxyServer = Config.get().getProxyServer();
|
||||
if(proxyServer == null || proxyServer.equals("")) {
|
||||
proxyServer = "localhost:9050";
|
||||
Config.get().setProxyServer(proxyServer);
|
||||
}
|
||||
|
||||
if(proxyServer.equals("localhost:9050") || proxyServer.equals("127.0.0.1:9050")) {
|
||||
Config.get().setUseProxy(true);
|
||||
torService.cancel();
|
||||
restartServices();
|
||||
EventManager.get().post(new TorExternalStatusEvent());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
EventManager.get().post(new TorFailedStatusEvent(workerStateEvent.getSource().getException()));
|
||||
});
|
||||
|
||||
@@ -333,7 +385,7 @@ public class AppServices {
|
||||
stage.setMinWidth(650);
|
||||
stage.setMinHeight(730);
|
||||
stage.setScene(scene);
|
||||
stage.getIcons().add(new Image(MainApp.class.getResourceAsStream("/image/sparrow.png")));
|
||||
stage.getIcons().add(new Image(MainApp.class.getResourceAsStream("/image/sparrow-large.png")));
|
||||
|
||||
appController.initializeView();
|
||||
stage.show();
|
||||
@@ -352,6 +404,15 @@ public class AppServices {
|
||||
return application;
|
||||
}
|
||||
|
||||
public void minimizeStage(Stage stage) {
|
||||
if(trayManager == null) {
|
||||
trayManager = new TrayManager();
|
||||
}
|
||||
|
||||
trayManager.addStage(stage);
|
||||
stage.hide();
|
||||
}
|
||||
|
||||
public Map<Wallet, Storage> getOpenWallets() {
|
||||
Map<Wallet, Storage> openWallets = new LinkedHashMap<>();
|
||||
for(List<WalletTabData> walletTabDataList : walletWindows.values()) {
|
||||
@@ -452,7 +513,7 @@ public class AppServices {
|
||||
|
||||
public static void setStageIcon(Window window) {
|
||||
Stage stage = (Stage)window;
|
||||
stage.getIcons().add(new Image(AppServices.class.getResourceAsStream("/image/sparrow.png")));
|
||||
stage.getIcons().add(new Image(AppServices.class.getResourceAsStream("/image/sparrow-large.png")));
|
||||
|
||||
if(stage.getScene() != null && Config.get().getTheme() == Theme.DARK) {
|
||||
stage.getScene().getStylesheets().add(AppServices.class.getResource("darktheme.css").toExternalForm());
|
||||
@@ -520,7 +581,7 @@ public class AppServices {
|
||||
public void versionCheckStatus(VersionCheckStatusEvent event) {
|
||||
versionCheckService.cancel();
|
||||
|
||||
if(Config.get().getMode() != Mode.OFFLINE && event.isEnabled()) {
|
||||
if(Config.get().getMode() != Mode.OFFLINE && event.isEnabled() && Network.get() == Network.MAINNET) {
|
||||
versionCheckService = createVersionCheckService();
|
||||
versionCheckService.start();
|
||||
}
|
||||
@@ -539,7 +600,7 @@ public class AppServices {
|
||||
Platform.runLater(() -> {
|
||||
if(!Window.getWindows().isEmpty()) {
|
||||
List<File> walletFiles = allWallets.stream().map(walletTabData -> walletTabData.getStorage().getWalletFile()).collect(Collectors.toList());
|
||||
Config.get().setRecentWalletFiles(walletFiles);
|
||||
Config.get().setRecentWalletFiles(Config.get().isLoadRecentWallets() ? walletFiles : Collections.emptyList());
|
||||
}
|
||||
});
|
||||
|
||||
@@ -584,7 +645,7 @@ public class AppServices {
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletSettingsChanged(WalletSettingsChangedEvent event) {
|
||||
public void walletAddressesChanged(WalletAddressesChangedEvent event) {
|
||||
restartBwt(event.getWallet());
|
||||
}
|
||||
|
||||
|
||||
@@ -19,8 +19,10 @@ import javafx.application.Application;
|
||||
import javafx.scene.text.Font;
|
||||
import javafx.stage.Stage;
|
||||
import org.controlsfx.glyphfont.GlyphFontRegistry;
|
||||
import org.controlsfx.tools.Platform;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.slf4j.bridge.SLF4JBridgeHandler;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.*;
|
||||
@@ -28,7 +30,7 @@ import java.util.stream.Collectors;
|
||||
|
||||
public class MainApp extends Application {
|
||||
public static final String APP_NAME = "Sparrow";
|
||||
public static final String APP_VERSION = "1.3.0";
|
||||
public static final String APP_VERSION = "1.3.2";
|
||||
public static final String APP_HOME_PROPERTY = "sparrow.home";
|
||||
public static final String NETWORK_ENV_PROPERTY = "SPARROW_NETWORK";
|
||||
|
||||
@@ -78,6 +80,10 @@ public class MainApp extends Application {
|
||||
Config.get().setCoreWallet("");
|
||||
}
|
||||
|
||||
if(Config.get().getHdCapture() == null && Platform.getCurrent() == Platform.OSX) {
|
||||
Config.get().setHdCapture(Boolean.TRUE);
|
||||
}
|
||||
|
||||
System.setProperty(Wallet.ALLOW_DERIVATIONS_MATCHING_OTHER_SCRIPT_TYPES_PROPERTY, Boolean.toString(!Config.get().isValidateDerivationPaths()));
|
||||
|
||||
AppController appController = AppServices.newAppWindow(stage);
|
||||
@@ -156,6 +162,8 @@ public class MainApp extends Application {
|
||||
getLogger().info("Using " + Network.get() + " configuration");
|
||||
}
|
||||
|
||||
SLF4JBridgeHandler.removeHandlersForRootLogger();
|
||||
SLF4JBridgeHandler.install();
|
||||
com.sun.javafx.application.LauncherImpl.launchApplication(MainApp.class, MainAppPreloader.class, argv);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.WalletDataChangedEvent;
|
||||
import com.sparrowwallet.sparrow.event.WalletHistoryStatusEvent;
|
||||
import com.sparrowwallet.sparrow.event.WalletSettingsChangedEvent;
|
||||
import com.sparrowwallet.sparrow.event.WalletAddressesChangedEvent;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
import com.sparrowwallet.sparrow.net.ServerType;
|
||||
@@ -92,7 +92,7 @@ public class CoinTreeTable extends TreeTableView<Entry> {
|
||||
EventManager.get().post(new WalletDataChangedEvent(wallet));
|
||||
//Trigger full wallet rescan
|
||||
wallet.clearHistory();
|
||||
EventManager.get().post(new WalletSettingsChangedEvent(wallet, pastWallet, storage.getWalletFile()));
|
||||
EventManager.get().post(new WalletAddressesChangedEvent(wallet, pastWallet, storage.getWalletFile()));
|
||||
}
|
||||
});
|
||||
if(wallet.getBirthDate() == null) {
|
||||
|
||||
@@ -4,16 +4,15 @@ import com.sparrowwallet.drongo.ExtendedKey;
|
||||
import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.OutputDescriptor;
|
||||
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
||||
import com.sparrowwallet.drongo.policy.Policy;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.KeystoreSource;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.AddressDisplayedEvent;
|
||||
import com.sparrowwallet.sparrow.event.KeystoreImportEvent;
|
||||
import com.sparrowwallet.sparrow.event.MessageSignedEvent;
|
||||
import com.sparrowwallet.sparrow.event.PSBTSignedEvent;
|
||||
import com.sparrowwallet.sparrow.event.*;
|
||||
import com.sparrowwallet.sparrow.io.Device;
|
||||
import com.sparrowwallet.sparrow.io.Hwi;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
@@ -34,6 +33,7 @@ import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -197,18 +197,31 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
importButton.setText("Import Keystore");
|
||||
importButton.setOnAction(event -> {
|
||||
importButton.setDisable(true);
|
||||
importKeystore(wallet.getScriptType().getDefaultDerivation());
|
||||
importKeystore(wallet.getScriptType() == null ? ScriptType.P2WPKH.getDefaultDerivation() : wallet.getScriptType().getDefaultDerivation());
|
||||
});
|
||||
String[] accounts = new String[] {"Default Account #0", "Account #1", "Account #2", "Account #3", "Account #4", "Account #5", "Account #6", "Account #7", "Account #8", "Account #9"};
|
||||
int scriptAccountsLength = ScriptType.P2SH.equals(wallet.getScriptType()) ? 1 : accounts.length;
|
||||
for(int i = 0; i < scriptAccountsLength; i++) {
|
||||
MenuItem item = new MenuItem(accounts[i]);
|
||||
final List<ChildNumber> derivation = wallet.getScriptType().getDefaultDerivation(i);
|
||||
item.setOnAction(event -> {
|
||||
importButton.setDisable(true);
|
||||
importKeystore(derivation);
|
||||
});
|
||||
importButton.getItems().add(item);
|
||||
if(wallet.getScriptType() == null) {
|
||||
ScriptType[] scriptTypes = new ScriptType[] {ScriptType.P2WPKH, ScriptType.P2SH_P2WPKH, ScriptType.P2PKH};
|
||||
for(ScriptType scriptType : scriptTypes) {
|
||||
MenuItem item = new MenuItem(scriptType.getDescription());
|
||||
final List<ChildNumber> derivation = scriptType.getDefaultDerivation();
|
||||
item.setOnAction(event -> {
|
||||
importButton.setDisable(true);
|
||||
importKeystore(derivation);
|
||||
});
|
||||
importButton.getItems().add(item);
|
||||
}
|
||||
} else {
|
||||
String[] accounts = new String[] {"Default Account #0", "Account #1", "Account #2", "Account #3", "Account #4", "Account #5", "Account #6", "Account #7", "Account #8", "Account #9"};
|
||||
int scriptAccountsLength = ScriptType.P2SH.equals(wallet.getScriptType()) ? 1 : accounts.length;
|
||||
for(int i = 0; i < scriptAccountsLength; i++) {
|
||||
MenuItem item = new MenuItem(accounts[i]);
|
||||
final List<ChildNumber> derivation = wallet.getScriptType().getDefaultDerivation(i);
|
||||
item.setOnAction(event -> {
|
||||
importButton.setDisable(true);
|
||||
importKeystore(derivation);
|
||||
});
|
||||
importButton.getItems().add(item);
|
||||
}
|
||||
}
|
||||
importButton.managedProperty().bind(importButton.visibleProperty());
|
||||
importButton.setVisible(false);
|
||||
@@ -312,12 +325,22 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
passphrase.bind(passphraseField.textProperty());
|
||||
HBox.setHgrow(passphraseField, Priority.ALWAYS);
|
||||
|
||||
Button sendPassphraseButton = new Button("Send Passphrase");
|
||||
SplitMenuButton sendPassphraseButton = new SplitMenuButton();
|
||||
sendPassphraseButton.setText("Send Passphrase");
|
||||
sendPassphraseButton.setOnAction(event -> {
|
||||
setExpanded(false);
|
||||
setDescription("Confirm passphrase on device...");
|
||||
sendPassphrase(passphrase.get());
|
||||
});
|
||||
|
||||
MenuItem removePassphrase = new MenuItem("Toggle Passphrase Off");
|
||||
removePassphrase.setOnAction(event -> {
|
||||
setExpanded(false);
|
||||
setDescription("Toggling passphrase off, check device...");
|
||||
togglePassphraseOff();
|
||||
});
|
||||
sendPassphraseButton.getItems().add(removePassphrase);
|
||||
|
||||
HBox contentBox = new HBox();
|
||||
contentBox.setAlignment(Pos.TOP_RIGHT);
|
||||
contentBox.setSpacing(20);
|
||||
@@ -330,6 +353,35 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
return contentBox;
|
||||
}
|
||||
|
||||
private Node getTogglePassphraseOn() {
|
||||
CopyableLabel label = new CopyableLabel("Passphrase is currently disabled");
|
||||
HBox.setHgrow(label, Priority.ALWAYS);
|
||||
|
||||
Button togglePassphraseOn = new Button("Toggle Passphrase On");
|
||||
togglePassphraseOn.setOnAction(event -> {
|
||||
setExpanded(false);
|
||||
hideButtons(importButton, signButton, displayAddressButton, signMessageButton);
|
||||
setDescription("Toggling passphrase on, check device...");
|
||||
togglePassphraseOn();
|
||||
});
|
||||
|
||||
HBox contentBox = new HBox();
|
||||
contentBox.setSpacing(20);
|
||||
contentBox.setAlignment(Pos.CENTER_LEFT);
|
||||
contentBox.getChildren().addAll(label, togglePassphraseOn);
|
||||
contentBox.setPadding(new Insets(10, 30, 10, 30));
|
||||
|
||||
return contentBox;
|
||||
}
|
||||
|
||||
private void hideButtons(Node... buttons) {
|
||||
for(Node button : buttons) {
|
||||
if(button != null) {
|
||||
button.setVisible(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void promptPin() {
|
||||
Hwi.PromptPinService promptPinService = new Hwi.PromptPinService(device);
|
||||
promptPinService.setOnSucceeded(workerStateEvent -> {
|
||||
@@ -343,7 +395,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
}
|
||||
});
|
||||
promptPinService.setOnFailed(workerStateEvent -> {
|
||||
setError(promptPinService.getException().getMessage(), null);
|
||||
setError("Error", promptPinService.getException().getMessage());
|
||||
unlockButton.setDisable(false);
|
||||
});
|
||||
promptPinService.start();
|
||||
@@ -366,6 +418,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
setExpanded(true);
|
||||
} else {
|
||||
showOperationButton();
|
||||
setContent(getTogglePassphraseOn());
|
||||
}
|
||||
} else {
|
||||
setError("Incorrect PIN", null);
|
||||
@@ -405,13 +458,43 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
}
|
||||
});
|
||||
enumerateService.setOnFailed(workerStateEvent -> {
|
||||
setError(enumerateService.getException().getMessage(), null);
|
||||
setError("Error", enumerateService.getException().getMessage());
|
||||
setPassphraseButton.setDisable(false);
|
||||
setPassphraseButton.setVisible(true);
|
||||
});
|
||||
enumerateService.start();
|
||||
}
|
||||
|
||||
private void togglePassphraseOff() {
|
||||
Hwi.TogglePassphraseService togglePassphraseService = new Hwi.TogglePassphraseService(device);
|
||||
togglePassphraseService.setOnSucceeded(workerStateEvent -> {
|
||||
device.setNeedsPassphraseSent(false);
|
||||
setPassphraseButton.setVisible(false);
|
||||
setDescription("Unlocked");
|
||||
showOperationButton();
|
||||
});
|
||||
togglePassphraseService.setOnFailed(workerStateEvent -> {
|
||||
setError("Error", togglePassphraseService.getException().getMessage());
|
||||
});
|
||||
togglePassphraseService.start();
|
||||
}
|
||||
|
||||
private void togglePassphraseOn() {
|
||||
Hwi.TogglePassphraseService togglePassphraseService = new Hwi.TogglePassphraseService(device);
|
||||
togglePassphraseService.setOnSucceeded(workerStateEvent -> {
|
||||
device.setNeedsPassphraseSent(true);
|
||||
setPassphraseButton.setVisible(true);
|
||||
setPassphraseButton.setDisable(true);
|
||||
setDescription("Enter passphrase");
|
||||
setContent(getPassphraseEntry());
|
||||
setExpanded(true);
|
||||
});
|
||||
togglePassphraseService.setOnFailed(workerStateEvent -> {
|
||||
setError("Error", togglePassphraseService.getException().getMessage());
|
||||
});
|
||||
togglePassphraseService.start();
|
||||
}
|
||||
|
||||
private void importKeystore(List<ChildNumber> derivation) {
|
||||
if(device.getFingerprint() == null) {
|
||||
Hwi.EnumerateService enumerateService = new Hwi.EnumerateService(passphrase.get());
|
||||
@@ -426,7 +509,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
importXpub(derivation);
|
||||
});
|
||||
enumerateService.setOnFailed(workerStateEvent -> {
|
||||
setError(enumerateService.getException().getMessage(), null);
|
||||
setError("Error", enumerateService.getException().getMessage());
|
||||
importButton.setDisable(false);
|
||||
});
|
||||
enumerateService.start();
|
||||
@@ -450,7 +533,18 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
keystore.setKeyDerivation(new KeyDerivation(device.getFingerprint(), derivationPath));
|
||||
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(xpub));
|
||||
|
||||
EventManager.get().post(new KeystoreImportEvent(keystore));
|
||||
if(wallet.getScriptType() == null) {
|
||||
ScriptType scriptType = Arrays.stream(ScriptType.SINGLE_HASH_TYPES).filter(type -> type.getDefaultDerivation().get(0).equals(derivation.get(0))).findFirst().orElse(ScriptType.P2PKH);
|
||||
wallet.setName(device.getModel().toDisplayString());
|
||||
wallet.setPolicyType(PolicyType.SINGLE);
|
||||
wallet.setScriptType(scriptType);
|
||||
wallet.getKeystores().add(keystore);
|
||||
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, scriptType, wallet.getKeystores(), null));
|
||||
|
||||
EventManager.get().post(new WalletImportEvent(wallet));
|
||||
} else {
|
||||
EventManager.get().post(new KeystoreImportEvent(keystore));
|
||||
}
|
||||
} catch(Exception e) {
|
||||
setError("Could not retrieve xpub", e.getMessage());
|
||||
}
|
||||
@@ -513,7 +607,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
importButton.setVisible(true);
|
||||
showHideLink.setText("Show derivation...");
|
||||
showHideLink.setVisible(true);
|
||||
setContent(getDerivationEntry(wallet.getScriptType().getDefaultDerivation()));
|
||||
setContent(getDerivationEntry(wallet.getScriptType() == null ? ScriptType.P2WPKH.getDefaultDerivation() : wallet.getScriptType().getDefaultDerivation()));
|
||||
} else if(deviceOperation.equals(DeviceOperation.SIGN)) {
|
||||
signButton.setVisible(true);
|
||||
showHideLink.setVisible(false);
|
||||
@@ -562,6 +656,10 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
return contentBox;
|
||||
}
|
||||
|
||||
public Device getDevice() {
|
||||
return device;
|
||||
}
|
||||
|
||||
public enum DeviceOperation {
|
||||
IMPORT, SIGN, DISPLAY_ADDRESS, SIGN_MESSAGE;
|
||||
}
|
||||
|
||||
@@ -19,14 +19,21 @@ import javafx.scene.input.ClipboardContent;
|
||||
import javafx.scene.layout.HBox;
|
||||
import org.controlsfx.glyphfont.FontAwesome;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
private static final Logger log = LoggerFactory.getLogger(EntryCell.class);
|
||||
|
||||
public static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm");
|
||||
private static final Pattern REPLACED_BY_FEE_SUFFIX = Pattern.compile("(.*)\\(Replaced By Fee( #)?(\\d+)?\\).*");
|
||||
|
||||
public EntryCell() {
|
||||
super();
|
||||
@@ -200,12 +207,32 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
externalOutputs.addAll(consolidationOutputs);
|
||||
List<Payment> payments = externalOutputs.stream().map(txOutput -> {
|
||||
try {
|
||||
return new Payment(txOutput.getScript().getToAddresses()[0], transactionEntry.getLabel(), txOutput.getValue(), false);
|
||||
String label = transactionEntry.getLabel() == null ? "" : transactionEntry.getLabel();
|
||||
Matcher matcher = REPLACED_BY_FEE_SUFFIX.matcher(label);
|
||||
if(matcher.matches()) {
|
||||
String base = matcher.group(1);
|
||||
if(matcher.groupCount() > 2 && matcher.group(3) != null) {
|
||||
int count = Integer.parseInt(matcher.group(3)) + 1;
|
||||
label = base + "(Replaced By Fee #" + count + ")";
|
||||
} else {
|
||||
label = base + "(Replaced By Fee #2)";
|
||||
}
|
||||
} else {
|
||||
label += (label.isEmpty() ? "" : " ") + "(Replaced By Fee)";
|
||||
}
|
||||
|
||||
return new Payment(txOutput.getScript().getToAddresses()[0], label, txOutput.getValue(), false);
|
||||
} catch(Exception e) {
|
||||
log.error("Error creating RBF payment", e);
|
||||
return null;
|
||||
}
|
||||
}).filter(Objects::nonNull).collect(Collectors.toList());
|
||||
|
||||
if(payments.isEmpty()) {
|
||||
AppServices.showErrorDialog("Replace By Fee Error", "Error creating RBF transaction, check log for details");
|
||||
return;
|
||||
}
|
||||
|
||||
EventManager.get().post(new SendActionEvent(transactionEntry.getWallet(), utxos));
|
||||
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), utxos, payments, blockTransaction.getFee(), true)));
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import com.sparrowwallet.sparrow.event.TimedEvent;
|
||||
import com.sparrowwallet.sparrow.event.WalletExportEvent;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.io.CoboVaultMultisig;
|
||||
import com.sparrowwallet.sparrow.io.PassportMultisig;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
import com.sparrowwallet.sparrow.io.WalletExport;
|
||||
import javafx.geometry.Pos;
|
||||
@@ -81,7 +82,10 @@ public class FileWalletExportPane extends TitledDescriptionPane {
|
||||
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle("Export " + exporter.getWalletModel().toDisplayString() + " File");
|
||||
fileChooser.setInitialFileName(wallet.getName() + "-" + exporter.getWalletModel().toDisplayString().toLowerCase() + "." + exporter.getExportFileExtension());
|
||||
String extension = exporter.getExportFileExtension(wallet);
|
||||
fileChooser.setInitialFileName(wallet.getName() + "-" +
|
||||
exporter.getWalletModel().toDisplayString().toLowerCase().replace(" ", "") +
|
||||
(extension == null || extension.isEmpty() ? "" : "." + extension));
|
||||
|
||||
File file = fileChooser.showSaveDialog(window);
|
||||
if(file != null) {
|
||||
@@ -90,10 +94,9 @@ public class FileWalletExportPane extends TitledDescriptionPane {
|
||||
}
|
||||
|
||||
private void exportWallet(File file) {
|
||||
Wallet copy = wallet.copy();
|
||||
|
||||
if(copy.isEncrypted()) {
|
||||
WalletPasswordDialog dlg = new WalletPasswordDialog(WalletPasswordDialog.PasswordRequirement.LOAD);
|
||||
if(wallet.isEncrypted() && exporter.walletExportRequiresDecryption()) {
|
||||
Wallet copy = wallet.copy();
|
||||
WalletPasswordDialog dlg = new WalletPasswordDialog(wallet.getName(), WalletPasswordDialog.PasswordRequirement.LOAD);
|
||||
Optional<SecureString> password = dlg.showAndWait();
|
||||
if(password.isPresent()) {
|
||||
final File walletFile = AppServices.get().getOpenWallets().get(wallet).getWalletFile();
|
||||
@@ -101,7 +104,12 @@ public class FileWalletExportPane extends TitledDescriptionPane {
|
||||
decryptWalletService.setOnSucceeded(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(walletFile, TimedEvent.Action.END, "Done"));
|
||||
Wallet decryptedWallet = decryptWalletService.getValue();
|
||||
exportWallet(file, decryptedWallet);
|
||||
|
||||
try {
|
||||
exportWallet(file, decryptedWallet);
|
||||
} finally {
|
||||
decryptedWallet.clearPrivate();
|
||||
}
|
||||
});
|
||||
decryptWalletService.setOnFailed(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(walletFile, TimedEvent.Action.END, "Failed"));
|
||||
@@ -111,22 +119,24 @@ public class FileWalletExportPane extends TitledDescriptionPane {
|
||||
decryptWalletService.start();
|
||||
}
|
||||
} else {
|
||||
exportWallet(file, copy);
|
||||
exportWallet(file, wallet);
|
||||
}
|
||||
}
|
||||
|
||||
private void exportWallet(File file, Wallet decryptedWallet) {
|
||||
private void exportWallet(File file, Wallet exportWallet) {
|
||||
try {
|
||||
if(file != null) {
|
||||
OutputStream outputStream = new FileOutputStream(file);
|
||||
exporter.exportWallet(decryptedWallet, outputStream);
|
||||
EventManager.get().post(new WalletExportEvent(decryptedWallet));
|
||||
exporter.exportWallet(exportWallet, outputStream);
|
||||
EventManager.get().post(new WalletExportEvent(exportWallet));
|
||||
} else {
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
exporter.exportWallet(decryptedWallet, outputStream);
|
||||
exporter.exportWallet(exportWallet, outputStream);
|
||||
QRDisplayDialog qrDisplayDialog;
|
||||
if(exporter instanceof CoboVaultMultisig) {
|
||||
qrDisplayDialog = new QRDisplayDialog(RegistryType.BYTES.toString(), outputStream.toByteArray(), true);
|
||||
} else if(exporter instanceof PassportMultisig) {
|
||||
qrDisplayDialog = new QRDisplayDialog(RegistryType.BYTES.toString(), outputStream.toByteArray(), false);
|
||||
} else {
|
||||
qrDisplayDialog = new QRDisplayDialog(outputStream.toString(StandardCharsets.UTF_8));
|
||||
}
|
||||
@@ -138,8 +148,6 @@ public class FileWalletExportPane extends TitledDescriptionPane {
|
||||
errorMessage = e.getCause().getMessage();
|
||||
}
|
||||
setError("Export Error", errorMessage);
|
||||
} finally {
|
||||
decryptedWallet.clearPrivate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,7 +308,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
return;
|
||||
}
|
||||
|
||||
WalletPasswordDialog dlg = new WalletPasswordDialog(WalletPasswordDialog.PasswordRequirement.LOAD);
|
||||
WalletPasswordDialog dlg = new WalletPasswordDialog(wallet.getName(), WalletPasswordDialog.PasswordRequirement.LOAD);
|
||||
Optional<SecureString> password = dlg.showAndWait();
|
||||
if(password.isPresent()) {
|
||||
Storage.DecryptWalletService decryptWalletService = new Storage.DecryptWalletService(wallet.copy(), password.get());
|
||||
|
||||
@@ -30,6 +30,7 @@ import org.controlsfx.validation.ValidationSupport;
|
||||
import org.controlsfx.validation.Validator;
|
||||
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
|
||||
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
@@ -275,7 +276,14 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
|
||||
int mnemonicSeedLength = wordEntriesProperty.get().size() * 11;
|
||||
int entropyLength = mnemonicSeedLength - (mnemonicSeedLength/33);
|
||||
|
||||
DeterministicSeed deterministicSeed = new DeterministicSeed(new SecureRandom(), entropyLength, "");
|
||||
SecureRandom secureRandom;
|
||||
try {
|
||||
secureRandom = SecureRandom.getInstanceStrong();
|
||||
} catch(NoSuchAlgorithmException e) {
|
||||
secureRandom = new SecureRandom();
|
||||
}
|
||||
|
||||
DeterministicSeed deterministicSeed = new DeterministicSeed(secureRandom, entropyLength, "");
|
||||
generatedMnemonicCode = deterministicSeed.getMnemonicCode();
|
||||
|
||||
displayMnemonicCode();
|
||||
@@ -392,6 +400,19 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
|
||||
label.setAlignment(Pos.CENTER_RIGHT);
|
||||
wordField = new TextField();
|
||||
wordField.setMaxWidth(100);
|
||||
TextFormatter<?> formatter = new TextFormatter<>((TextFormatter.Change change) -> {
|
||||
String text = change.getText();
|
||||
// if text was added, fix the text to fit the requirements
|
||||
if(!text.isEmpty()) {
|
||||
String newText = text.replace(" ", "").toLowerCase();
|
||||
int carretPos = change.getCaretPosition() - text.length() + newText.length();
|
||||
change.setText(newText);
|
||||
// fix caret position based on difference in originally added text and fixed text
|
||||
change.selectRange(carretPos, carretPos);
|
||||
}
|
||||
return change;
|
||||
});
|
||||
wordField.setTextFormatter(formatter);
|
||||
|
||||
wordList = Bip39MnemonicCode.INSTANCE.getWordList();
|
||||
AutoCompletionBinding<String> autoCompletionBinding = TextFields.bindAutoCompletion(wordField, new WordlistSuggestionProvider(wordList));
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.sparrowwallet.sparrow.control;
|
||||
import com.github.sarxos.webcam.WebcamEvent;
|
||||
import com.github.sarxos.webcam.WebcamListener;
|
||||
import com.github.sarxos.webcam.WebcamResolution;
|
||||
import com.github.sarxos.webcam.WebcamUpdater;
|
||||
import com.sparrowwallet.drongo.ExtendedKey;
|
||||
import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.OutputDescriptor;
|
||||
@@ -16,6 +17,7 @@ import com.sparrowwallet.drongo.protocol.Base43;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
import com.sparrowwallet.drongo.psbt.PSBTParseException;
|
||||
import com.sparrowwallet.drongo.uri.BitcoinURI;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
@@ -28,13 +30,17 @@ import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.DoubleProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleDoubleProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.util.Duration;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.controlsfx.tools.Borders;
|
||||
import org.slf4j.Logger;
|
||||
@@ -59,13 +65,15 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
private final WebcamService webcamService;
|
||||
private List<String> parts;
|
||||
|
||||
private boolean isUr;
|
||||
private QRScanDialog.Result result;
|
||||
|
||||
private static final Pattern PART_PATTERN = Pattern.compile("p(\\d+)of(\\d+) (.+)");
|
||||
|
||||
private static final int SCAN_PERIOD_MILLIS = 100;
|
||||
private final ObjectProperty<WebcamResolution> webcamResolutionProperty = new SimpleObjectProperty<>(WebcamResolution.VGA);
|
||||
|
||||
private final DoubleProperty percentComplete = new SimpleDoubleProperty(0.0);
|
||||
|
||||
public QRScanDialog() {
|
||||
this.decoder = new URDecoder();
|
||||
this.legacyDecoder = new LegacyURDecoder();
|
||||
@@ -74,7 +82,9 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
webcamResolutionProperty.set(WebcamResolution.HD);
|
||||
}
|
||||
|
||||
this.webcamService = new WebcamService(webcamResolutionProperty.get(), new QRScanListener());
|
||||
this.webcamService = new WebcamService(webcamResolutionProperty.get(), new QRScanListener(), new ScanDelayCalculator());
|
||||
webcamService.setPeriod(Duration.millis(SCAN_PERIOD_MILLIS));
|
||||
webcamService.setRestartOnFailure(false);
|
||||
WebcamView webcamView = new WebcamView(webcamService);
|
||||
|
||||
final DialogPane dialogPane = new QRScanDialogPane();
|
||||
@@ -83,8 +93,23 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
|
||||
StackPane stackPane = new StackPane();
|
||||
stackPane.getChildren().add(webcamView.getView());
|
||||
Node wrappedView = Borders.wrap(stackPane).lineBorder().buildAll();
|
||||
|
||||
dialogPane.setContent(Borders.wrap(stackPane).lineBorder().buildAll());
|
||||
ProgressBar progressBar = new ProgressBar();
|
||||
progressBar.setMinHeight(20);
|
||||
progressBar.setPadding(new Insets(0, 10, 0, 10));
|
||||
progressBar.setPrefWidth(Integer.MAX_VALUE);
|
||||
progressBar.progressProperty().bind(percentComplete);
|
||||
webcamService.openingProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if(percentComplete.get() <= 0.0) {
|
||||
Platform.runLater(() -> percentComplete.set(newValue ? 0.0 : -1.0));
|
||||
}
|
||||
});
|
||||
|
||||
VBox vBox = new VBox(20);
|
||||
vBox.getChildren().addAll(wrappedView, progressBar);
|
||||
|
||||
dialogPane.setContent(vBox);
|
||||
|
||||
webcamService.resultProperty().addListener(new QRResultListener());
|
||||
webcamService.setOnFailed(failedEvent -> {
|
||||
@@ -111,7 +136,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
final ButtonType hdButtonType = new javafx.scene.control.ButtonType("Use HD Capture", ButtonBar.ButtonData.LEFT);
|
||||
dialogPane.getButtonTypes().addAll(hdButtonType, cancelButtonType);
|
||||
dialogPane.setPrefWidth(646);
|
||||
dialogPane.setPrefHeight(webcamResolutionProperty.get() == WebcamResolution.HD ? 450 : 550);
|
||||
dialogPane.setPrefHeight(webcamResolutionProperty.get() == WebcamResolution.HD ? 490 : 590);
|
||||
|
||||
setResultConverter(dialogButton -> dialogButton != cancelButtonType ? result : null);
|
||||
}
|
||||
@@ -127,11 +152,10 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
String qrtext = qrResult.getText();
|
||||
Matcher partMatcher = PART_PATTERN.matcher(qrtext);
|
||||
|
||||
if(isUr || qrtext.toLowerCase().startsWith(UR.UR_PREFIX)) {
|
||||
isUr = true;
|
||||
|
||||
if(qrtext.toLowerCase().startsWith(UR.UR_PREFIX)) {
|
||||
if(LegacyURDecoder.isLegacyURFragment(qrtext)) {
|
||||
legacyDecoder.receivePart(qrtext.toLowerCase());
|
||||
legacyDecoder.receivePart(qrtext);
|
||||
Platform.runLater(() -> percentComplete.setValue(legacyDecoder.getPercentComplete()));
|
||||
|
||||
if(legacyDecoder.isComplete()) {
|
||||
try {
|
||||
@@ -143,6 +167,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
}
|
||||
} else {
|
||||
decoder.receivePart(qrtext);
|
||||
Platform.runLater(() -> percentComplete.setValue(decoder.getEstimatedPercentComplete()));
|
||||
|
||||
if(decoder.getResult() != null) {
|
||||
URDecoder.Result urResult = decoder.getResult();
|
||||
@@ -164,12 +189,20 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
}
|
||||
parts.set(m - 1, payload);
|
||||
|
||||
if(n > 0) {
|
||||
Platform.runLater(() -> percentComplete.setValue((double)parts.stream().filter(Objects::nonNull).count() / n));
|
||||
}
|
||||
|
||||
if(parts.stream().filter(Objects::nonNull).count() == n) {
|
||||
String complete = String.join("", parts);
|
||||
try {
|
||||
PSBT psbt = PSBT.fromString(complete);
|
||||
result = new Result(psbt);
|
||||
return;
|
||||
} catch(PSBTParseException e) {
|
||||
if(PSBT.isPSBT(complete)) {
|
||||
log.error("Error parsing PSBT", e);
|
||||
}
|
||||
} catch(Exception e) {
|
||||
//ignore, bytes not parsable as PSBT
|
||||
}
|
||||
@@ -218,6 +251,10 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
psbt = PSBT.fromString(qrtext);
|
||||
result = new Result(psbt);
|
||||
return;
|
||||
} catch(PSBTParseException e) {
|
||||
if(PSBT.isPSBT(qrtext)) {
|
||||
log.error("Error parsing PSBT", e);
|
||||
}
|
||||
} catch(Exception e) {
|
||||
//Ignore, not parseable as Base64 or hex
|
||||
}
|
||||
@@ -276,6 +313,10 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
try {
|
||||
PSBT psbt = new PSBT(urBytes);
|
||||
return new Result(psbt);
|
||||
} catch(PSBTParseException e) {
|
||||
if(PSBT.isPSBT(urBytes)) {
|
||||
log.error("Error parsing PSBT", e);
|
||||
}
|
||||
} catch(Exception e) {
|
||||
//ignore, bytes not parsable as PSBT
|
||||
}
|
||||
@@ -484,6 +525,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
private class QRScanDialogPane extends DialogPane {
|
||||
@Override
|
||||
protected Node createButton(ButtonType buttonType) {
|
||||
Node button = null;
|
||||
if(buttonType.getButtonData() == ButtonBar.ButtonData.LEFT) {
|
||||
ToggleButton hd = new ToggleButton(buttonType.getText());
|
||||
hd.setSelected(webcamResolutionProperty.get() == WebcamResolution.HD);
|
||||
@@ -497,10 +539,13 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
setHdGraphic(hd, newValue);
|
||||
});
|
||||
|
||||
return hd;
|
||||
button = hd;
|
||||
} else {
|
||||
button = super.createButton(buttonType);
|
||||
}
|
||||
|
||||
return super.createButton(buttonType);
|
||||
button.disableProperty().bind(webcamService.openingProperty());
|
||||
return button;
|
||||
}
|
||||
|
||||
private void setHdGraphic(ToggleButton hd, boolean isHd) {
|
||||
@@ -663,4 +708,10 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
|
||||
public static class ScanDelayCalculator implements WebcamUpdater.DelayCalculator {
|
||||
public long calculateDelay(long snapshotDuration, double deviceFps) {
|
||||
return Math.max(SCAN_PERIOD_MILLIS - snapshotDuration, 0L);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ 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;
|
||||
|
||||
@@ -18,6 +20,9 @@ public class TextAreaDialog extends Dialog<String> {
|
||||
public TextAreaDialog(@NamedArg("defaultValue") String defaultValue) {
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
|
||||
Image image = new Image("/image/sparrow-small.png");
|
||||
dialogPane.setGraphic(new ImageView(image));
|
||||
|
||||
HBox hbox = new HBox();
|
||||
this.textArea = new TextArea(defaultValue);
|
||||
this.textArea.setMaxWidth(Double.MAX_VALUE);
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.stage.Stage;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.image.BaseMultiResolutionImage;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class TrayManager {
|
||||
private static final Logger log = LoggerFactory.getLogger(TrayManager.class);
|
||||
|
||||
private final SystemTray tray;
|
||||
private final TrayIcon trayIcon;
|
||||
private final PopupMenu popupMenu = new PopupMenu();
|
||||
|
||||
public TrayManager() {
|
||||
if(!SystemTray.isSupported()) {
|
||||
throw new UnsupportedOperationException("SystemTray icons are not supported by the current desktop environment.");
|
||||
}
|
||||
|
||||
tray = SystemTray.getSystemTray();
|
||||
|
||||
try {
|
||||
List<Image> imgList = new ArrayList<>();
|
||||
if(org.controlsfx.tools.Platform.getCurrent() == org.controlsfx.tools.Platform.WINDOWS) {
|
||||
imgList.add(ImageIO.read(getClass().getResource("/image/sparrow-black-small.png")));
|
||||
imgList.add(ImageIO.read(getClass().getResource("/image/sparrow-black-small@2x.png")));
|
||||
imgList.add(ImageIO.read(getClass().getResource("/image/sparrow-black-small@3x.png")));
|
||||
} else {
|
||||
imgList.add(ImageIO.read(getClass().getResource("/image/sparrow-white-small.png")));
|
||||
imgList.add(ImageIO.read(getClass().getResource("/image/sparrow-white-small@2x.png")));
|
||||
imgList.add(ImageIO.read(getClass().getResource("/image/sparrow-white-small@3x.png")));
|
||||
}
|
||||
|
||||
BaseMultiResolutionImage mrImage = new BaseMultiResolutionImage(imgList.toArray(new Image[0]));
|
||||
|
||||
this.trayIcon = new TrayIcon(mrImage, "Sparrow", popupMenu);
|
||||
|
||||
MenuItem miExit = new MenuItem("Quit Sparrow");
|
||||
miExit.addActionListener(e -> {
|
||||
SwingUtilities.invokeLater(() -> { tray.remove(this.trayIcon); });
|
||||
Platform.exit();
|
||||
});
|
||||
this.popupMenu.add(miExit);
|
||||
} catch(IOException e) {
|
||||
log.error("Could not load system tray image", e);
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void addStage(Stage stage) {
|
||||
EventQueue.invokeLater(() -> {
|
||||
MenuItem miStage = new MenuItem(stage.getTitle());
|
||||
miStage.setFont(Font.decode(null).deriveFont(Font.BOLD));
|
||||
miStage.addActionListener(e -> Platform.runLater(() -> {
|
||||
stage.show();
|
||||
EventQueue.invokeLater(() -> {
|
||||
popupMenu.remove(miStage);
|
||||
|
||||
if(popupMenu.getItemCount() == 1) {
|
||||
Platform.setImplicitExit(true);
|
||||
SwingUtilities.invokeLater(() -> tray.remove(trayIcon));
|
||||
}
|
||||
});
|
||||
}));
|
||||
//Make sure it's always at the top
|
||||
this.popupMenu.insert(miStage,popupMenu.getItemCount() - 1);
|
||||
|
||||
if(!isShowing()) {
|
||||
// Keeps the JVM running even if there are no
|
||||
// visible JavaFX Stages, otherwise JVM would
|
||||
// exit and we lose the TrayIcon
|
||||
Platform.setImplicitExit(false);
|
||||
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
try {
|
||||
tray.add(this.trayIcon);
|
||||
} catch(AWTException e) {
|
||||
log.error("Unable to add system tray icon", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public boolean isShowing() {
|
||||
return Arrays.stream(tray.getTrayIcons()).collect(Collectors.toList()).contains(trayIcon);
|
||||
}
|
||||
|
||||
public static boolean isSupported() {
|
||||
return Desktop.isDesktopSupported() && SystemTray.isSupported();
|
||||
}
|
||||
}
|
||||
@@ -41,9 +41,9 @@ public class WalletExportDialog extends Dialog<Wallet> {
|
||||
|
||||
List<WalletExport> exporters;
|
||||
if(wallet.getPolicyType() == PolicyType.SINGLE) {
|
||||
exporters = List.of(new Electrum(), new SpecterDesktop());
|
||||
exporters = List.of(new Electrum(), new SpecterDesktop(), new Sparrow());
|
||||
} else if(wallet.getPolicyType() == PolicyType.MULTI) {
|
||||
exporters = List.of(new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new SpecterDesktop(), new BlueWalletMultisig());
|
||||
exporters = List.of(new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new PassportMultisig(), new SpecterDesktop(), new BlueWalletMultisig(), new Sparrow());
|
||||
} else {
|
||||
throw new UnsupportedOperationException("Cannot export wallet with policy type " + wallet.getPolicyType());
|
||||
}
|
||||
|
||||
@@ -4,16 +4,24 @@ import com.google.common.eventbus.Subscribe;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.UsbDeviceEvent;
|
||||
import com.sparrowwallet.sparrow.event.WalletImportEvent;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5Brands;
|
||||
import com.sparrowwallet.sparrow.io.*;
|
||||
import javafx.application.Platform;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.AnchorPane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class WalletImportDialog extends Dialog<Wallet> {
|
||||
private Wallet wallet;
|
||||
private final Accordion importAccordion;
|
||||
private final Button scanButton;
|
||||
|
||||
public WalletImportDialog() {
|
||||
EventManager.get().register(this);
|
||||
@@ -38,7 +46,7 @@ public class WalletImportDialog extends Dialog<Wallet> {
|
||||
AnchorPane.setLeftAnchor(scrollPane, 0.0);
|
||||
AnchorPane.setRightAnchor(scrollPane, 0.0);
|
||||
|
||||
Accordion importAccordion = new Accordion();
|
||||
importAccordion = new Accordion();
|
||||
List<KeystoreFileImport> keystoreImporters = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig(), new PassportSinglesig());
|
||||
for(KeystoreFileImport importer : keystoreImporters) {
|
||||
FileWalletKeystoreImportPane importPane = new FileWalletKeystoreImportPane(importer);
|
||||
@@ -53,7 +61,16 @@ public class WalletImportDialog extends Dialog<Wallet> {
|
||||
scrollPane.setContent(importAccordion);
|
||||
|
||||
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||
dialogPane.getButtonTypes().addAll(cancelButtonType);
|
||||
final ButtonType scanButtonType = new javafx.scene.control.ButtonType("Scan for Connected Devices", ButtonBar.ButtonData.LEFT);
|
||||
dialogPane.getButtonTypes().addAll(scanButtonType, cancelButtonType);
|
||||
|
||||
scanButton = (Button) dialogPane.lookupButton(scanButtonType);
|
||||
scanButton.setGraphic(new Glyph(FontAwesome5Brands.FONT_NAME, FontAwesome5Brands.Glyph.USB));
|
||||
scanButton.addEventFilter(ActionEvent.ACTION, event -> {
|
||||
scan();
|
||||
event.consume();
|
||||
});
|
||||
|
||||
dialogPane.setPrefWidth(500);
|
||||
dialogPane.setPrefHeight(500);
|
||||
|
||||
@@ -65,4 +82,26 @@ public class WalletImportDialog extends Dialog<Wallet> {
|
||||
wallet = event.getWallet();
|
||||
setResult(wallet);
|
||||
}
|
||||
|
||||
private void scan() {
|
||||
Hwi.EnumerateService enumerateService = new Hwi.EnumerateService(null);
|
||||
enumerateService.setOnSucceeded(workerStateEvent -> {
|
||||
scanButton.setGraphic(new Glyph(FontAwesome5Brands.FONT_NAME, FontAwesome5Brands.Glyph.USB));
|
||||
scanButton.setTooltip(null);
|
||||
List<Device> devices = enumerateService.getValue();
|
||||
importAccordion.getPanes().removeIf(titledPane -> titledPane instanceof DevicePane);
|
||||
for(Device device : devices) {
|
||||
DevicePane devicePane = new DevicePane(new Wallet(), device);
|
||||
importAccordion.getPanes().add(0, devicePane);
|
||||
}
|
||||
Platform.runLater(() -> EventManager.get().post(new UsbDeviceEvent(devices)));
|
||||
});
|
||||
enumerateService.setOnFailed(workerStateEvent -> {
|
||||
Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_CIRCLE);
|
||||
glyph.getStyleClass().add("failure");
|
||||
scanButton.setGraphic(glyph);
|
||||
scanButton.setTooltip(new Tooltip(workerStateEvent.getSource().getException().getMessage()));
|
||||
});
|
||||
enumerateService.start();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,12 +25,8 @@ public class WalletPasswordDialog extends Dialog<SecureString> {
|
||||
private final CheckBox deleteBackups;
|
||||
private boolean addingPassword;
|
||||
|
||||
public WalletPasswordDialog(PasswordRequirement requirement) {
|
||||
this(null, requirement);
|
||||
}
|
||||
|
||||
public WalletPasswordDialog(String walletName, PasswordRequirement requirement) {
|
||||
this(null, requirement, false);
|
||||
this(walletName, requirement, false);
|
||||
}
|
||||
|
||||
public WalletPasswordDialog(String walletName, PasswordRequirement requirement, boolean suggestChangePassword) {
|
||||
@@ -135,10 +131,10 @@ public class WalletPasswordDialog extends Dialog<SecureString> {
|
||||
|
||||
public enum PasswordRequirement {
|
||||
LOAD("Please enter the wallet password:", "Unlock"),
|
||||
UPDATE_NEW("Add a password to the wallet?\nLeave empty for none:", "No Password"),
|
||||
UPDATE_EMPTY("This wallet has no password.\nAdd a password to the wallet?\nLeave empty for none:", "No Password"),
|
||||
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"),
|
||||
UPDATE_SET("Please re-enter the wallet password:", "Verify Password"),
|
||||
UPDATE_CHANGE("Enter the new wallet password.\nLeave empty for none:", "No Password");
|
||||
UPDATE_CHANGE("Enter the new wallet password.\nLeave empty for no password:", "No Password");
|
||||
|
||||
private final String description;
|
||||
private final String okButtonText;
|
||||
|
||||
@@ -0,0 +1,354 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.github.sarxos.webcam.*;
|
||||
import com.github.sarxos.webcam.ds.buildin.natives.Device;
|
||||
import com.github.sarxos.webcam.ds.buildin.natives.DeviceList;
|
||||
import com.github.sarxos.webcam.ds.buildin.natives.OpenIMAJGrabber;
|
||||
import org.bridj.Pointer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.color.ColorSpace;
|
||||
import java.awt.image.*;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Hashtable;
|
||||
import java.util.Iterator;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public class WebcamScanDevice implements WebcamDevice, WebcamDevice.BufferAccess, Runnable, WebcamDevice.FPSSource {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(WebcamScanDevice.class);
|
||||
private static final int DEVICE_BUFFER_SIZE = 5;
|
||||
private static final Dimension[] DIMENSIONS;
|
||||
private static final int[] BAND_OFFSETS;
|
||||
private static final int[] BITS;
|
||||
private static final int[] OFFSET;
|
||||
private static final int DATA_TYPE = 0;
|
||||
private static final ColorSpace COLOR_SPACE;
|
||||
public static final int SCAN_LOOP_WAIT_MILLIS = 100;
|
||||
private int timeout = 5000;
|
||||
private OpenIMAJGrabber grabber = null;
|
||||
private Device device = null;
|
||||
private Dimension size = null;
|
||||
private ComponentSampleModel smodel = null;
|
||||
private ColorModel cmodel = null;
|
||||
private boolean failOnSizeMismatch = false;
|
||||
private final AtomicBoolean disposed = new AtomicBoolean(false);
|
||||
private final AtomicBoolean open = new AtomicBoolean(false);
|
||||
private final AtomicBoolean fresh = new AtomicBoolean(false);
|
||||
private Thread refresher = null;
|
||||
private String name = null;
|
||||
private String id = null;
|
||||
private String fullname = null;
|
||||
private long t1 = -1L;
|
||||
private long t2 = -1L;
|
||||
private volatile double fps = 0.0D;
|
||||
|
||||
protected WebcamScanDevice(Device device) {
|
||||
this.device = device;
|
||||
this.name = device.getNameStr();
|
||||
this.id = device.getIdentifierStr();
|
||||
this.fullname = String.format("%s %s", this.name, this.id);
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return this.fullname;
|
||||
}
|
||||
|
||||
public String getDeviceName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public String getDeviceId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
public Device getDeviceRef() {
|
||||
return this.device;
|
||||
}
|
||||
|
||||
public Dimension[] getResolutions() {
|
||||
return DIMENSIONS;
|
||||
}
|
||||
|
||||
public Dimension getResolution() {
|
||||
if (this.size == null) {
|
||||
this.size = this.getResolutions()[0];
|
||||
}
|
||||
|
||||
return this.size;
|
||||
}
|
||||
|
||||
public void setResolution(Dimension size) {
|
||||
if (size == null) {
|
||||
throw new IllegalArgumentException("Size cannot be null");
|
||||
} else if (this.open.get()) {
|
||||
throw new IllegalStateException("Cannot change resolution when webcam is open, please close it first");
|
||||
} else {
|
||||
this.size = size;
|
||||
}
|
||||
}
|
||||
|
||||
public ByteBuffer getImageBytes() {
|
||||
if (this.disposed.get()) {
|
||||
LOG.debug("Webcam is disposed, image will be null");
|
||||
return null;
|
||||
} else if (!this.open.get()) {
|
||||
LOG.debug("Webcam is closed, image will be null");
|
||||
return null;
|
||||
} else {
|
||||
if (this.fresh.compareAndSet(false, true)) {
|
||||
this.updateFrameBuffer();
|
||||
}
|
||||
|
||||
LOG.trace("Webcam grabber get image pointer");
|
||||
Pointer<Byte> image = this.grabber.getImage();
|
||||
this.fresh.set(false);
|
||||
if (image == null) {
|
||||
LOG.warn("Null array pointer found instead of image");
|
||||
return null;
|
||||
} else {
|
||||
int length = this.size.width * this.size.height * 3;
|
||||
LOG.trace("Webcam device get buffer, read {} bytes", length);
|
||||
return image.getByteBuffer((long)length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void getImageBytes(ByteBuffer target) {
|
||||
if (this.disposed.get()) {
|
||||
LOG.debug("Webcam is disposed, image will be null");
|
||||
} else if (!this.open.get()) {
|
||||
LOG.debug("Webcam is closed, image will be null");
|
||||
} else {
|
||||
int minSize = this.size.width * this.size.height * 3;
|
||||
int curSize = target.remaining();
|
||||
if (minSize > curSize) {
|
||||
throw new IllegalArgumentException(String.format("Not enough remaining space in target buffer (%d necessary vs %d remaining)", minSize, curSize));
|
||||
} else {
|
||||
if (this.fresh.compareAndSet(false, true)) {
|
||||
this.updateFrameBuffer();
|
||||
}
|
||||
|
||||
LOG.trace("Webcam grabber get image pointer");
|
||||
Pointer<Byte> image = this.grabber.getImage();
|
||||
this.fresh.set(false);
|
||||
if (image == null) {
|
||||
LOG.warn("Null array pointer found instead of image");
|
||||
} else {
|
||||
LOG.trace("Webcam device read buffer {} bytes", minSize);
|
||||
image = image.validBytes((long)minSize);
|
||||
image.getBytes(target);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public BufferedImage getImage() {
|
||||
ByteBuffer buffer = this.getImageBytes();
|
||||
if (buffer == null) {
|
||||
LOG.error("Images bytes buffer is null!");
|
||||
return null;
|
||||
} else {
|
||||
byte[] bytes = new byte[this.size.width * this.size.height * 3];
|
||||
byte[][] data = new byte[][]{bytes};
|
||||
buffer.get(bytes);
|
||||
DataBufferByte dbuf = new DataBufferByte(data, bytes.length, OFFSET);
|
||||
WritableRaster raster = Raster.createWritableRaster(this.smodel, dbuf, (Point)null);
|
||||
BufferedImage bi = new BufferedImage(this.cmodel, raster, false, (Hashtable)null);
|
||||
bi.flush();
|
||||
return bi;
|
||||
}
|
||||
}
|
||||
|
||||
public void open() {
|
||||
if (!this.disposed.get()) {
|
||||
LOG.debug("Opening webcam device {}", this.getName());
|
||||
if (this.size == null) {
|
||||
this.size = this.getResolutions()[0];
|
||||
}
|
||||
|
||||
if (this.size == null) {
|
||||
throw new RuntimeException("The resolution size cannot be null");
|
||||
} else {
|
||||
LOG.debug("Webcam device {} starting session, size {}", this.device.getIdentifierStr(), this.size);
|
||||
this.grabber = new OpenIMAJGrabber();
|
||||
DeviceList list = (DeviceList)this.grabber.getVideoDevices().get();
|
||||
Iterator var2 = list.asArrayList().iterator();
|
||||
|
||||
while(var2.hasNext()) {
|
||||
Device d = (Device)var2.next();
|
||||
d.getNameStr();
|
||||
d.getIdentifierStr();
|
||||
}
|
||||
|
||||
boolean started = this.grabber.startSession(this.size.width, this.size.height, 50, Pointer.pointerTo(this.device));
|
||||
if (!started) {
|
||||
throw new WebcamException("Cannot start native grabber!");
|
||||
} else {
|
||||
this.grabber.setTimeout(this.timeout);
|
||||
LOG.debug("Webcam device session started");
|
||||
Dimension size2 = new Dimension(this.grabber.getWidth(), this.grabber.getHeight());
|
||||
int w1 = this.size.width;
|
||||
int w2 = size2.width;
|
||||
int h1 = this.size.height;
|
||||
int h2 = size2.height;
|
||||
if (w1 != w2 || h1 != h2) {
|
||||
if (this.failOnSizeMismatch) {
|
||||
throw new WebcamException(String.format("Different size obtained vs requested - [%dx%d] vs [%dx%d]", w1, h1, w2, h2));
|
||||
}
|
||||
|
||||
Object[] args = new Object[]{w1, h1, w2, h2, w2, h2};
|
||||
LOG.warn("Different size obtained vs requested - [{}x{}] vs [{}x{}]. Setting correct one. New size is [{}x{}]", args);
|
||||
this.size = new Dimension(w2, h2);
|
||||
}
|
||||
|
||||
this.smodel = new ComponentSampleModel(0, this.size.width, this.size.height, 3, this.size.width * 3, BAND_OFFSETS);
|
||||
this.cmodel = new ComponentColorModel(COLOR_SPACE, BITS, false, false, 1, 0);
|
||||
LOG.debug("Clear memory buffer");
|
||||
this.clearMemoryBuffer();
|
||||
LOG.debug("Webcam device {} is now open", this);
|
||||
this.open.set(true);
|
||||
this.refresher = this.startFramesRefresher();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void clearMemoryBuffer() {
|
||||
for(int i = 0; i < 5; ++i) {
|
||||
this.grabber.nextFrame();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private Thread startFramesRefresher() {
|
||||
Thread refresher = new Thread(this, String.format("frames-refresher-[%s]", this.id));
|
||||
refresher.setUncaughtExceptionHandler(WebcamExceptionHandler.getInstance());
|
||||
refresher.setDaemon(true);
|
||||
refresher.start();
|
||||
return refresher;
|
||||
}
|
||||
|
||||
public void close() {
|
||||
if (this.open.compareAndSet(true, false)) {
|
||||
LOG.debug("Closing webcam device");
|
||||
this.grabber.stopSession();
|
||||
}
|
||||
}
|
||||
|
||||
public void dispose() {
|
||||
if (this.disposed.compareAndSet(false, true)) {
|
||||
LOG.debug("Disposing webcam device {}", this.getName());
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
public void setFailOnSizeMismatch(boolean fail) {
|
||||
this.failOnSizeMismatch = fail;
|
||||
}
|
||||
|
||||
public boolean isOpen() {
|
||||
return this.open.get();
|
||||
}
|
||||
|
||||
public int getTimeout() {
|
||||
return this.timeout;
|
||||
}
|
||||
|
||||
public void setTimeout(int timeout) {
|
||||
if (this.isOpen()) {
|
||||
throw new WebcamException("Timeout must be set before webcam is open");
|
||||
} else {
|
||||
this.timeout = timeout;
|
||||
}
|
||||
}
|
||||
|
||||
private void updateFrameBuffer() {
|
||||
LOG.trace("Next frame");
|
||||
if (this.t1 == -1L || this.t2 == -1L) {
|
||||
this.t1 = System.currentTimeMillis();
|
||||
this.t2 = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
int result = (new WebcamScanDevice.NextFrameTask(this)).nextFrame();
|
||||
this.t1 = this.t2;
|
||||
this.t2 = System.currentTimeMillis();
|
||||
this.fps = (4.0D * this.fps + (double)(1000L / (this.t2 - this.t1 + 1L))) / 5.0D;
|
||||
if (result == -1) {
|
||||
LOG.error("Timeout when requesting image!");
|
||||
} else if (result < -1) {
|
||||
LOG.error("Error requesting new frame!");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void run() {
|
||||
do {
|
||||
try {
|
||||
Thread.sleep(SCAN_LOOP_WAIT_MILLIS);
|
||||
} catch(InterruptedException e) {
|
||||
//ignore
|
||||
}
|
||||
|
||||
if (Thread.interrupted()) {
|
||||
LOG.debug("Refresher has been interrupted");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.open.get()) {
|
||||
LOG.debug("Cancelling refresher");
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateFrameBuffer();
|
||||
} while(this.open.get());
|
||||
|
||||
}
|
||||
|
||||
public double getFPS() {
|
||||
return this.fps;
|
||||
}
|
||||
|
||||
static {
|
||||
DIMENSIONS = new Dimension[]{WebcamResolution.QQVGA.getSize(), WebcamResolution.QVGA.getSize(), WebcamResolution.VGA.getSize()};
|
||||
BAND_OFFSETS = new int[]{0, 1, 2};
|
||||
BITS = new int[]{8, 8, 8};
|
||||
OFFSET = new int[]{0};
|
||||
COLOR_SPACE = ColorSpace.getInstance(1000);
|
||||
}
|
||||
|
||||
private class NextFrameTask extends WebcamTask {
|
||||
private final AtomicInteger result = new AtomicInteger(0);
|
||||
|
||||
public NextFrameTask(WebcamDevice device) {
|
||||
super(device);
|
||||
}
|
||||
|
||||
public int nextFrame() {
|
||||
try {
|
||||
this.process();
|
||||
} catch (InterruptedException var2) {
|
||||
WebcamScanDevice.LOG.debug("Image buffer request interrupted", var2);
|
||||
}
|
||||
|
||||
return this.result.get();
|
||||
}
|
||||
|
||||
protected void handle() {
|
||||
WebcamScanDevice device = (WebcamScanDevice)this.getDevice();
|
||||
if (device.isOpen()) {
|
||||
try {
|
||||
Thread.sleep(SCAN_LOOP_WAIT_MILLIS);
|
||||
} catch(InterruptedException e) {
|
||||
//ignore
|
||||
}
|
||||
|
||||
this.result.set(WebcamScanDevice.this.grabber.nextFrame());
|
||||
WebcamScanDevice.this.fresh.set(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.github.sarxos.webcam.WebcamDevice;
|
||||
import com.github.sarxos.webcam.ds.buildin.WebcamDefaultDevice;
|
||||
import com.github.sarxos.webcam.ds.buildin.WebcamDefaultDriver;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class WebcamScanDriver extends WebcamDefaultDriver {
|
||||
@Override
|
||||
public List<WebcamDevice> getDevices() {
|
||||
List<WebcamDevice> devices = super.getDevices();
|
||||
List<WebcamDevice> scanDevices = new ArrayList<>();
|
||||
for(WebcamDevice device : devices) {
|
||||
WebcamDefaultDevice defaultDevice = (WebcamDefaultDevice)device;
|
||||
scanDevices.add(new WebcamScanDevice(defaultDevice.getDeviceRef()));
|
||||
}
|
||||
|
||||
return scanDevices;
|
||||
}
|
||||
}
|
||||
@@ -3,29 +3,46 @@ package com.sparrowwallet.sparrow.control;
|
||||
import com.github.sarxos.webcam.Webcam;
|
||||
import com.github.sarxos.webcam.WebcamListener;
|
||||
import com.github.sarxos.webcam.WebcamResolution;
|
||||
import com.github.sarxos.webcam.WebcamUpdater;
|
||||
import com.google.zxing.*;
|
||||
import com.google.zxing.client.j2se.BufferedImageLuminanceSource;
|
||||
import com.google.zxing.common.HybridBinarizer;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.concurrent.Service;
|
||||
import javafx.concurrent.ScheduledService;
|
||||
import javafx.concurrent.Task;
|
||||
import javafx.embed.swing.SwingFXUtils;
|
||||
import javafx.scene.image.Image;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class WebcamService extends Service<Image> {
|
||||
public class WebcamService extends ScheduledService<Image> {
|
||||
private WebcamResolution resolution;
|
||||
private final WebcamListener listener;
|
||||
private final WebcamUpdater.DelayCalculator delayCalculator;
|
||||
private final BooleanProperty opening = new SimpleBooleanProperty(false);
|
||||
|
||||
private final ObjectProperty<Result> resultProperty = new SimpleObjectProperty<>(null);
|
||||
|
||||
public WebcamService(WebcamResolution resolution, WebcamListener listener) {
|
||||
private static final int QR_SAMPLE_PERIOD_MILLIS = 400;
|
||||
|
||||
private Webcam cam;
|
||||
private long lastQrSampleTime;
|
||||
|
||||
static {
|
||||
Webcam.setDriver(new WebcamScanDriver());
|
||||
}
|
||||
|
||||
public WebcamService(WebcamResolution resolution, WebcamListener listener, WebcamUpdater.DelayCalculator delayCalculator) {
|
||||
this.resolution = resolution;
|
||||
this.listener = listener;
|
||||
this.delayCalculator = delayCalculator;
|
||||
this.lastQrSampleTime = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -33,31 +50,57 @@ public class WebcamService extends Service<Image> {
|
||||
return new Task<Image>() {
|
||||
@Override
|
||||
protected Image call() throws Exception {
|
||||
Webcam cam = Webcam.getWebcams(1, TimeUnit.MINUTES).get(0);
|
||||
try {
|
||||
cam.setCustomViewSizes(resolution.getSize());
|
||||
cam.setViewSize(resolution.getSize());
|
||||
if(!Arrays.asList(cam.getWebcamListeners()).contains(listener)) {
|
||||
cam.addWebcamListener(listener);
|
||||
if(cam == null) {
|
||||
List<Webcam> webcams = Webcam.getWebcams(1, TimeUnit.MINUTES);
|
||||
if(webcams.isEmpty()) {
|
||||
throw new UnsupportedOperationException("No camera available.");
|
||||
}
|
||||
|
||||
cam = webcams.get(0);
|
||||
cam.setCustomViewSizes(resolution.getSize());
|
||||
cam.setViewSize(resolution.getSize());
|
||||
if(!Arrays.asList(cam.getWebcamListeners()).contains(listener)) {
|
||||
cam.addWebcamListener(listener);
|
||||
}
|
||||
|
||||
opening.set(true);
|
||||
cam.open(true, delayCalculator);
|
||||
opening.set(false);
|
||||
}
|
||||
|
||||
cam.open();
|
||||
while(!isCancelled()) {
|
||||
if(cam.isImageNew()) {
|
||||
BufferedImage bimg = cam.getImage();
|
||||
updateValue(SwingFXUtils.toFXImage(bimg, null));
|
||||
readQR(bimg);
|
||||
}
|
||||
BufferedImage bimg = cam.getImage();
|
||||
Image image = SwingFXUtils.toFXImage(bimg, null);
|
||||
updateValue(image);
|
||||
|
||||
if(System.currentTimeMillis() > (lastQrSampleTime + QR_SAMPLE_PERIOD_MILLIS)) {
|
||||
readQR(bimg);
|
||||
lastQrSampleTime = System.currentTimeMillis();
|
||||
}
|
||||
cam.close();
|
||||
return getValue();
|
||||
|
||||
return image;
|
||||
} finally {
|
||||
cam.close();
|
||||
opening.set(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
cam = null;
|
||||
super.reset();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean cancel() {
|
||||
if(cam != null && !cam.close()) {
|
||||
cam.close();
|
||||
}
|
||||
|
||||
return super.cancel();
|
||||
}
|
||||
|
||||
private void readQR(BufferedImage bufferedImage) {
|
||||
LuminanceSource source = new BufferedImageLuminanceSource(bufferedImage);
|
||||
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
|
||||
@@ -89,4 +132,12 @@ public class WebcamService extends Service<Image> {
|
||||
public void setResolution(WebcamResolution resolution) {
|
||||
this.resolution = resolution;
|
||||
}
|
||||
|
||||
public boolean isOpening() {
|
||||
return opening.get();
|
||||
}
|
||||
|
||||
public BooleanProperty openingProperty() {
|
||||
return opening;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.Region;
|
||||
import org.slf4j.Logger;
|
||||
@@ -14,7 +17,9 @@ public class WebcamView {
|
||||
private final WebcamService service;
|
||||
private final Region view;
|
||||
|
||||
private final Label statusPlaceholder ;
|
||||
private final Label statusPlaceholder;
|
||||
|
||||
private final ObjectProperty<Image> imageProperty = new SimpleObjectProperty<>(null);
|
||||
|
||||
public WebcamView(WebcamService service) {
|
||||
this.service = service ;
|
||||
@@ -23,22 +28,32 @@ public class WebcamView {
|
||||
// make the cam behave like a mirror:
|
||||
imageView.setScaleX(-1);
|
||||
|
||||
service.valueProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if(newValue != null) {
|
||||
imageProperty.set(newValue);
|
||||
}
|
||||
});
|
||||
|
||||
this.statusPlaceholder = new Label();
|
||||
this.view = new Region() {
|
||||
{
|
||||
service.stateProperty().addListener((obs, oldState, newState) -> {
|
||||
switch (newState) {
|
||||
case READY:
|
||||
statusPlaceholder.setText("Initializing");
|
||||
getChildren().setAll(statusPlaceholder);
|
||||
if(imageProperty.get() == null) {
|
||||
statusPlaceholder.setText("Initializing");
|
||||
getChildren().setAll(statusPlaceholder);
|
||||
}
|
||||
break ;
|
||||
case SCHEDULED:
|
||||
statusPlaceholder.setText("Waiting");
|
||||
getChildren().setAll(statusPlaceholder);
|
||||
if(imageProperty.get() == null) {
|
||||
statusPlaceholder.setText("Waiting");
|
||||
getChildren().setAll(statusPlaceholder);
|
||||
}
|
||||
break ;
|
||||
case RUNNING:
|
||||
imageView.imageProperty().unbind();
|
||||
imageView.imageProperty().bind(service.valueProperty());
|
||||
imageView.imageProperty().bind(imageProperty);
|
||||
getChildren().setAll(imageView);
|
||||
break ;
|
||||
case CANCELLED:
|
||||
|
||||
@@ -1,23 +1,18 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransaction;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class BlockTransactionFetchedEvent extends PagedEvent {
|
||||
public class BlockTransactionFetchedEvent extends TransactionReferencesFinishedEvent {
|
||||
private final Sha256Hash txId;
|
||||
private final BlockTransaction blockTransaction;
|
||||
private final Map<Sha256Hash, BlockTransaction> inputTransactions;
|
||||
|
||||
public BlockTransactionFetchedEvent(Sha256Hash txId, BlockTransaction blockTransaction, Map<Sha256Hash, BlockTransaction> inputTransactions) {
|
||||
this(txId, blockTransaction, inputTransactions, 0, blockTransaction.getTransaction().getInputs().size());
|
||||
}
|
||||
|
||||
public BlockTransactionFetchedEvent(Sha256Hash txId, BlockTransaction blockTransaction, Map<Sha256Hash, BlockTransaction> inputTransactions, int pageStart, int pageEnd) {
|
||||
super(pageStart, pageEnd);
|
||||
this.txId = txId;
|
||||
this.blockTransaction = blockTransaction;
|
||||
public BlockTransactionFetchedEvent(Transaction transaction, BlockTransaction blockTransaction, Map<Sha256Hash, BlockTransaction> inputTransactions, int pageStart, int pageEnd) {
|
||||
super(transaction, blockTransaction, pageStart, pageEnd);
|
||||
this.txId = transaction.getTxId();
|
||||
this.inputTransactions = inputTransactions;
|
||||
}
|
||||
|
||||
@@ -25,10 +20,6 @@ public class BlockTransactionFetchedEvent extends PagedEvent {
|
||||
return txId;
|
||||
}
|
||||
|
||||
public BlockTransaction getBlockTransaction() {
|
||||
return blockTransaction;
|
||||
}
|
||||
|
||||
public Map<Sha256Hash, BlockTransaction> getInputTransactions() {
|
||||
return inputTransactions;
|
||||
}
|
||||
|
||||
+5
-4
@@ -1,17 +1,18 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransaction;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class BlockTransactionOutputsFetchedEvent extends PagedEvent {
|
||||
public class BlockTransactionOutputsFetchedEvent extends TransactionReferencesEvent {
|
||||
private final Sha256Hash txId;
|
||||
private final List<BlockTransaction> outputTransactions;
|
||||
|
||||
public BlockTransactionOutputsFetchedEvent(Sha256Hash txId, List<BlockTransaction> outputTransactions, int pageStart, int pageEnd) {
|
||||
super(pageStart, pageEnd);
|
||||
this.txId = txId;
|
||||
public BlockTransactionOutputsFetchedEvent(Transaction transaction, List<BlockTransaction> outputTransactions, int pageStart, int pageEnd) {
|
||||
super(transaction, pageStart, pageEnd);
|
||||
this.txId = transaction.getTxId();
|
||||
this.outputTransactions = outputTransactions;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.sparrow.net.TlsServerException;
|
||||
|
||||
public class ConnectionFailedEvent {
|
||||
private final Throwable exception;
|
||||
|
||||
@@ -10,4 +12,26 @@ public class ConnectionFailedEvent {
|
||||
public Throwable getException() {
|
||||
return exception;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
if(exception instanceof TlsServerException) {
|
||||
return exception.getMessage();
|
||||
}
|
||||
|
||||
Throwable cause = (exception.getCause() != null ? exception.getCause() : exception);
|
||||
cause = (cause.getCause() != null ? cause.getCause() : cause);
|
||||
String message = splitCamelCase(cause.getClass().getSimpleName().replace("Exception", "Error"));
|
||||
return message + " (" + cause.getMessage() + ")";
|
||||
}
|
||||
|
||||
static String splitCamelCase(String s) {
|
||||
return s.replaceAll(
|
||||
String.format("%s|%s|%s",
|
||||
"(?<=[A-Z])(?=[A-Z][a-z])",
|
||||
"(?<=[^A-Z])(?=[A-Z])",
|
||||
"(?<=[A-Za-z])(?=[^A-Za-z])"
|
||||
),
|
||||
" "
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.sparrow.TabData;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Tab;
|
||||
|
||||
public class TabEvent {
|
||||
@@ -15,7 +16,7 @@ public class TabEvent {
|
||||
}
|
||||
|
||||
public String getTabName() {
|
||||
return tab.getText();
|
||||
return ((Label)tab.getGraphic()).getText();
|
||||
}
|
||||
|
||||
public TabData getTabData() {
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
public class TorExternalStatusEvent extends TorStatusEvent {
|
||||
public TorExternalStatusEvent() {
|
||||
super("Tor is already running, using external instance...");
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.sparrow.net.TorServerAlreadyBoundException;
|
||||
|
||||
public class TorFailedStatusEvent extends TorStatusEvent {
|
||||
private final Throwable exception;
|
||||
|
||||
public TorFailedStatusEvent(Throwable exception) {
|
||||
super("Tor failed to start: " + (exception.getCause() != null ?
|
||||
(exception.getCause().getMessage().contains("Failed to bind") ? exception.getCause().getMessage() + " Is a Tor proxy already running?" : exception.getCause().getMessage() ) :
|
||||
exception.getMessage()));
|
||||
super("Tor failed to start: " + (exception instanceof TorServerAlreadyBoundException ? exception.getCause().getMessage() + " Is a Tor proxy already running?" :
|
||||
(exception.getCause() != null ? exception.getCause().getMessage() : exception.getMessage())));
|
||||
this.exception = exception;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
|
||||
public class TransactionReferencesEvent extends PagedEvent {
|
||||
private final Transaction transaction;
|
||||
|
||||
public TransactionReferencesEvent(Transaction transaction) {
|
||||
this(transaction, 0, 0);
|
||||
}
|
||||
|
||||
public TransactionReferencesEvent(Transaction transaction, int pageStart, int pageEnd) {
|
||||
super(pageStart, pageEnd);
|
||||
this.transaction = transaction;
|
||||
}
|
||||
|
||||
public Transaction getTransaction() {
|
||||
return transaction;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
|
||||
public class TransactionReferencesFailedEvent extends TransactionReferencesEvent {
|
||||
private final Throwable exception;
|
||||
|
||||
public TransactionReferencesFailedEvent(Transaction transaction, Throwable exception) {
|
||||
super(transaction);
|
||||
this.exception = exception;
|
||||
}
|
||||
|
||||
public TransactionReferencesFailedEvent(Transaction transaction, Throwable exception, int pageStart, int pageEnd) {
|
||||
super(transaction, pageStart, pageEnd);
|
||||
this.exception = exception;
|
||||
}
|
||||
|
||||
public Throwable getException() {
|
||||
return exception;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransaction;
|
||||
|
||||
public class TransactionReferencesFinishedEvent extends TransactionReferencesEvent {
|
||||
private final BlockTransaction blockTransaction;
|
||||
|
||||
public TransactionReferencesFinishedEvent(Transaction transaction, BlockTransaction blockTransaction) {
|
||||
super(transaction);
|
||||
this.blockTransaction = blockTransaction;
|
||||
}
|
||||
|
||||
public TransactionReferencesFinishedEvent(Transaction transaction, BlockTransaction blockTransaction, int pageStart, int pageEnd) {
|
||||
super(transaction, pageStart, pageEnd);
|
||||
this.blockTransaction = blockTransaction;
|
||||
}
|
||||
|
||||
public BlockTransaction getBlockTransaction() {
|
||||
return blockTransaction;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
|
||||
public class TransactionReferencesStartedEvent extends TransactionReferencesEvent {
|
||||
public TransactionReferencesStartedEvent(Transaction transaction) {
|
||||
super(transaction);
|
||||
}
|
||||
|
||||
public TransactionReferencesStartedEvent(Transaction transaction, int pageStart, int pageEnd) {
|
||||
super(transaction, pageStart, pageEnd);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* This event is posted when a wallet's addresses are changed (keystores, policy, script type).
|
||||
* This event marks a fundamental change that is used to update application level UI, clear node entry caches and similar. It should only be subscribed to by application-level classes.
|
||||
* Note that WalletForm does not listen to this event to save the wallet, since the wallet is foreground saved directly in SettingsController before this event is posted.
|
||||
* This is because any failure in saving the wallet must be immediately reported to the user.
|
||||
* Note that all wallet detail controllers that share a WalletForm, and that class posts WalletNodesChangedEvent once it has cleared it's entry caches.
|
||||
*/
|
||||
public class WalletAddressesChangedEvent extends WalletSettingsChangedEvent {
|
||||
public WalletAddressesChangedEvent(Wallet wallet, Wallet pastWallet, File walletFile) {
|
||||
super(wallet, pastWallet, walletFile);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
public class WalletHistoryFailedEvent extends WalletHistoryStatusEvent {
|
||||
private final Throwable exception;
|
||||
|
||||
public WalletHistoryFailedEvent(Wallet wallet, Throwable exception) {
|
||||
super(wallet, exception.getCause() == null ? exception.getMessage() : exception.getCause().getMessage());
|
||||
this.exception = exception;
|
||||
}
|
||||
|
||||
public Throwable getException() {
|
||||
return exception;
|
||||
}
|
||||
}
|
||||
@@ -6,8 +6,4 @@ public class WalletHistoryFinishedEvent extends WalletHistoryStatusEvent {
|
||||
public WalletHistoryFinishedEvent(Wallet wallet) {
|
||||
super(wallet, false);
|
||||
}
|
||||
|
||||
public WalletHistoryFinishedEvent(Wallet wallet, String errorMessage) {
|
||||
super(wallet, errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletNode;
|
||||
|
||||
public class WalletHistoryStartedEvent extends WalletHistoryStatusEvent {
|
||||
public WalletHistoryStartedEvent(Wallet wallet) {
|
||||
private final WalletNode walletNode;
|
||||
|
||||
public WalletHistoryStartedEvent(Wallet wallet, WalletNode walletNode) {
|
||||
super(wallet, true);
|
||||
this.walletNode = walletNode;
|
||||
}
|
||||
|
||||
public WalletNode getWalletNode() {
|
||||
return walletNode;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,11 +5,9 @@ import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* This event is posted when a wallet's settings are changed (keystores, policy, script type).
|
||||
* This event marks a fundamental change that is used to update application level UI, clear node entry caches and similar. It should only be subscribed to by application-level classes.
|
||||
* Note that WalletForm does not listen to this event to save the wallet, since the wallet is foreground saved directly in SettingsController before this event is posted.
|
||||
* This is because any failure in saving the wallet must be immediately reported to the user.
|
||||
* Note that all wallet detail controllers that share a WalletForm, and that class posts WalletNodesChangedEvent once it has cleared it's entry caches.
|
||||
* This event is posted when a wallet's settings are changed in a way that does not update the wallet addresses or history.
|
||||
* For example, changing the keystore source or label will trigger this event, which updates the wallet but avoids a full wallet refresh.
|
||||
* It is impossible for the validity of a wallet to change on this event - listen for WalletAddressesChangedEvent for this
|
||||
*/
|
||||
public class WalletSettingsChangedEvent extends WalletChangedEvent {
|
||||
private final Wallet pastWallet;
|
||||
|
||||
@@ -7,6 +7,8 @@ import com.sparrowwallet.drongo.policy.Policy;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
@@ -14,6 +16,8 @@ import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class CoboVaultSinglesig implements KeystoreFileImport, WalletImport {
|
||||
private static final Logger log = LoggerFactory.getLogger(CoboVaultSinglesig.class);
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Cobo Vault";
|
||||
@@ -53,7 +57,8 @@ public class CoboVaultSinglesig implements KeystoreFileImport, WalletImport {
|
||||
|
||||
return keystore;
|
||||
} catch (Exception e) {
|
||||
throw new ImportException(e);
|
||||
log.error("Error getting Cobo Vault keystore", e);
|
||||
throw new ImportException("Error getting Cobo Vault keystore", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.KeystoreSource;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
@@ -19,6 +21,8 @@ import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public class ColdcardMultisig implements WalletImport, KeystoreFileImport, WalletExport {
|
||||
private static final Logger log = LoggerFactory.getLogger(ColdcardMultisig.class);
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Coldcard Multisig";
|
||||
@@ -81,7 +85,7 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getExportFileExtension() {
|
||||
public String getExportFileExtension(Wallet wallet) {
|
||||
return "txt";
|
||||
}
|
||||
|
||||
@@ -146,7 +150,8 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
|
||||
|
||||
return wallet;
|
||||
} catch(Exception e) {
|
||||
throw new ImportException(e);
|
||||
log.error("Error importing Coldcard multisig wallet", e);
|
||||
throw new ImportException("Error importing Coldcard multisig wallet", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,7 +204,8 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
|
||||
writer.flush();
|
||||
writer.close();
|
||||
} catch(Exception e) {
|
||||
throw new ExportException(e);
|
||||
log.error("Error exporting Coldcard multisig wallet", e);
|
||||
throw new ExportException("Error exporting Coldcard multisig wallet", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,4 +233,9 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
|
||||
public boolean isWalletExportScannable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean walletExportRequiresDecryption() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ import com.sparrowwallet.drongo.policy.Policy;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
@@ -18,6 +20,8 @@ import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
|
||||
public class ColdcardSinglesig implements KeystoreFileImport, WalletImport {
|
||||
private static final Logger log = LoggerFactory.getLogger(ColdcardSinglesig.class);
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Coldcard";
|
||||
@@ -82,7 +86,8 @@ public class ColdcardSinglesig implements KeystoreFileImport, WalletImport {
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new ImportException(e);
|
||||
log.error("Error getting Coldcard keystore", e);
|
||||
throw new ImportException("Error getting Coldcard keystore", e);
|
||||
}
|
||||
|
||||
throw new ImportException("Correct derivation not found for script type: " + scriptType);
|
||||
|
||||
@@ -25,6 +25,7 @@ public class Config {
|
||||
private FeeRatesSelection feeRatesSelection;
|
||||
private Currency fiatCurrency;
|
||||
private ExchangeSource exchangeSource;
|
||||
private boolean loadRecentWallets = true;
|
||||
private boolean validateDerivationPaths = true;
|
||||
private boolean groupByAddress = true;
|
||||
private boolean includeMempoolChange = true;
|
||||
@@ -37,7 +38,7 @@ public class Config {
|
||||
private List<File> recentWalletFiles;
|
||||
private Integer keyDerivationPeriod;
|
||||
private File hwi;
|
||||
private boolean hdCapture;
|
||||
private Boolean hdCapture;
|
||||
private ServerType serverType;
|
||||
private String publicElectrumServer;
|
||||
private String coreServer;
|
||||
@@ -155,6 +156,15 @@ public class Config {
|
||||
flush();
|
||||
}
|
||||
|
||||
public boolean isLoadRecentWallets() {
|
||||
return loadRecentWallets;
|
||||
}
|
||||
|
||||
public void setLoadRecentWallets(boolean loadRecentWallets) {
|
||||
this.loadRecentWallets = loadRecentWallets;
|
||||
flush();
|
||||
}
|
||||
|
||||
public boolean isValidateDerivationPaths() {
|
||||
return validateDerivationPaths;
|
||||
}
|
||||
@@ -263,11 +273,15 @@ public class Config {
|
||||
flush();
|
||||
}
|
||||
|
||||
public boolean isHdCapture() {
|
||||
public Boolean getHdCapture() {
|
||||
return hdCapture;
|
||||
}
|
||||
|
||||
public void setHdCapture(boolean hdCapture) {
|
||||
public Boolean isHdCapture() {
|
||||
return hdCapture != null && hdCapture;
|
||||
}
|
||||
|
||||
public void setHdCapture(Boolean hdCapture) {
|
||||
this.hdCapture = hdCapture;
|
||||
flush();
|
||||
}
|
||||
@@ -289,8 +303,16 @@ public class Config {
|
||||
return getServerType() == ServerType.BITCOIN_CORE ? getCoreServer() : (getServerType() == ServerType.PUBLIC_ELECTRUM_SERVER ? getPublicElectrumServer() : getElectrumServer());
|
||||
}
|
||||
|
||||
public boolean requiresInternalTor() {
|
||||
if(isUseProxy()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return requiresTor();
|
||||
}
|
||||
|
||||
public boolean requiresTor() {
|
||||
if(isUseProxy() || !hasServerAddress()) {
|
||||
if(!hasServerAddress()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.*;
|
||||
import java.lang.reflect.Type;
|
||||
@@ -22,6 +24,8 @@ import java.util.*;
|
||||
import java.util.zip.InflaterInputStream;
|
||||
|
||||
public class Electrum implements KeystoreFileImport, WalletImport, WalletExport {
|
||||
private static final Logger log = LoggerFactory.getLogger(Electrum.class);
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Electrum";
|
||||
@@ -246,7 +250,8 @@ public class Electrum implements KeystoreFileImport, WalletImport, WalletExport
|
||||
|
||||
return wallet;
|
||||
} catch (Exception e) {
|
||||
throw new ImportException(e);
|
||||
log.error("Error importing Electrum Wallet", e);
|
||||
throw new ImportException("Error importing Electrum Wallet", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,7 +278,7 @@ public class Electrum implements KeystoreFileImport, WalletImport, WalletExport
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getExportFileExtension() {
|
||||
public String getExportFileExtension(Wallet wallet) {
|
||||
return "json";
|
||||
}
|
||||
|
||||
@@ -350,7 +355,8 @@ public class Electrum implements KeystoreFileImport, WalletImport, WalletExport
|
||||
outputStream.flush();
|
||||
outputStream.close();
|
||||
} catch (Exception e) {
|
||||
throw new ExportException(e);
|
||||
log.error("Error exporting Electrum Wallet", e);
|
||||
throw new ExportException("Error exporting Electrum Wallet", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -379,6 +385,11 @@ public class Electrum implements KeystoreFileImport, WalletImport, WalletExport
|
||||
return "Export this wallet as an Electrum wallet file.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean walletExportRequiresDecryption() {
|
||||
return true;
|
||||
}
|
||||
|
||||
private static class ElectrumJsonWallet {
|
||||
public Map<String, ElectrumKeystore> keystores = new LinkedHashMap<>();
|
||||
public String wallet_type;
|
||||
|
||||
@@ -30,7 +30,7 @@ import java.util.zip.ZipInputStream;
|
||||
|
||||
public class Hwi {
|
||||
private static final Logger log = LoggerFactory.getLogger(Hwi.class);
|
||||
private static final String VERSION_PREFIX = "hwi-2.0.0-rc.2";
|
||||
private static final String VERSION_PREFIX = "hwi-2.0.1";
|
||||
|
||||
private static boolean isPromptActive = false;
|
||||
|
||||
@@ -51,6 +51,7 @@ public class Hwi {
|
||||
}
|
||||
return Arrays.stream(devices).filter(device -> device != null && device.getModel() != null).collect(Collectors.toList());
|
||||
} catch(IOException e) {
|
||||
log.error("Error executing " + VERSION_PREFIX, e);
|
||||
throw new ImportException(e);
|
||||
} finally {
|
||||
isPromptActive = false;
|
||||
@@ -77,6 +78,16 @@ public class Hwi {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean togglePassphrase(Device device) throws ImportException {
|
||||
try {
|
||||
String output = execute(getDeviceCommand(device, Command.TOGGLE_PASSPHRASE));
|
||||
isPromptActive = false;
|
||||
return wasSuccessful(output);
|
||||
} catch(IOException e) {
|
||||
throw new ImportException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public String getXpub(Device device, String passphrase, String derivationPath) throws ImportException {
|
||||
try {
|
||||
String output;
|
||||
@@ -109,8 +120,7 @@ public class Hwi {
|
||||
throw new IllegalArgumentException("Cannot display address for script type " + scriptType + ": Only single hash types supported");
|
||||
}
|
||||
|
||||
//Remove replace once HWI-2.0.0 is released - see https://github.com/bitcoin-core/HWI/pull/488
|
||||
String descriptor = outputDescriptor.toString().replace("sortedmulti", "multi");
|
||||
String descriptor = outputDescriptor.toString();
|
||||
|
||||
isPromptActive = true;
|
||||
String output;
|
||||
@@ -468,6 +478,24 @@ public class Hwi {
|
||||
}
|
||||
}
|
||||
|
||||
public static class TogglePassphraseService extends Service<Boolean> {
|
||||
private final Device device;
|
||||
|
||||
public TogglePassphraseService(Device device) {
|
||||
this.device = device;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Task<Boolean> createTask() {
|
||||
return new Task<>() {
|
||||
protected Boolean call() throws ImportException {
|
||||
Hwi hwi = new Hwi();
|
||||
return hwi.togglePassphrase(device);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static class DisplayAddressService extends Service<String> {
|
||||
private final Device device;
|
||||
private final String passphrase;
|
||||
@@ -595,6 +623,7 @@ public class Hwi {
|
||||
ENUMERATE("enumerate", true),
|
||||
PROMPT_PIN("promptpin", true),
|
||||
SEND_PIN("sendpin", false),
|
||||
TOGGLE_PASSPHRASE("togglepassphrase", true),
|
||||
DISPLAY_ADDRESS("displayaddress", true),
|
||||
SIGN_MESSAGE("signmessage", true),
|
||||
GET_XPUB("getxpub", true),
|
||||
|
||||
@@ -28,11 +28,21 @@ public class PassportMultisig extends ColdcardMultisig {
|
||||
|
||||
@Override
|
||||
public String getKeystoreImportDescription() {
|
||||
return "Import file or QR created from Settings > Pair Software Wallet > Sparrow > Multisig > microSD/QR on your Passport.";
|
||||
return "Import file or QR created from New Account > Sparrow > Multisig > QR Code/microSD on your Passport. For existing accounts, use Manage Account > Export by QR/microSD.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isKeystoreImportScannable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getWalletExportDescription() {
|
||||
return "As part of the New Account > Sparrow > Multisig flow, Passport will ask you to scan or import the multisig configuration from Sparrow.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isWalletExportScannable() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ public class PassportSinglesig extends ColdcardSinglesig {
|
||||
|
||||
@Override
|
||||
public String getKeystoreImportDescription() {
|
||||
return "Import file or QR created from Settings > Pair Software Wallet > Sparrow > Single Sig > microSD/QR on your Passport.";
|
||||
return "Import file or QR created from New Account > Sparrow > Standard > QR Code/microSD on your Passport. For existing accounts, use Manage Account > Export by QR/microSD.";
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.OutputStream;
|
||||
import java.nio.file.Files;
|
||||
|
||||
public class Sparrow implements WalletExport {
|
||||
private static final Logger log = LoggerFactory.getLogger(Sparrow.class);
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Sparrow";
|
||||
}
|
||||
|
||||
@Override
|
||||
public WalletModel getWalletModel() {
|
||||
return WalletModel.SPARROW;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException {
|
||||
try {
|
||||
Storage storage = AppServices.get().getOpenWallets().get(wallet);
|
||||
Files.copy(storage.getWalletFile().toPath(), outputStream);
|
||||
outputStream.flush();
|
||||
outputStream.close();
|
||||
} catch(Exception e) {
|
||||
log.error("Error exporting Sparrow wallet file", e);
|
||||
throw new ExportException("Error exporting Sparrow wallet file", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getWalletExportDescription() {
|
||||
return "Exports a copy of your Sparrow wallet file, which can be loaded in another Sparrow instance running on any supported platform.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getExportFileExtension(Wallet wallet) {
|
||||
Storage storage = AppServices.get().getOpenWallets().get(wallet);
|
||||
if(storage != null && (storage.getEncryptionPubKey() == null || Storage.NO_PASSWORD_KEY.equals(storage.getEncryptionPubKey()))) {
|
||||
return "json";
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isWalletExportScannable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean walletExportRequiresDecryption() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,8 @@ import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.KeystoreSource;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@@ -15,6 +17,8 @@ import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class SpecterDIY implements KeystoreFileImport {
|
||||
private static final Logger log = LoggerFactory.getLogger(SpecterDIY.class);
|
||||
|
||||
@Override
|
||||
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
|
||||
try {
|
||||
@@ -34,7 +38,8 @@ public class SpecterDIY implements KeystoreFileImport {
|
||||
|
||||
return keystore;
|
||||
} catch(IOException e) {
|
||||
throw new ImportException(e);
|
||||
log.error("Error getting Specter DIY keystore", e);
|
||||
throw new ImportException("Error getting Specter DIY keystore", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ import com.sparrowwallet.drongo.wallet.BlockTransactionHash;
|
||||
import com.sparrowwallet.drongo.wallet.InvalidWalletException;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
@@ -15,6 +17,8 @@ import java.io.OutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class SpecterDesktop implements WalletImport, WalletExport {
|
||||
private static final Logger log = LoggerFactory.getLogger(SpecterDesktop.class);
|
||||
|
||||
@Override
|
||||
public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException {
|
||||
try {
|
||||
@@ -29,7 +33,8 @@ public class SpecterDesktop implements WalletImport, WalletExport {
|
||||
outputStream.flush();
|
||||
outputStream.close();
|
||||
} catch(Exception e) {
|
||||
throw new ExportException(e);
|
||||
log.error("Error exporting Specter Desktop wallet", e);
|
||||
throw new ExportException("Error exporting Specter Desktop wallet", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +44,7 @@ public class SpecterDesktop implements WalletImport, WalletExport {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getExportFileExtension() {
|
||||
public String getExportFileExtension(Wallet wallet) {
|
||||
return "json";
|
||||
}
|
||||
|
||||
@@ -68,7 +73,8 @@ public class SpecterDesktop implements WalletImport, WalletExport {
|
||||
return wallet;
|
||||
}
|
||||
} catch(Exception e) {
|
||||
throw new ImportException(e);
|
||||
log.error("Error importing Specter Desktop wallet", e);
|
||||
throw new ImportException("Error importing Specter Desktop wallet", e);
|
||||
}
|
||||
|
||||
throw new ImportException("File was not a valid Specter Desktop wallet");
|
||||
@@ -99,6 +105,11 @@ public class SpecterDesktop implements WalletImport, WalletExport {
|
||||
return WalletModel.SPECTER_DESKTOP;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean walletExportRequiresDecryption() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public static class SpecterWallet {
|
||||
public String label;
|
||||
public Integer blockheight;
|
||||
|
||||
@@ -24,6 +24,8 @@ import java.lang.reflect.Type;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
@@ -44,6 +46,7 @@ public class Storage {
|
||||
public static final String WINDOWS_SPARROW_DIR = "Sparrow";
|
||||
public static final String WALLETS_DIR = "wallets";
|
||||
public static final String WALLETS_BACKUP_DIR = "backup";
|
||||
public static final String CERTS_DIR = "certs";
|
||||
public static final String HEADER_MAGIC_1 = "SPRW1";
|
||||
private static final int BINARY_HEADER_LENGTH = 28;
|
||||
public static final String TEMP_BACKUP_EXTENSION = "tmp";
|
||||
@@ -384,6 +387,37 @@ public class Storage {
|
||||
return walletsDir;
|
||||
}
|
||||
|
||||
public static File getCertificateFile(String host) {
|
||||
File certsDir = getCertsDir();
|
||||
File[] certs = certsDir.listFiles((dir, name) -> name.equals(host));
|
||||
if(certs.length > 0) {
|
||||
return certs[0];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void saveCertificate(String host, Certificate cert) {
|
||||
try(FileWriter writer = new FileWriter(new File(getCertsDir(), host))) {
|
||||
writer.write("-----BEGIN CERTIFICATE-----\n");
|
||||
writer.write(Base64.getEncoder().encodeToString(cert.getEncoded()).replaceAll("(.{64})", "$1\n"));
|
||||
writer.write("\n-----END CERTIFICATE-----\n");
|
||||
} catch(CertificateEncodingException e) {
|
||||
log.error("Error encoding PEM certificate", e);
|
||||
} catch(IOException e) {
|
||||
log.error("Error writing PEM certificate", e);
|
||||
}
|
||||
}
|
||||
|
||||
static File getCertsDir() {
|
||||
File certsDir = new File(getSparrowDir(), CERTS_DIR);
|
||||
if(!certsDir.exists()) {
|
||||
certsDir.mkdirs();
|
||||
}
|
||||
|
||||
return certsDir;
|
||||
}
|
||||
|
||||
static File getSparrowDir() {
|
||||
if(Network.get() != Network.MAINNET) {
|
||||
return new File(getSparrowHome(), Network.get().getName());
|
||||
|
||||
@@ -7,6 +7,7 @@ import java.io.OutputStream;
|
||||
public interface WalletExport extends Export {
|
||||
void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException;
|
||||
String getWalletExportDescription();
|
||||
String getExportFileExtension();
|
||||
String getExportFileExtension(Wallet wallet);
|
||||
boolean isWalletExportScannable();
|
||||
boolean walletExportRequiresDecryption();
|
||||
}
|
||||
|
||||
@@ -29,6 +29,8 @@ import java.io.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.locks.Condition;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class ElectrumServer {
|
||||
@@ -46,6 +48,8 @@ public class ElectrumServer {
|
||||
|
||||
private static String bwtElectrumServer;
|
||||
|
||||
private static final Pattern RPC_WALLET_LOADING_PATTERN = Pattern.compile(".*\"(Wallet loading failed:[^\"]*)\".*");
|
||||
|
||||
private static synchronized Transport getTransport() throws ServerException {
|
||||
if(transport == null) {
|
||||
try {
|
||||
@@ -824,11 +828,16 @@ public class ElectrumServer {
|
||||
bwtStartCondition.await();
|
||||
|
||||
if(!bwt.isReady()) {
|
||||
if(bwtStartException != null && bwtStartException.getMessage().contains("Wallet file not specified")) {
|
||||
throw new ServerException("Bitcoin Core requires Multi-Wallet to be enabled in the Server Preferences");
|
||||
} else {
|
||||
throw new ServerException("Check if Bitcoin Core is running, and the authentication details are correct.");
|
||||
if(bwtStartException != null) {
|
||||
Matcher walletLoadingMatcher = RPC_WALLET_LOADING_PATTERN.matcher(bwtStartException.getMessage());
|
||||
if(bwtStartException.getMessage().contains("Wallet file not specified")) {
|
||||
throw new ServerException("Bitcoin Core requires Multi-Wallet to be enabled in the Server Preferences");
|
||||
} else if(walletLoadingMatcher.matches() && walletLoadingMatcher.group(1) != null) {
|
||||
throw new ServerException(walletLoadingMatcher.group(1));
|
||||
}
|
||||
}
|
||||
|
||||
throw new ServerException("Check if Bitcoin Core is running, and the authentication details are correct.");
|
||||
}
|
||||
} catch(InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
@@ -944,7 +953,7 @@ public class ElectrumServer {
|
||||
});
|
||||
disconnectionService.start();
|
||||
} else if(subscribe) {
|
||||
EventManager.get().post(new DisconnectionEvent());
|
||||
Platform.runLater(() -> EventManager.get().post(new DisconnectionEvent()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -56,7 +56,11 @@ public enum ExchangeSource {
|
||||
Gson gson = new Gson();
|
||||
return gson.fromJson(reader, CoinbaseRates.class);
|
||||
} catch (Exception e) {
|
||||
log.error("Error retrieving currency rates", e);
|
||||
if(log.isDebugEnabled()) {
|
||||
log.warn("Error retrieving currency rates", e);
|
||||
} else {
|
||||
log.warn("Error retrieving currency rates (" + e.getMessage() + ")");
|
||||
}
|
||||
return new CoinbaseRates();
|
||||
}
|
||||
}
|
||||
@@ -86,8 +90,12 @@ public enum ExchangeSource {
|
||||
try(InputStream is = (proxy == null ? new URL(url).openStream() : new URL(url).openConnection(proxy).getInputStream()); Reader reader = new InputStreamReader(is, StandardCharsets.UTF_8)) {
|
||||
Gson gson = new Gson();
|
||||
return gson.fromJson(reader, CoinGeckoRates.class);
|
||||
} catch (Exception e) {
|
||||
log.error("Error retrieving currency rates", e);
|
||||
} catch(Exception e) {
|
||||
if(log.isDebugEnabled()) {
|
||||
log.warn("Error retrieving currency rates", e);
|
||||
} else {
|
||||
log.warn("Error retrieving currency rates (" + e.getMessage() + ")");
|
||||
}
|
||||
return new CoinGeckoRates();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ public enum FeeRatesSource {
|
||||
try(InputStream is = (proxy == null ? new URL(url).openStream() : new URL(url).openConnection(proxy).getInputStream()); Reader reader = new InputStreamReader(is, StandardCharsets.UTF_8)) {
|
||||
Gson gson = new Gson();
|
||||
ThreeTierRates threeTierRates = gson.fromJson(reader, ThreeTierRates.class);
|
||||
Double lastRate = null;
|
||||
for(Integer blockTarget : defaultblockTargetFeeRates.keySet()) {
|
||||
if(blockTarget < BLOCKS_IN_HALF_HOUR) {
|
||||
blockTargetFeeRates.put(blockTarget, threeTierRates.fastestFee);
|
||||
@@ -70,16 +71,27 @@ public enum FeeRatesSource {
|
||||
blockTargetFeeRates.put(blockTarget, threeTierRates.halfHourFee);
|
||||
} else if(blockTarget < BLOCKS_IN_TWO_HOURS || defaultblockTargetFeeRates.get(blockTarget) > threeTierRates.hourFee) {
|
||||
blockTargetFeeRates.put(blockTarget, threeTierRates.hourFee);
|
||||
} else if(threeTierRates.minimumFee != null && defaultblockTargetFeeRates.get(blockTarget) < threeTierRates.minimumFee) {
|
||||
blockTargetFeeRates.put(blockTarget, threeTierRates.minimumFee + (threeTierRates.hourFee > threeTierRates.minimumFee ? threeTierRates.hourFee * 0.2 : 0.0));
|
||||
} else {
|
||||
blockTargetFeeRates.put(blockTarget, defaultblockTargetFeeRates.get(blockTarget));
|
||||
}
|
||||
|
||||
if(lastRate != null) {
|
||||
blockTargetFeeRates.put(blockTarget, Math.min(lastRate, blockTargetFeeRates.get(blockTarget)));
|
||||
}
|
||||
lastRate = blockTargetFeeRates.get(blockTarget);
|
||||
}
|
||||
|
||||
if(threeTierRates.minimumFee != null) {
|
||||
blockTargetFeeRates.put(Integer.MAX_VALUE, threeTierRates.minimumFee);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("Error retrieving recommended fee rates from " + url, e);
|
||||
if(log.isDebugEnabled()) {
|
||||
log.warn("Error retrieving recommended fee rates from " + url, e);
|
||||
} else {
|
||||
log.warn("Error retrieving recommended fee rates from " + url + " (" + e.getMessage() + ")");
|
||||
}
|
||||
}
|
||||
|
||||
return blockTargetFeeRates;
|
||||
|
||||
@@ -39,7 +39,7 @@ public enum Protocol {
|
||||
},
|
||||
SSL {
|
||||
@Override
|
||||
public Transport getTransport(HostAndPort server) throws KeyManagementException, NoSuchAlgorithmException {
|
||||
public Transport getTransport(HostAndPort server) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException {
|
||||
if(isOnionAddress(server)) {
|
||||
return new TorTcpOverTlsTransport(server);
|
||||
}
|
||||
@@ -57,7 +57,7 @@ public enum Protocol {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Transport getTransport(HostAndPort server, HostAndPort proxy) throws NoSuchAlgorithmException, KeyManagementException {
|
||||
public Transport getTransport(HostAndPort server, HostAndPort proxy) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException {
|
||||
return new ProxyTcpOverTlsTransport(server, proxy);
|
||||
}
|
||||
|
||||
@@ -68,27 +68,27 @@ public enum Protocol {
|
||||
},
|
||||
HTTP {
|
||||
@Override
|
||||
public Transport getTransport(HostAndPort server) throws KeyManagementException, NoSuchAlgorithmException {
|
||||
public Transport getTransport(HostAndPort server) {
|
||||
throw new UnsupportedOperationException("No transport supported for HTTP");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Transport getTransport(HostAndPort server, File serverCert) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException {
|
||||
public Transport getTransport(HostAndPort server, File serverCert) {
|
||||
throw new UnsupportedOperationException("No transport supported for HTTP");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Transport getTransport(HostAndPort server, HostAndPort proxy) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException {
|
||||
public Transport getTransport(HostAndPort server, HostAndPort proxy) {
|
||||
throw new UnsupportedOperationException("No transport supported for HTTP");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Transport getTransport(HostAndPort server, File serverCert, HostAndPort proxy) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException {
|
||||
public Transport getTransport(HostAndPort server, File serverCert, HostAndPort proxy) {
|
||||
throw new UnsupportedOperationException("No transport supported for HTTP");
|
||||
}
|
||||
};
|
||||
|
||||
public abstract Transport getTransport(HostAndPort server) throws KeyManagementException, NoSuchAlgorithmException;
|
||||
public abstract Transport getTransport(HostAndPort server) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException;
|
||||
|
||||
public abstract Transport getTransport(HostAndPort server, File serverCert) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException;
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.sparrowwallet.sparrow.net;
|
||||
|
||||
public class ProxyServerException extends ServerException {
|
||||
public ProxyServerException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ public class ProxyTcpOverTlsTransport extends TcpOverTlsTransport {
|
||||
|
||||
private final HostAndPort proxy;
|
||||
|
||||
public ProxyTcpOverTlsTransport(HostAndPort server, HostAndPort proxy) throws KeyManagementException, NoSuchAlgorithmException {
|
||||
public ProxyTcpOverTlsTransport(HostAndPort server, HostAndPort proxy) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException {
|
||||
super(server);
|
||||
this.proxy = proxy;
|
||||
}
|
||||
@@ -34,7 +34,7 @@ public class ProxyTcpOverTlsTransport extends TcpOverTlsTransport {
|
||||
Socket underlying = new Socket(new Proxy(Proxy.Type.SOCKS, proxyAddr));
|
||||
underlying.connect(new InetSocketAddress(server.getHost(), server.getPortOrDefault(DEFAULT_PORT)));
|
||||
SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(underlying, proxy.getHost(), proxy.getPortOrDefault(DEFAULT_PROXY_PORT), true);
|
||||
sslSocket.startHandshake();
|
||||
startHandshake(sslSocket);
|
||||
|
||||
return sslSocket;
|
||||
}
|
||||
|
||||
@@ -145,6 +145,9 @@ public class SimpleElectrumServerRpc implements ElectrumServerRpc {
|
||||
String blockHeader = new RetryLogic<String>(MAX_RETRIES, RETRY_DELAY, List.of(IllegalStateException.class, IllegalArgumentException.class)).getResult(() ->
|
||||
client.createRequest().returnAs(String.class).method("blockchain.block.header").id(idCounter.incrementAndGet()).params(blockHeight).execute());
|
||||
result.put(blockHeight, blockHeader);
|
||||
} catch(ServerException e) {
|
||||
//If there is an error with the server connection, don't keep trying - this may take too long given many blocks
|
||||
throw new ElectrumServerRpcException("Failed to retrieve block header for block height: " + blockHeight, e);
|
||||
} catch(JsonRpcException e) {
|
||||
log.warn("Failed to retrieve block header for block height: " + blockHeight + " (" + e.getErrorMessage() + ")");
|
||||
} catch(Exception e) {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package com.sparrowwallet.sparrow.net;
|
||||
|
||||
import com.google.common.net.HostAndPort;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.net.ssl.*;
|
||||
import java.io.File;
|
||||
@@ -8,35 +11,23 @@ import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.Socket;
|
||||
import java.security.*;
|
||||
import java.security.cert.*;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
|
||||
public class TcpOverTlsTransport extends TcpTransport {
|
||||
private static final Logger log = LoggerFactory.getLogger(TcpOverTlsTransport.class);
|
||||
|
||||
public static final int DEFAULT_PORT = 50002;
|
||||
|
||||
protected final SSLSocketFactory sslSocketFactory;
|
||||
|
||||
public TcpOverTlsTransport(HostAndPort server) throws NoSuchAlgorithmException, KeyManagementException {
|
||||
public TcpOverTlsTransport(HostAndPort server) throws NoSuchAlgorithmException, KeyManagementException, CertificateException, KeyStoreException, IOException {
|
||||
super(server);
|
||||
|
||||
TrustManager[] trustAllCerts = new TrustManager[]{
|
||||
new X509TrustManager() {
|
||||
public X509Certificate[] getAcceptedIssuers() {
|
||||
return new X509Certificate[0];
|
||||
}
|
||||
|
||||
public void checkClientTrusted(X509Certificate[] certs, String authType) {
|
||||
}
|
||||
|
||||
public void checkServerTrusted(X509Certificate[] certs, String authType) {
|
||||
}
|
||||
}
|
||||
};
|
||||
TrustManager[] trustManagers = getTrustManagers(Storage.getCertificateFile(server.getHost()));
|
||||
|
||||
SSLContext sslContext = SSLContext.getInstance("TLS");
|
||||
sslContext.init(null, trustAllCerts, new SecureRandom());
|
||||
sslContext.init(null, trustManagers, new SecureRandom());
|
||||
|
||||
this.sslSocketFactory = sslContext.getSocketFactory();
|
||||
}
|
||||
@@ -44,25 +35,77 @@ public class TcpOverTlsTransport extends TcpTransport {
|
||||
public TcpOverTlsTransport(HostAndPort server, File crtFile) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException {
|
||||
super(server);
|
||||
|
||||
Certificate certificate = CertificateFactory.getInstance("X.509").generateCertificate(new FileInputStream(crtFile));
|
||||
|
||||
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
|
||||
keyStore.load(null, null);
|
||||
keyStore.setCertificateEntry("electrumx", certificate);
|
||||
|
||||
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
|
||||
trustManagerFactory.init(keyStore);
|
||||
TrustManager[] trustManagers = getTrustManagers(crtFile);
|
||||
|
||||
SSLContext sslContext = SSLContext.getInstance("TLS");
|
||||
sslContext.init(null, trustManagerFactory.getTrustManagers(), null);
|
||||
sslContext.init(null, trustManagers, null);
|
||||
|
||||
sslSocketFactory = sslContext.getSocketFactory();
|
||||
}
|
||||
|
||||
private TrustManager[] getTrustManagers(File crtFile) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException {
|
||||
if(crtFile == null) {
|
||||
return new TrustManager[] {
|
||||
new X509TrustManager() {
|
||||
public X509Certificate[] getAcceptedIssuers() {
|
||||
return new X509Certificate[0];
|
||||
}
|
||||
|
||||
public void checkClientTrusted(X509Certificate[] certs, String authType) throws CertificateException {
|
||||
}
|
||||
|
||||
public void checkServerTrusted(X509Certificate[] certs, String authType) throws CertificateException {
|
||||
if(certs.length == 0) {
|
||||
throw new CertificateException("No server certificate provided");
|
||||
}
|
||||
|
||||
certs[0].checkValidity();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Certificate certificate = CertificateFactory.getInstance("X.509").generateCertificate(new FileInputStream(crtFile));
|
||||
if(certificate instanceof X509Certificate) {
|
||||
try {
|
||||
X509Certificate x509Certificate = (X509Certificate)certificate;
|
||||
x509Certificate.checkValidity();
|
||||
} catch(CertificateException e) {
|
||||
crtFile.delete();
|
||||
return getTrustManagers(null);
|
||||
}
|
||||
}
|
||||
|
||||
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
|
||||
keyStore.load(null, null);
|
||||
keyStore.setCertificateEntry("electrum-server", certificate);
|
||||
|
||||
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
|
||||
trustManagerFactory.init(keyStore);
|
||||
|
||||
return trustManagerFactory.getTrustManagers();
|
||||
}
|
||||
|
||||
protected Socket createSocket() throws IOException {
|
||||
SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(server.getHost(), server.getPortOrDefault(DEFAULT_PORT));
|
||||
sslSocket.startHandshake();
|
||||
|
||||
startHandshake(sslSocket);
|
||||
return sslSocket;
|
||||
}
|
||||
|
||||
protected void startHandshake(SSLSocket sslSocket) throws IOException {
|
||||
sslSocket.addHandshakeCompletedListener(event -> {
|
||||
if(Storage.getCertificateFile(server.getHost()) == null) {
|
||||
try {
|
||||
Certificate[] certs = event.getPeerCertificates();
|
||||
if(certs.length > 0) {
|
||||
Storage.saveCertificate(server.getHost(), certs[0]);
|
||||
}
|
||||
} catch(SSLPeerUnverifiedException e) {
|
||||
log.warn("Attempting to retrieve certificate for unverified peer", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
sslSocket.startHandshake();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.net.SocketFactory;
|
||||
import javax.net.ssl.SSLHandshakeException;
|
||||
import java.io.*;
|
||||
import java.net.Socket;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
@@ -192,7 +193,13 @@ public class TcpTransport implements Transport, Closeable {
|
||||
try {
|
||||
socket = createSocket();
|
||||
running = true;
|
||||
} catch(SSLHandshakeException e) {
|
||||
throw new TlsServerException(server, e);
|
||||
} catch(IOException e) {
|
||||
if(e.getStackTrace().length > 0 && e.getStackTrace()[0].getClassName().contains("SocksSocketImpl")) {
|
||||
throw new ProxyServerException(e);
|
||||
}
|
||||
|
||||
throw new ServerException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.sparrowwallet.sparrow.net;
|
||||
|
||||
import com.google.common.net.HostAndPort;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
|
||||
import java.io.File;
|
||||
import java.security.cert.CertificateExpiredException;
|
||||
import java.security.cert.CertificateNotYetValidException;
|
||||
|
||||
public class TlsServerException extends ServerException {
|
||||
private final HostAndPort server;
|
||||
|
||||
public TlsServerException(HostAndPort server) {
|
||||
this.server = server;
|
||||
}
|
||||
|
||||
public TlsServerException(HostAndPort server, String message) {
|
||||
super(message);
|
||||
this.server = server;
|
||||
}
|
||||
|
||||
public TlsServerException(HostAndPort server, Throwable cause) {
|
||||
this(server, getMessage(cause, server), cause);
|
||||
}
|
||||
|
||||
public TlsServerException(HostAndPort server, String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
this.server = server;
|
||||
}
|
||||
|
||||
public HostAndPort getServer() {
|
||||
return server;
|
||||
}
|
||||
|
||||
private static String getMessage(Throwable cause, HostAndPort server) {
|
||||
if(cause != null) {
|
||||
if(cause.getMessage().contains("PKIX path building failed")) {
|
||||
File configCrtFile = Config.get().getElectrumServerCert();
|
||||
File savedCrtFile = Storage.getCertificateFile(server.getHost());
|
||||
if(configCrtFile != null) {
|
||||
return "Provided server certificate from " + server.getHost() + " did not match configured certificate at " + configCrtFile.getAbsolutePath();
|
||||
} else if(savedCrtFile != null) {
|
||||
return "Provided server certificate from " + server.getHost() + " did not match previously saved certificate at " + savedCrtFile.getAbsolutePath();
|
||||
}
|
||||
|
||||
return "Provided server certificate from " + server.getHost() + " was invalid: " + (cause.getCause() != null ? cause.getCause().getMessage() : cause.getMessage());
|
||||
} else if(cause.getCause() instanceof CertificateNotYetValidException) {
|
||||
return cause.getMessage().replace("NotBefore:", "Certificate only valid from");
|
||||
} else if(cause.getCause() instanceof CertificateExpiredException) {
|
||||
return cause.getMessage().replace("NotAfter:", "Certificate expired at");
|
||||
}
|
||||
|
||||
return cause.getMessage();
|
||||
}
|
||||
|
||||
return "SSL Handshake Error";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.sparrowwallet.sparrow.net;
|
||||
|
||||
public class TorServerAlreadyBoundException extends TorServerException {
|
||||
public TorServerAlreadyBoundException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public TorServerAlreadyBoundException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.sparrowwallet.sparrow.net;
|
||||
|
||||
public class TorServerException extends ServerException {
|
||||
public TorServerException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public TorServerException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,7 @@ public class TorService extends ScheduledService<NativeTor> {
|
||||
@Override
|
||||
protected Task<NativeTor> createTask() {
|
||||
return new Task<>() {
|
||||
protected NativeTor call() throws IOException {
|
||||
protected NativeTor call() throws IOException, TorServerException {
|
||||
if(Tor.getDefault() == null) {
|
||||
Path path = Files.createTempDirectory(TOR_DIR_PREFIX);
|
||||
File torInstallDir = path.toFile();
|
||||
@@ -45,11 +45,15 @@ public class TorService extends ScheduledService<NativeTor> {
|
||||
|
||||
return new NativeTor(torInstallDir, Collections.emptyList(), override);
|
||||
} catch(TorCtlException e) {
|
||||
log.error("Failed to start Tor", e);
|
||||
if(e.getCause() instanceof TorControlError) {
|
||||
throw new IOException("Failed to start Tor", e.getCause());
|
||||
if(e.getCause().getMessage().contains("Failed to bind")) {
|
||||
throw new TorServerAlreadyBoundException("Tor server already bound", e.getCause());
|
||||
}
|
||||
log.error("Failed to start Tor", e);
|
||||
throw new TorServerException("Failed to start Tor", e.getCause());
|
||||
} else {
|
||||
throw new IOException("Failed to start Tor", e);
|
||||
log.error("Failed to start Tor", e);
|
||||
throw new TorServerException("Failed to start Tor", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import java.security.cert.CertificateException;
|
||||
public class TorTcpOverTlsTransport extends TcpOverTlsTransport {
|
||||
private static final Logger log = LoggerFactory.getLogger(TorTcpOverTlsTransport.class);
|
||||
|
||||
public TorTcpOverTlsTransport(HostAndPort server) throws NoSuchAlgorithmException, KeyManagementException {
|
||||
public TorTcpOverTlsTransport(HostAndPort server) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException {
|
||||
super(server);
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ public class TorTcpOverTlsTransport extends TcpOverTlsTransport {
|
||||
}
|
||||
|
||||
SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(socket, server.getHost(), server.getPortOrDefault(DEFAULT_PORT), true);
|
||||
sslSocket.startHandshake();
|
||||
startHandshake(sslSocket);
|
||||
|
||||
return sslSocket;
|
||||
}
|
||||
|
||||
@@ -33,6 +33,9 @@ public class GeneralPreferencesController extends PreferencesDetailController {
|
||||
@FXML
|
||||
private ComboBox<ExchangeSource> exchangeSource;
|
||||
|
||||
@FXML
|
||||
private UnlabeledToggleSwitch loadRecentWallets;
|
||||
|
||||
@FXML
|
||||
private UnlabeledToggleSwitch validateDerivationPaths;
|
||||
|
||||
@@ -97,6 +100,12 @@ public class GeneralPreferencesController extends PreferencesDetailController {
|
||||
|
||||
updateCurrencies(exchangeSource.getSelectionModel().getSelectedItem());
|
||||
|
||||
loadRecentWallets.setSelected(config.isLoadRecentWallets());
|
||||
loadRecentWallets.selectedProperty().addListener((observableValue, oldValue, newValue) -> {
|
||||
config.setLoadRecentWallets(newValue);
|
||||
EventManager.get().post(new RequestOpenWalletsEvent());
|
||||
});
|
||||
|
||||
validateDerivationPaths.setSelected(config.isValidateDerivationPaths());
|
||||
validateDerivationPaths.selectedProperty().addListener((observableValue, oldValue, newValue) -> {
|
||||
config.setValidateDerivationPaths(newValue);
|
||||
|
||||
@@ -47,7 +47,7 @@ public class PreferencesDialog extends Dialog<Boolean> {
|
||||
}
|
||||
|
||||
dialogPane.setPrefWidth(750);
|
||||
dialogPane.setPrefHeight(600);
|
||||
dialogPane.setPrefHeight(630);
|
||||
|
||||
preferencesController.reconnectOnClosingProperty().set(AppServices.isConnecting() || AppServices.isConnected());
|
||||
setOnCloseRequest(event -> {
|
||||
|
||||
+50
-6
@@ -11,6 +11,7 @@ import com.sparrowwallet.sparrow.control.UnlabeledToggleSwitch;
|
||||
import com.sparrowwallet.sparrow.event.*;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
import com.sparrowwallet.sparrow.net.*;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
@@ -35,13 +36,13 @@ import org.slf4j.LoggerFactory;
|
||||
import tornadofx.control.Field;
|
||||
import tornadofx.control.Form;
|
||||
|
||||
import javax.net.ssl.SSLHandshakeException;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Random;
|
||||
|
||||
public class ServerPreferencesController extends PreferencesDetailController {
|
||||
@@ -151,6 +152,8 @@ public class ServerPreferencesController extends PreferencesDetailController {
|
||||
|
||||
private Boolean useSslOriginal;
|
||||
|
||||
private Boolean useProxyOriginal;
|
||||
|
||||
@Override
|
||||
public void initializeView(Config config) {
|
||||
EventManager.get().register(this);
|
||||
@@ -303,7 +306,7 @@ public class ServerPreferencesController extends PreferencesDetailController {
|
||||
testConnection.setGraphic(getGlyph(FontAwesome5.Glyph.ELLIPSIS_H, null));
|
||||
testResults.setText("Connecting to " + config.getServerAddress() + "...");
|
||||
|
||||
if(Config.get().requiresTor() && Tor.getDefault() == null) {
|
||||
if(Config.get().requiresInternalTor() && Tor.getDefault() == null) {
|
||||
startTor();
|
||||
} else {
|
||||
startElectrumConnection();
|
||||
@@ -424,6 +427,20 @@ public class ServerPreferencesController extends PreferencesDetailController {
|
||||
torService.cancel();
|
||||
testResults.appendText("\nTor failed to start");
|
||||
showConnectionFailure(workerStateEvent.getSource().getException());
|
||||
|
||||
Throwable exception = workerStateEvent.getSource().getException();
|
||||
if(Config.get().getServerType() == ServerType.ELECTRUM_SERVER &&
|
||||
exception instanceof TorServerAlreadyBoundException &&
|
||||
useProxyOriginal == null && !useProxy.isSelected() &&
|
||||
(proxyHost.getText().isEmpty() || proxyHost.getText().equals("localhost") || proxyHost.getText().equals("127.0.0.1")) &&
|
||||
(proxyPort.getText().isEmpty() || proxyPort.getText().equals("9050"))) {
|
||||
useProxy.setSelected(true);
|
||||
proxyHost.setText("localhost");
|
||||
proxyPort.setText("9050");
|
||||
useProxyOriginal = false;
|
||||
testResults.appendText("\n\nAssuming Tor proxy is running on port 9050 and trying again...");
|
||||
startElectrumConnection();
|
||||
}
|
||||
});
|
||||
|
||||
torService.start();
|
||||
@@ -448,6 +465,7 @@ public class ServerPreferencesController extends PreferencesDetailController {
|
||||
getMasterController().reconnectOnClosingProperty().set(true);
|
||||
Config.get().setMode(Mode.ONLINE);
|
||||
connectionService.cancel();
|
||||
useProxyOriginal = null;
|
||||
});
|
||||
connectionService.setOnFailed(workerStateEvent -> {
|
||||
EventManager.get().unregister(connectionService);
|
||||
@@ -475,6 +493,13 @@ public class ServerPreferencesController extends PreferencesDetailController {
|
||||
useSslOriginal = null;
|
||||
}
|
||||
}
|
||||
|
||||
if(useProxyOriginal != null && !useProxyOriginal) {
|
||||
useProxy.setSelected(false);
|
||||
proxyHost.setText("");
|
||||
proxyPort.setText("");
|
||||
useProxyOriginal = null;
|
||||
}
|
||||
});
|
||||
connectionService.start();
|
||||
}
|
||||
@@ -523,10 +548,29 @@ public class ServerPreferencesController extends PreferencesDetailController {
|
||||
private void showConnectionFailure(Throwable exception) {
|
||||
log.error("Connection error", exception);
|
||||
String reason = exception.getCause() != null ? exception.getCause().getMessage() : exception.getMessage();
|
||||
if(exception.getCause() != null && exception.getCause() instanceof SSLHandshakeException) {
|
||||
reason = "SSL Handshake Error\n" + reason;
|
||||
}
|
||||
if(exception.getCause() != null && exception.getCause() instanceof TorControlError && exception.getCause().getMessage().contains("Failed to bind")) {
|
||||
if(exception instanceof TlsServerException && exception.getCause() != null) {
|
||||
TlsServerException tlsServerException = (TlsServerException)exception;
|
||||
if(exception.getCause().getMessage().contains("PKIX path building failed")) {
|
||||
File configCrtFile = Config.get().getElectrumServerCert();
|
||||
File savedCrtFile = Storage.getCertificateFile(tlsServerException.getServer().getHost());
|
||||
if(configCrtFile == null && savedCrtFile != null) {
|
||||
Optional<ButtonType> optButton = AppServices.showErrorDialog("SSL Handshake Failed", "The certificate provided by the server at " + tlsServerException.getServer().getHost() + " appears to have changed." +
|
||||
"\n\nThis may indicate a man-in-the-middle attack!" +
|
||||
"\n\nDo you still want to proceed?", ButtonType.NO, ButtonType.YES);
|
||||
if(optButton.isPresent()) {
|
||||
if(optButton.get() == ButtonType.YES) {
|
||||
savedCrtFile.delete();
|
||||
Platform.runLater(this::startElectrumConnection);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reason = tlsServerException.getMessage() + "\n\n" + reason;
|
||||
} else if(exception instanceof ProxyServerException) {
|
||||
reason += ". Check if the proxy server is running.";
|
||||
} else if(exception instanceof TorServerAlreadyBoundException) {
|
||||
reason += "\nIs a Tor proxy already running on port " + TorService.PROXY_PORT + "?";
|
||||
}
|
||||
|
||||
|
||||
@@ -680,10 +680,8 @@ public class HeadersController extends TransactionFormController implements Init
|
||||
file = new File(file.getAbsolutePath() + ".psbt");
|
||||
}
|
||||
|
||||
try {
|
||||
try(PrintWriter writer = new PrintWriter(file, StandardCharsets.UTF_8)) {
|
||||
writer.print(headersForm.getPsbt().toBase64String());
|
||||
}
|
||||
try(FileOutputStream outputStream = new FileOutputStream(file)) {
|
||||
outputStream.write(headersForm.getPsbt().serialize());
|
||||
} catch(IOException e) {
|
||||
log.error("Error saving PSBT", e);
|
||||
AppServices.showErrorDialog("Error saving PSBT", "Cannot write to " + file.getAbsolutePath());
|
||||
@@ -716,7 +714,7 @@ public class HeadersController extends TransactionFormController implements Init
|
||||
File file = headersForm.getAvailableWallets().get(headersForm.getSigningWallet()).getWalletFile();
|
||||
|
||||
if(copy.isEncrypted()) {
|
||||
WalletPasswordDialog dlg = new WalletPasswordDialog(WalletPasswordDialog.PasswordRequirement.LOAD);
|
||||
WalletPasswordDialog dlg = new WalletPasswordDialog(copy.getName(), WalletPasswordDialog.PasswordRequirement.LOAD);
|
||||
Optional<SecureString> password = dlg.showAndWait();
|
||||
if(password.isPresent()) {
|
||||
Storage.DecryptWalletService decryptWalletService = new Storage.DecryptWalletService(copy, password.get());
|
||||
@@ -846,10 +844,13 @@ public class HeadersController extends TransactionFormController implements Init
|
||||
headersForm.setBlockTransaction(blockTransaction);
|
||||
updateBlockchainForm(blockTransaction, AppServices.getCurrentBlockHeight());
|
||||
}
|
||||
EventManager.get().post(new TransactionReferencesFinishedEvent(headersForm.getTransaction(), blockTransaction));
|
||||
});
|
||||
transactionReferenceService.setOnFailed(failedEvent -> {
|
||||
log.error("Error fetching broadcasted transaction", failedEvent.getSource().getException());
|
||||
EventManager.get().post(new TransactionReferencesFailedEvent(headersForm.getTransaction(), failedEvent.getSource().getException()));
|
||||
});
|
||||
EventManager.get().post(new TransactionReferencesStartedEvent(headersForm.getTransaction()));
|
||||
transactionReferenceService.start();
|
||||
}
|
||||
});
|
||||
@@ -1132,10 +1133,13 @@ public class HeadersController extends TransactionFormController implements Init
|
||||
headersForm.setBlockTransaction(blockTransaction);
|
||||
updateBlockchainForm(blockTransaction, AppServices.getCurrentBlockHeight());
|
||||
}
|
||||
EventManager.get().post(new TransactionReferencesFinishedEvent(headersForm.getTransaction(), blockTransaction));
|
||||
});
|
||||
transactionReferenceService.setOnFailed(failEvent -> {
|
||||
log.error("Could not update block transaction", failEvent.getSource().getException());
|
||||
EventManager.get().post(new TransactionReferencesFailedEvent(headersForm.getTransaction(), failEvent.getSource().getException()));
|
||||
});
|
||||
EventManager.get().post(new TransactionReferencesStartedEvent(headersForm.getTransaction()));
|
||||
transactionReferenceService.start();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -374,12 +374,14 @@ public class TransactionController implements Initializable {
|
||||
|
||||
final BlockTransaction blockTx = thisBlockTx;
|
||||
Platform.runLater(() -> {
|
||||
EventManager.get().post(new BlockTransactionFetchedEvent(getTransaction().getTxId(), blockTx, inputTransactions, indexStart, maxIndex));
|
||||
EventManager.get().post(new BlockTransactionFetchedEvent(getTransaction(), blockTx, inputTransactions, indexStart, maxIndex));
|
||||
});
|
||||
});
|
||||
transactionReferenceService.setOnFailed(failedEvent -> {
|
||||
log.error("Error fetching transaction or input references", failedEvent.getSource().getException());
|
||||
EventManager.get().post(new TransactionReferencesFailedEvent(getTransaction(), failedEvent.getSource().getException(), indexStart, maxIndex));
|
||||
});
|
||||
EventManager.get().post(new TransactionReferencesStartedEvent(getTransaction(), indexStart, maxIndex));
|
||||
transactionReferenceService.start();
|
||||
}
|
||||
}
|
||||
@@ -391,12 +393,14 @@ public class TransactionController implements Initializable {
|
||||
transactionOutputsReferenceService.setOnSucceeded(successEvent -> {
|
||||
List<BlockTransaction> outputTransactions = transactionOutputsReferenceService.getValue();
|
||||
Platform.runLater(() -> {
|
||||
EventManager.get().post(new BlockTransactionOutputsFetchedEvent(getTransaction().getTxId(), outputTransactions, indexStart, maxIndex));
|
||||
EventManager.get().post(new BlockTransactionOutputsFetchedEvent(getTransaction(), outputTransactions, indexStart, maxIndex));
|
||||
});
|
||||
});
|
||||
transactionOutputsReferenceService.setOnFailed(failedEvent -> {
|
||||
log.error("Error fetching transaction output references", failedEvent.getSource().getException());
|
||||
EventManager.get().post(new TransactionReferencesFailedEvent(getTransaction(), failedEvent.getSource().getException(), indexStart, maxIndex));
|
||||
});
|
||||
EventManager.get().post(new TransactionReferencesStartedEvent(getTransaction(), indexStart, maxIndex));
|
||||
transactionOutputsReferenceService.start();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,33 @@
|
||||
package com.sparrowwallet.sparrow.wallet;
|
||||
|
||||
import com.csvreader.CsvWriter;
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletNode;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.control.AddressTreeTable;
|
||||
import com.sparrowwallet.sparrow.event.*;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.fxml.Initializable;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.stage.Stage;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
public class AddressesController extends WalletFormController implements Initializable {
|
||||
private static final Logger log = LoggerFactory.getLogger(AddressesController.class);
|
||||
public static final int DEFAULT_EXPORT_ADDRESSES_LENGTH = 250;
|
||||
|
||||
@FXML
|
||||
private AddressTreeTable receiveTable;
|
||||
|
||||
@@ -83,4 +97,45 @@ public class AddressesController extends WalletFormController implements Initial
|
||||
changeTable.updateAll(getWalletForm().getNodeEntry(KeyPurpose.CHANGE));
|
||||
}
|
||||
}
|
||||
|
||||
public void exportReceiveAddresses(ActionEvent event) {
|
||||
exportAddresses(KeyPurpose.RECEIVE);
|
||||
}
|
||||
|
||||
public void exportChangeAddresses(ActionEvent event) {
|
||||
exportAddresses(KeyPurpose.CHANGE);
|
||||
}
|
||||
|
||||
private void exportAddresses(KeyPurpose keyPurpose) {
|
||||
Stage window = new Stage();
|
||||
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle("Export Addresses to CSV");
|
||||
fileChooser.setInitialFileName(getWalletForm().getWallet().getName() + "-" + keyPurpose.name().toLowerCase() + "-addresses.txt");
|
||||
|
||||
Wallet copy = getWalletForm().getWallet().copy();
|
||||
WalletNode purposeNode = copy.getNode(keyPurpose);
|
||||
purposeNode.fillToIndex(Math.max(purposeNode.getChildren().size(), DEFAULT_EXPORT_ADDRESSES_LENGTH));
|
||||
|
||||
File file = fileChooser.showSaveDialog(window);
|
||||
if(file != null) {
|
||||
try(FileOutputStream outputStream = new FileOutputStream(file)) {
|
||||
CsvWriter writer = new CsvWriter(outputStream, ',', StandardCharsets.UTF_8);
|
||||
writer.writeRecord(new String[] {"Index", "Payment Address", "Derivation", "Label"});
|
||||
for(WalletNode indexNode : purposeNode.getChildren()) {
|
||||
writer.write(Integer.toString(indexNode.getIndex()));
|
||||
writer.write(copy.getAddress(indexNode).toString());
|
||||
writer.write(getDerivationPath(indexNode));
|
||||
Optional<Entry> optLabelEntry = getWalletForm().getNodeEntry(keyPurpose).getChildren().stream()
|
||||
.filter(entry -> ((NodeEntry)entry).getNode().getIndex() == indexNode.getIndex()).findFirst();
|
||||
writer.write(optLabelEntry.isPresent() ? optLabelEntry.get().getLabel() : "");
|
||||
writer.endRecord();
|
||||
}
|
||||
writer.close();
|
||||
} catch(IOException e) {
|
||||
log.error("Error exporting addresses as CSV", e);
|
||||
AppServices.showErrorDialog("Error exporting addresses as CSV", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -252,12 +252,21 @@ public class KeystoreController extends WalletFormController implements Initiali
|
||||
importButton.setTooltip(new Tooltip(keystore.getSource() == KeystoreSource.SW_WATCH ? "Import a keystore from an external source" : "Replace this keystore with another source"));
|
||||
|
||||
boolean editable = (keystore.getSource() == KeystoreSource.SW_WATCH);
|
||||
fingerprint.setEditable(editable);
|
||||
derivation.setEditable(editable);
|
||||
xpub.setEditable(editable);
|
||||
setEditable(fingerprint, editable);
|
||||
setEditable(derivation, editable);
|
||||
setEditable(xpub, editable);
|
||||
scanXpubQR.setVisible(editable);
|
||||
}
|
||||
|
||||
private void setEditable(TextInputControl textInputControl, boolean editable) {
|
||||
textInputControl.setEditable(editable);
|
||||
if(!editable && !textInputControl.getStyleClass().contains("readonly")) {
|
||||
textInputControl.getStyleClass().add("readonly");
|
||||
} else if(editable) {
|
||||
textInputControl.getStyleClass().remove("readonly");
|
||||
}
|
||||
}
|
||||
|
||||
private String getTypeLabel(Keystore keystore) {
|
||||
switch (keystore.getSource()) {
|
||||
case HW_USB:
|
||||
@@ -329,7 +338,7 @@ public class KeystoreController extends WalletFormController implements Initiali
|
||||
Wallet copy = getWalletForm().getWallet().copy();
|
||||
|
||||
if(copy.isEncrypted()) {
|
||||
WalletPasswordDialog dlg = new WalletPasswordDialog(WalletPasswordDialog.PasswordRequirement.LOAD);
|
||||
WalletPasswordDialog dlg = new WalletPasswordDialog(copy.getName(), WalletPasswordDialog.PasswordRequirement.LOAD);
|
||||
Optional<SecureString> password = dlg.showAndWait();
|
||||
if(password.isPresent()) {
|
||||
Storage.DecryptWalletService decryptWalletService = new Storage.DecryptWalletService(copy, password.get());
|
||||
|
||||
@@ -117,20 +117,7 @@ public class ReceiveController extends WalletFormController implements Initializ
|
||||
}
|
||||
|
||||
private void updateDerivationPath(NodeEntry nodeEntry) {
|
||||
KeyDerivation firstDerivation = getWalletForm().getWallet().getKeystores().get(0).getKeyDerivation();
|
||||
boolean singleDerivationPath = true;
|
||||
for(Keystore keystore : getWalletForm().getWallet().getKeystores()) {
|
||||
if(!keystore.getKeyDerivation().getDerivationPath().equals(firstDerivation.getDerivationPath())) {
|
||||
singleDerivationPath = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(singleDerivationPath) {
|
||||
derivationPath.setText(firstDerivation.extend(nodeEntry.getNode().getDerivation()).getDerivationPath());
|
||||
} else {
|
||||
derivationPath.setText(nodeEntry.getNode().getDerivationPath().replace("m", "multi"));
|
||||
}
|
||||
derivationPath.setText(getDerivationPath(nodeEntry.getNode()));
|
||||
}
|
||||
|
||||
private void updateLastUsed() {
|
||||
@@ -258,7 +245,7 @@ public class ReceiveController extends WalletFormController implements Initializ
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletSettingsChanged(WalletSettingsChangedEvent event) {
|
||||
public void walletAddressesChanged(WalletAddressesChangedEvent event) {
|
||||
displayAddress.setUserData(null);
|
||||
}
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ import java.util.stream.Collectors;
|
||||
public class SendController extends WalletFormController implements Initializable {
|
||||
private static final Logger log = LoggerFactory.getLogger(SendController.class);
|
||||
|
||||
public static final List<Integer> TARGET_BLOCKS_RANGE = List.of(1, 2, 3, 4, 5, 10, 25, 50, 100, 500);
|
||||
public static final List<Integer> TARGET_BLOCKS_RANGE = List.of(1, 2, 3, 4, 5, 10, 25, 50);
|
||||
public static final List<Long> FEE_RATES_RANGE = List.of(1L, 2L, 4L, 8L, 16L, 32L, 64L, 128L, 256L, 512L, 1024L);
|
||||
|
||||
public static final double FALLBACK_FEE_RATE = 20000d / 1000;
|
||||
@@ -644,7 +644,7 @@ public class SendController extends WalletFormController implements Initializabl
|
||||
Integer targetBlocks = getTargetBlocks(feeRateAmt);
|
||||
if(targetBlocksFeeRates.get(Integer.MAX_VALUE) != null) {
|
||||
Double minFeeRate = targetBlocksFeeRates.get(Integer.MAX_VALUE);
|
||||
if(minFeeRate > 1.0 && feeRateAmt <= minFeeRate) {
|
||||
if(minFeeRate > 1.0 && feeRateAmt < minFeeRate) {
|
||||
feeRatePriority.setText("Below Minimum");
|
||||
feeRatePriority.setTooltip(new Tooltip("Transactions at this fee rate are currently being purged from the default sized mempool"));
|
||||
feeRatePriorityGlyph.setStyle("-fx-text-fill: #a0a1a7cc");
|
||||
@@ -653,7 +653,7 @@ public class SendController extends WalletFormController implements Initializabl
|
||||
}
|
||||
|
||||
Double lowestBlocksRate = targetBlocksFeeRates.get(TARGET_BLOCKS_RANGE.get(TARGET_BLOCKS_RANGE.size() - 1));
|
||||
if(lowestBlocksRate > minFeeRate && feeRateAmt < (minFeeRate + ((lowestBlocksRate - minFeeRate) / 2))) {
|
||||
if(lowestBlocksRate >= minFeeRate && feeRateAmt < (minFeeRate + ((lowestBlocksRate - minFeeRate) / 2)) && !isPayjoinTx()) {
|
||||
feeRatePriority.setText("Try Then Replace");
|
||||
feeRatePriority.setTooltip(new Tooltip("Send a transaction, verify it appears in the destination wallet, then RBF to get it confirmed or sent to another address"));
|
||||
feeRatePriorityGlyph.setStyle("-fx-text-fill: #7eb7c9cc");
|
||||
@@ -691,6 +691,14 @@ public class SendController extends WalletFormController implements Initializabl
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isPayjoinTx() {
|
||||
if(walletTransactionProperty.get() != null) {
|
||||
return walletTransactionProperty.get().getPayments().stream().anyMatch(payment -> AppServices.getPayjoinURI(payment.getAddress()) != null);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private Node getSliderThumb() {
|
||||
return targetBlocks.lookup(".thumb");
|
||||
}
|
||||
@@ -921,6 +929,8 @@ public class SendController extends WalletFormController implements Initializabl
|
||||
mempoolSizeFeeRatesChart.update(getMempoolHistogram());
|
||||
if(targetBlocksField.isVisible()) {
|
||||
setFeeRate(event.getTargetBlockFeeRates().get(getTargetBlocks()));
|
||||
} else {
|
||||
setFeeRatePriority(getFeeRangeRate());
|
||||
}
|
||||
addFeeRangeTrackHighlight(0);
|
||||
}
|
||||
|
||||
@@ -65,6 +65,8 @@ public class SettingsController extends WalletFormController implements Initiali
|
||||
|
||||
private TabPane keystoreTabs;
|
||||
|
||||
@FXML Button export;
|
||||
|
||||
@FXML
|
||||
private Button apply;
|
||||
|
||||
@@ -221,6 +223,7 @@ public class SettingsController extends WalletFormController implements Initiali
|
||||
scriptType.getSelectionModel().select(walletForm.getWallet().getScriptType());
|
||||
}
|
||||
|
||||
export.setDisable(!walletForm.getWallet().isValid());
|
||||
revert.setDisable(true);
|
||||
apply.setDisable(true);
|
||||
}
|
||||
@@ -276,7 +279,7 @@ public class SettingsController extends WalletFormController implements Initiali
|
||||
|
||||
TextAreaDialog dialog = new TextAreaDialog(outputDescriptorString);
|
||||
dialog.setTitle("Edit wallet output descriptor");
|
||||
dialog.getDialogPane().setHeaderText("Wallet output descriptor:");
|
||||
dialog.getDialogPane().setHeaderText("The wallet configuration is specified in the output descriptor.\nChanges to the output descriptor will modify the wallet configuration.");
|
||||
Optional<String> text = dialog.showAndWait();
|
||||
if(text.isPresent() && !text.get().isEmpty() && !text.get().equals(outputDescriptorString)) {
|
||||
setDescriptorText(text.get());
|
||||
@@ -312,6 +315,20 @@ public class SettingsController extends WalletFormController implements Initiali
|
||||
}
|
||||
}
|
||||
|
||||
public void exportWallet(ActionEvent event) {
|
||||
if(walletForm.getWalletFile() == null) {
|
||||
throw new IllegalStateException("Cannot export unsaved wallet");
|
||||
}
|
||||
|
||||
Optional<Wallet> optWallet = AppServices.get().getOpenWallets().entrySet().stream().filter(entry -> walletForm.getWalletFile().equals(entry.getValue().getWalletFile())).map(Map.Entry::getKey).findFirst();
|
||||
if(optWallet.isPresent()) {
|
||||
WalletExportDialog dlg = new WalletExportDialog(optWallet.get());
|
||||
dlg.showAndWait();
|
||||
} else {
|
||||
AppServices.showErrorDialog("Cannot export wallet", "Wallet cannot be exported, please save it first.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String describeKeystore(Keystore keystore) {
|
||||
if(!keystore.isValid()) {
|
||||
@@ -341,11 +358,19 @@ public class SettingsController extends WalletFormController implements Initiali
|
||||
|
||||
revert.setDisable(false);
|
||||
apply.setDisable(!wallet.isValid());
|
||||
export.setDisable(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletSettingsChanged(WalletSettingsChangedEvent event) {
|
||||
if(event.getWalletFile().equals(walletForm.getWalletFile())) {
|
||||
export.setDisable(!event.getWallet().isValid());
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletAddressesChanged(WalletAddressesChangedEvent event) {
|
||||
updateBirthDate(event.getWalletFile(), event.getWallet());
|
||||
}
|
||||
|
||||
@@ -404,9 +429,7 @@ public class SettingsController extends WalletFormController implements Initiali
|
||||
try {
|
||||
walletForm.getStorage().setEncryptionPubKey(Storage.NO_PASSWORD_KEY);
|
||||
walletForm.saveAndRefresh();
|
||||
if(requirement == WalletPasswordDialog.PasswordRequirement.UPDATE_NEW || requirement == WalletPasswordDialog.PasswordRequirement.UPDATE_CHANGE) {
|
||||
EventManager.get().post(new RequestOpenWalletsEvent());
|
||||
}
|
||||
EventManager.get().post(new RequestOpenWalletsEvent());
|
||||
} catch (IOException e) {
|
||||
log.error("Error saving wallet", e);
|
||||
AppServices.showErrorDialog("Error saving wallet", e.getMessage());
|
||||
@@ -450,10 +473,7 @@ public class SettingsController extends WalletFormController implements Initiali
|
||||
walletForm.getWallet().encrypt(key);
|
||||
walletForm.getStorage().setEncryptionPubKey(encryptionPubKey);
|
||||
walletForm.saveAndRefresh();
|
||||
|
||||
if(requirement == WalletPasswordDialog.PasswordRequirement.UPDATE_NEW || requirement == WalletPasswordDialog.PasswordRequirement.UPDATE_EMPTY) {
|
||||
EventManager.get().post(new RequestOpenWalletsEvent());
|
||||
}
|
||||
EventManager.get().post(new RequestOpenWalletsEvent());
|
||||
} catch (Exception e) {
|
||||
log.error("Error saving wallet", e);
|
||||
AppServices.showErrorDialog("Error saving wallet", e.getMessage());
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package com.sparrowwallet.sparrow.wallet;
|
||||
|
||||
import com.sparrowwallet.drongo.policy.Policy;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.WalletAddressesChangedEvent;
|
||||
import com.sparrowwallet.sparrow.event.WalletSettingsChangedEvent;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
|
||||
@@ -52,6 +54,8 @@ public class SettingsWalletForm extends WalletForm {
|
||||
save();
|
||||
|
||||
if(refreshAll) {
|
||||
EventManager.get().post(new WalletAddressesChangedEvent(wallet, pastWallet, getWalletFile()));
|
||||
} else {
|
||||
EventManager.get().post(new WalletSettingsChangedEvent(wallet, pastWallet, getWalletFile()));
|
||||
}
|
||||
}
|
||||
@@ -91,6 +95,10 @@ public class SettingsWalletForm extends WalletForm {
|
||||
|
||||
//TODO: Determine if Miniscript has changed for custom policies
|
||||
|
||||
if(!Objects.equals(getNumSignaturesRequired(original.getDefaultPolicy()), getNumSignaturesRequired(changed.getDefaultPolicy()))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if(original.getKeystores().size() != changed.getKeystores().size()) {
|
||||
return true;
|
||||
}
|
||||
@@ -110,4 +118,8 @@ public class SettingsWalletForm extends WalletForm {
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private Integer getNumSignaturesRequired(Policy policy) {
|
||||
return policy == null ? null : policy.getNumSignaturesRequired();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.ReceiveActionEvent;
|
||||
import com.sparrowwallet.sparrow.event.SendActionEvent;
|
||||
import com.sparrowwallet.sparrow.event.WalletSettingsChangedEvent;
|
||||
import com.sparrowwallet.sparrow.event.WalletAddressesChangedEvent;
|
||||
import javafx.application.Platform;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
@@ -106,7 +106,7 @@ public class WalletController extends WalletFormController implements Initializa
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletSettingsChanged(WalletSettingsChangedEvent event) {
|
||||
public void walletAddressesChanged(WalletAddressesChangedEvent event) {
|
||||
if(event.getWalletFile().equals(walletForm.getWalletFile())) {
|
||||
configure(event.getWallet().isValid());
|
||||
}
|
||||
|
||||
@@ -105,14 +105,10 @@ public class WalletForm {
|
||||
});
|
||||
historyService.setOnFailed(workerStateEvent -> {
|
||||
log.error("Error retrieving wallet history", workerStateEvent.getSource().getException());
|
||||
EventManager.get().post(new WalletHistoryFinishedEvent(wallet, workerStateEvent.getSource().getException().getMessage()));
|
||||
EventManager.get().post(new WalletHistoryFailedEvent(wallet, workerStateEvent.getSource().getException()));
|
||||
});
|
||||
if(node == null && wallet.getTransactions().isEmpty()) {
|
||||
EventManager.get().post(new WalletHistoryStartedEvent(wallet));
|
||||
} else {
|
||||
EventManager.get().post(new WalletHistoryStatusEvent(wallet, true));
|
||||
}
|
||||
|
||||
EventManager.get().post(new WalletHistoryStartedEvent(wallet, node));
|
||||
historyService.start();
|
||||
}
|
||||
}
|
||||
@@ -269,17 +265,20 @@ public class WalletForm {
|
||||
public void walletSettingsChanged(WalletSettingsChangedEvent event) {
|
||||
if(event.getWalletFile().equals(storage.getWalletFile())) {
|
||||
wallet = event.getWallet();
|
||||
walletTransactionsEntry = null;
|
||||
walletUtxosEntry = null;
|
||||
accountEntries.clear();
|
||||
EventManager.get().post(new WalletNodesChangedEvent(wallet));
|
||||
|
||||
//It is necessary to save the past wallet because the actual copying of the past labels only occurs on a later ConnectionEvent with bwt
|
||||
if(Config.get().getServerType() == ServerType.BITCOIN_CORE) {
|
||||
savedPastWallet = event.getPastWallet();
|
||||
if(event instanceof WalletAddressesChangedEvent) {
|
||||
walletTransactionsEntry = null;
|
||||
walletUtxosEntry = null;
|
||||
accountEntries.clear();
|
||||
EventManager.get().post(new WalletNodesChangedEvent(wallet));
|
||||
|
||||
//It is necessary to save the past wallet because the actual copying of the past labels only occurs on a later ConnectionEvent with bwt
|
||||
if(Config.get().getServerType() == ServerType.BITCOIN_CORE) {
|
||||
savedPastWallet = event.getPastWallet();
|
||||
}
|
||||
|
||||
refreshHistory(AppServices.getCurrentBlockHeight(), event.getPastWallet());
|
||||
}
|
||||
|
||||
refreshHistory(AppServices.getCurrentBlockHeight(), event.getPastWallet());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package com.sparrowwallet.sparrow.wallet;
|
||||
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.WalletNode;
|
||||
import com.sparrowwallet.sparrow.BaseController;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.WalletTabData;
|
||||
@@ -30,4 +33,24 @@ public abstract class WalletFormController extends BaseController {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean isSingleDerivationPath() {
|
||||
KeyDerivation firstDerivation = getWalletForm().getWallet().getKeystores().get(0).getKeyDerivation();
|
||||
for(Keystore keystore : getWalletForm().getWallet().getKeystores()) {
|
||||
if(!keystore.getKeyDerivation().getDerivationPath().equals(firstDerivation.getDerivationPath())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected String getDerivationPath(WalletNode node) {
|
||||
if(isSingleDerivationPath()) {
|
||||
KeyDerivation firstDerivation = getWalletForm().getWallet().getKeystores().get(0).getKeyDerivation();
|
||||
return firstDerivation.extend(node.getDerivation()).getDerivationPath();
|
||||
}
|
||||
|
||||
return node.getDerivationPath().replace("m", "multi");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,4 +28,6 @@ open module com.sparrowwallet.sparrow {
|
||||
requires bwt.jni;
|
||||
requires jtorctl;
|
||||
requires javacsv;
|
||||
requires jul.to.slf4j;
|
||||
requires bridj;
|
||||
}
|
||||
@@ -20,6 +20,10 @@
|
||||
-fx-fill: #383a42;
|
||||
}
|
||||
|
||||
.tab-label .failure {
|
||||
-fx-text-fill: rgb(202, 18, 67);
|
||||
}
|
||||
|
||||
.status-bar .status-label {
|
||||
-fx-alignment: center-left;
|
||||
}
|
||||
@@ -30,6 +34,15 @@
|
||||
-fx-spacing: 10;
|
||||
}
|
||||
|
||||
.version-hyperlink {
|
||||
-fx-border-color: transparent;
|
||||
-fx-text-fill: #1e88cf;
|
||||
}
|
||||
|
||||
.version-hyperlink:visited {
|
||||
-fx-underline: false;
|
||||
}
|
||||
|
||||
.core-server.toggle-switch:selected .thumb-area {
|
||||
-fx-background-color: linear-gradient(to bottom, derive(-fx-text-box-border, -20%), derive(-fx-text-box-border, -30%)), linear-gradient(to bottom, derive(#50a14f, 30%), #50a14f);
|
||||
-fx-background-insets: 0, 1;
|
||||
|
||||
@@ -27,6 +27,10 @@
|
||||
</items>
|
||||
</Menu>
|
||||
<MenuItem fx:id="saveTransaction" mnemonicParsing="false" text="Save Transaction..." accelerator="Shortcut+S" onAction="#saveTransaction"/>
|
||||
<Menu fx:id="savePSBT" mnemonicParsing="false" text="Save PSBT">
|
||||
<MenuItem text="As Binary..." onAction="#savePSBTBinary" accelerator="Shortcut+S"/>
|
||||
<MenuItem text="As Base64..." onAction="#savePSBTText"/>
|
||||
</Menu>
|
||||
<SeparatorMenuItem />
|
||||
<MenuItem mnemonicParsing="false" text="Import Wallet..." onAction="#importWallet"/>
|
||||
<MenuItem fx:id="exportWallet" mnemonicParsing="false" text="Export Wallet..." onAction="#exportWallet"/>
|
||||
@@ -43,7 +47,7 @@
|
||||
<fx:define>
|
||||
<ToggleGroup fx:id="theme"/>
|
||||
</fx:define>
|
||||
<Menu mnemonicParsing="false" text="View">
|
||||
<Menu fx:id="viewMenu" mnemonicParsing="false" text="View">
|
||||
<items>
|
||||
<Menu mnemonicParsing="false" text="Bitcoin Unit">
|
||||
<items>
|
||||
@@ -81,13 +85,16 @@
|
||||
<SeparatorMenuItem />
|
||||
<CheckMenuItem fx:id="openWalletsInNewWindows" mnemonicParsing="false" text="Open Wallets in New Windows" onAction="#openWalletsInNewWindows"/>
|
||||
<CheckMenuItem fx:id="hideEmptyUsedAddresses" mnemonicParsing="false" text="Hide Empty Used Addresses" onAction="#hideEmptyUsedAddresses"/>
|
||||
<CheckMenuItem fx:id="useHdCameraResolution" mnemonicParsing="false" text="Use HD Camera Resolution" onAction="#useHdCameraResolution"/>
|
||||
<CheckMenuItem fx:id="showTxHex" mnemonicParsing="false" text="Show Transaction Hex" onAction="#showTxHex"/>
|
||||
<SeparatorMenuItem />
|
||||
<MenuItem fx:id="minimizeToTray" mnemonicParsing="false" text="Minimize to System Tray" accelerator="Shortcut+Y" onAction="#minimizeToTray"/>
|
||||
<MenuItem fx:id="refreshWallet" mnemonicParsing="false" text="Refresh Wallet" accelerator="Shortcut+R" onAction="#refreshWallet"/>
|
||||
</items>
|
||||
</Menu>
|
||||
<Menu fx:id="toolsMenu" mnemonicParsing="false" text="Tools">
|
||||
<MenuItem mnemonicParsing="false" text="Sign/Verify Message" accelerator="Shortcut+M" onAction="#signVerifyMessage"/>
|
||||
<MenuItem styleClass="osxHide,windowsHide" mnemonicParsing="false" text="Install Udev Rules" onAction="#installUdevRules"/>
|
||||
</Menu>
|
||||
<Menu fx:id="helpMenu" mnemonicParsing="false" text="Help">
|
||||
<MenuItem mnemonicParsing="false" text="Show Introduction" onAction="#showIntroduction"/>
|
||||
|
||||
@@ -82,6 +82,10 @@
|
||||
-fx-bar-fill: rgba(135, 138, 149, 0.5);
|
||||
}
|
||||
|
||||
.tab-label .failure {
|
||||
-fx-fill: #e06c75;
|
||||
}
|
||||
|
||||
.root .titled-description-pane .status-error .text, .root .titled-description-pane .description-error .text {
|
||||
-fx-fill: #e06c75;
|
||||
}
|
||||
@@ -148,6 +152,14 @@
|
||||
color-grey: #3e4451;
|
||||
}
|
||||
|
||||
.root .readonly.text-input {
|
||||
-fx-text-fill: lightgray;
|
||||
}
|
||||
|
||||
.root .descriptor-text {
|
||||
-fx-fill: lightgray;
|
||||
}
|
||||
|
||||
.root .success {
|
||||
-fx-text-fill: #98c379;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
.descriptor-text { -fx-fill: -fx-text-inner-color }
|
||||
.descriptor-error { -fx-fill: #ca1243 }
|
||||
.descriptor-text { -fx-fill: derive(-fx-text-inner-color, 40%) }
|
||||
.descriptor-error { -fx-fill: rgba(202, 18, 67, 0.8) }
|
||||
|
||||
|
||||
@@ -128,6 +128,10 @@
|
||||
-fx-background-color: #116a8d;
|
||||
}
|
||||
|
||||
.readonly.text-input {
|
||||
-fx-text-fill: derive(-fx-text-inner-color, 40%);
|
||||
}
|
||||
|
||||
.help-label {
|
||||
-fx-padding: 0 0 0 10;
|
||||
}
|
||||
@@ -167,4 +171,16 @@
|
||||
|
||||
.alert .content.label {
|
||||
-fx-padding: 20px 20px 20px 20px;
|
||||
}
|
||||
|
||||
.icon-button {
|
||||
-fx-border-style: none;
|
||||
-fx-border-width: 0;
|
||||
-fx-border-insets: 0;
|
||||
-fx-background-color: transparent;
|
||||
-fx-opacity: 0.7;
|
||||
}
|
||||
|
||||
.icon-button:hover {
|
||||
-fx-opacity: 1.0;
|
||||
}
|
||||
@@ -72,6 +72,10 @@
|
||||
</Field>
|
||||
</Fieldset>
|
||||
<Fieldset inputGrow="SOMETIMES" text="Wallet" styleClass="wideLabelFieldSet">
|
||||
<Field text="Load recent wallets:">
|
||||
<UnlabeledToggleSwitch fx:id="loadRecentWallets" />
|
||||
<HelpLabel helpText="Keep a record of open wallets, and reopen them on startup."/>
|
||||
</Field>
|
||||
<Field text="Validate derivations:">
|
||||
<UnlabeledToggleSwitch fx:id="validateDerivationPaths" />
|
||||
<HelpLabel helpText="Disallow keystores to have derivation paths that match the defaults for other script types."/>
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import com.sparrowwallet.sparrow.control.AddressTreeTable?>
|
||||
<?import org.controlsfx.glyphfont.Glyph?>
|
||||
|
||||
<GridPane hgap="10.0" vgap="10.0" stylesheets="@addresses.css, @wallet.css, @../general.css" styleClass="wallet-pane" xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.sparrowwallet.sparrow.wallet.AddressesController">
|
||||
<padding>
|
||||
<Insets left="25.0" right="25.0" top="15.0" bottom="25.0" />
|
||||
@@ -21,7 +23,17 @@
|
||||
</rowConstraints>
|
||||
<BorderPane GridPane.columnIndex="0" GridPane.rowIndex="0">
|
||||
<top>
|
||||
<Label styleClass="addresses-treetable-label" text="Receiving Addresses"/>
|
||||
<HBox alignment="CENTER_LEFT">
|
||||
<Label styleClass="addresses-treetable-label" text="Receive Addresses"/>
|
||||
<Button onAction="#exportReceiveAddresses" styleClass="icon-button">
|
||||
<graphic>
|
||||
<Glyph fontFamily="Font Awesome 5 Free Solid" icon="ARROW_CIRCLE_DOWN" fontSize="12" />
|
||||
</graphic>
|
||||
<tooltip>
|
||||
<Tooltip text="Export receive addresses as CSV" />
|
||||
</tooltip>
|
||||
</Button>
|
||||
</HBox>
|
||||
</top>
|
||||
<center>
|
||||
<AddressTreeTable fx:id="receiveTable" />
|
||||
@@ -29,7 +41,17 @@
|
||||
</BorderPane>
|
||||
<BorderPane GridPane.columnIndex="0" GridPane.rowIndex="1">
|
||||
<top>
|
||||
<Label styleClass="addresses-treetable-label" text="Change Addresses"/>
|
||||
<HBox alignment="CENTER_LEFT">
|
||||
<Label styleClass="addresses-treetable-label" text="Change Addresses"/>
|
||||
<Button onAction="#exportChangeAddresses" styleClass="icon-button">
|
||||
<graphic>
|
||||
<Glyph fontFamily="Font Awesome 5 Free Solid" icon="ARROW_CIRCLE_DOWN" fontSize="12" />
|
||||
</graphic>
|
||||
<tooltip>
|
||||
<Tooltip text="Export change addresses as CSV" />
|
||||
</tooltip>
|
||||
</Button>
|
||||
</HBox>
|
||||
</top>
|
||||
<center>
|
||||
<AddressTreeTable fx:id="changeTable" />
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
<TextField fx:id="label" maxWidth="160"/>
|
||||
</Field>
|
||||
<Field text="Master fingerprint:">
|
||||
<TextField fx:id="fingerprint" maxWidth="80" promptText="00000000"/> <HelpLabel helpText="A master fingerprint is the first 4 bytes of the master public key hash.\nIt is safe to use any valid value (00000000) for Watch Only Wallets." />
|
||||
<TextField fx:id="fingerprint" maxWidth="80" promptText="00000000"/> <HelpLabel helpText="The master fingerprint uniquely identifies this keystore using the first 4 bytes of the master public key hash.\nIt is safe to use any valid value (00000000) for Watch Only Wallets." />
|
||||
</Field>
|
||||
<Field text="Derivation:">
|
||||
<TextField fx:id="derivation" maxWidth="200"/> <HelpLabel helpText="The derivation path to the xpub from the master private key.\nFor safety, derivations that match defaults for other script types are not valid." />
|
||||
|
||||
@@ -104,3 +104,10 @@
|
||||
#transactionDiagram .utxo-label:hover .button .label .text {
|
||||
-fx-fill: -fx-text-base-color;
|
||||
}
|
||||
|
||||
#targetBlocks .track {
|
||||
-fx-background-color: -fx-shadow-highlight-color,
|
||||
linear-gradient(to bottom, derive(-fx-text-box-border, -10%), -fx-text-box-border),
|
||||
linear-gradient(to bottom, derive(-fx-control-inner-background, -9%), derive(-fx-control-inner-background, 0%), derive(-fx-control-inner-background, -5%), derive(-fx-control-inner-background, -12%)),
|
||||
linear-gradient(to right, #c8416466 0%, #a0a1a766 50%, #a0a1a700 100%);
|
||||
}
|
||||
@@ -83,10 +83,10 @@
|
||||
</HBox>
|
||||
<Form GridPane.columnIndex="0" GridPane.rowIndex="2">
|
||||
<Fieldset inputGrow="SOMETIMES">
|
||||
<Field fx:id="targetBlocksField" text="Block target">
|
||||
<Field fx:id="targetBlocksField" text="Blocks:">
|
||||
<Slider fx:id="targetBlocks" snapToTicks="true" showTickLabels="true" showTickMarks="true" />
|
||||
</Field>
|
||||
<Field fx:id="feeRangeField" text="Rate Range:">
|
||||
<Field fx:id="feeRangeField" text="Range:">
|
||||
<Slider fx:id="feeRange" snapToTicks="false" showTickLabels="true" showTickMarks="true" />
|
||||
</Field>
|
||||
<Field fx:id="feeRateField" text="Rate:">
|
||||
|
||||
@@ -112,6 +112,9 @@
|
||||
<padding>
|
||||
<Insets left="25.0" right="25.0" bottom="25.0" />
|
||||
</padding>
|
||||
<HBox AnchorPane.leftAnchor="0" spacing="20">
|
||||
<Button fx:id="export" text="Export..." onAction="#exportWallet" />
|
||||
</HBox>
|
||||
<HBox AnchorPane.rightAnchor="10" spacing="20">
|
||||
<Button text="Advanced..." onAction="#showAdvanced" />
|
||||
<Button fx:id="revert" text="Revert" cancelButton="true" />
|
||||
|
||||
@@ -23,15 +23,3 @@
|
||||
.chart-line-symbol.selected {
|
||||
-fx-background-color: rgba(30, 136, 207, 0.6);
|
||||
}
|
||||
|
||||
#exportCsv {
|
||||
-fx-border-style: none;
|
||||
-fx-border-width: 0;
|
||||
-fx-border-insets: 0;
|
||||
-fx-background-color: transparent;
|
||||
-fx-opacity: 0.7;
|
||||
}
|
||||
|
||||
#exportCsv:hover {
|
||||
-fx-opacity: 1.0;
|
||||
}
|
||||
@@ -41,7 +41,7 @@
|
||||
</Field>
|
||||
<Field text="Transactions:">
|
||||
<CopyableLabel fx:id="transactionCount" />
|
||||
<Button fx:id="exportCsv" maxHeight="25" onAction="#exportCSV" translateY="-1">
|
||||
<Button fx:id="exportCsv" maxHeight="25" onAction="#exportCSV" translateY="-1" styleClass="icon-button">
|
||||
<graphic>
|
||||
<Glyph fontFamily="Font Awesome 5 Free Solid" icon="ARROW_CIRCLE_DOWN" fontSize="12" />
|
||||
</graphic>
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 550 B |
Binary file not shown.
|
After Width: | Height: | Size: 975 B |
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 544 B |
Binary file not shown.
|
After Width: | Height: | Size: 913 B |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user