Compare commits

...
72 Commits
Author SHA1 Message Date
Craig Raw f8fa929166 upgrade to hummingbird 1.5.4 which handles legacy encoding in uppercase 2021-03-24 14:37:06 +02:00
Craig Raw 6aaf532051 v1.3.1 2021-03-24 13:33:41 +02:00
Craig Raw fb59bdfff4 update fee rate priority as fee prediction changes 2021-03-24 13:26:23 +02:00
Craig Raw c8aa1b0a19 increase tab icon size by 1pt 2021-03-24 13:17:49 +02:00
Craig Raw 11a201b3f5 fix webcam opening/closing issues, display progress bar for qr animation scanning progress 2021-03-24 12:59:33 +02:00
Craig Raw b74741bccb add sparrow wallet file export, dont unnecessarily ask for password when exporting 2021-03-22 14:39:58 +02:00
Craig Raw b8739ace4f remove order resources, wait for jdk16 for deterministic jlink builds across servers 2021-03-22 12:02:58 +02:00
Craig Raw cd674ee9be order resources in merged module 2021-03-22 11:31:30 +02:00
Craig Raw 8b3ed8171b fix archive issues 2021-03-20 14:48:24 +02:00
Craig Raw 28ee16279c deterministic builds on linux and windows 2021-03-20 14:13:10 +02:00
Craig Raw ff43db0842 retain messages from rpc errors when loading wallets with bwt 2021-03-20 11:49:17 +02:00
Craig Raw 8ddac9acba reduce target blocks range, predicting too far ahead leads to incorrect estimates 2021-03-19 18:34:53 +02:00
Craig Raw 0c2554da72 add export button to wallet settings 2021-03-19 14:34:29 +02:00
Craig Raw 1274d56fce ensure fees decrease as target blocks increase, show priority on target blocks slider track 2021-03-19 13:35:59 +02:00
Craig Raw 8d9e253f93 update wallet in walletform on label and keystore source changes, detect threshold change 2021-03-19 12:27:08 +02:00
Craig Raw 9166160008 always show wallet name on password dialog 2021-03-18 11:59:59 +02:00
Craig Raw 35eb8fcdf9 add suffix to rbf transaction labels to indicate transaction has been replaced 2021-03-18 11:34:50 +02:00
Craig Raw d34c8b8bde add menu command to install udev rules via hwi on linux 2021-03-17 13:53:38 +02:00
Craig Raw 80a079cbee upgrade to hwi 2.0.0-final 2021-03-17 12:58:22 +02:00
Craig Raw 5d181d1abb upgrade to bwt 0.2.3 2021-03-17 12:39:55 +02:00
Craig Raw 6fbd332b4d add preference option to load recent wallets 2021-03-17 11:59:58 +02:00
Craig Raw 211e5952aa various minor ui improvements 2021-03-17 11:39:46 +02:00
Craig Raw 4078c61d6b add tab label icons, show loading and failure status there 2021-03-16 12:07:55 +02:00
Craig Raw 8b7d1e6888 try proxy setting if internal tor cant start 2021-03-15 14:07:48 +02:00
Craig Raw 7c6daf2e11 scan and import connected devices from import wallet dialog 2021-03-15 13:03:09 +02:00
Craig Raw e1f405d886 default to hd capture on new osx installs 2021-03-15 11:25:30 +02:00
Craig Raw 1d73956853 fixes to minimum and pay then replace fee rates 2021-03-15 10:34:18 +02:00
Craig Raw 205f78151a revert to original hwi file permissions 2021-03-15 09:53:20 +02:00
Craig Raw 5853eb01f5 unpack hwi to all executable file permissions 2021-03-14 12:59:01 +02:00
Craig Raw 1a5e71f852 remove debug on logging config 2021-03-12 18:42:59 +02:00
Craig Raw 7053e7b225 fix logback conflict 2021-03-12 18:32:16 +02:00
Craig Raw 5bee26a8cc followup to jul logging bridge 2021-03-12 18:29:04 +02:00
Craig Raw b9caff352b actually quieten logs by redirecting jul logging to slf4j 2021-03-12 18:14:07 +02:00
Craig Raw 73089ce99c actually quieten logs by redirecting jul logging to slf4j 2021-03-12 18:12:42 +02:00
Craig Raw 34080ae8b3 try to quieten logs with spurious css warnings 2021-03-12 17:29:13 +02:00
Craig Raw 307c7d53f8 add error logging when enumerate fails 2021-03-12 17:19:54 +02:00
Craig Raw a6d2a6563b Merge branch 'master' of github.com:sparrowwallet/sparrow 2021-03-12 17:15:18 +02:00
Craig Raw 577e570c99 update to hwi-2.0.0-rc.3 to fix trezor usage with passphrase 2021-03-12 17:14:23 +02:00
Craig Raw 06411d4e2f update to hwi-2.0.0-rc.3 to fix trezor usage with passphrase 2021-03-12 11:36:11 +02:00
Craig Raw 3e287bfb2d dont start version check service if not on mainnet 2021-03-12 10:46:59 +02:00
Craig Raw 9872fc9221 v1.3.0 2021-03-11 14:48:41 +02:00
Craig Raw 84d2db427c dont allow scheduled enumerate to run while interactive commands are running 2021-03-11 14:37:19 +02:00
Craig Raw cfbf9187e6 update linux and windows hwi binaries to post rc.2 HEAD 2021-03-11 14:01:00 +02:00
Craig Raw 5478ac05b8 show error when importing tpubs from usb on mainnet etc 2021-03-11 11:39:19 +02:00
Craig Raw 836bd6bc40 display error on sendpin correctly 2021-03-11 11:13:30 +02:00
Craig Raw 1bab07fc5f dont allow duplicate loading wallet references 2021-03-11 10:37:30 +02:00
Craig Raw 4b3de044c3 remove unused --addr_type parameter when using hwi displayaddress with --desc 2021-03-11 10:36:44 +02:00
Craig Raw f2c2fc6071 fix exception on debug logging when sending from two or more utxos on the same derivation 2021-03-11 10:33:51 +02:00
Craig Raw 4dd52eb9dd handle csv export with mempool txes 2021-03-11 10:08:41 +02:00
Craig Raw e45fdc72a0 reduce log level on electrs notification errors to quieten logs 2021-03-10 14:17:40 +02:00
Craig Raw d67dd515c4 stop progress bar when loading wallet is closed 2021-03-10 13:49:21 +02:00
Craig Raw ff962aa5f0 add overpaid indicator to fee priority 2021-03-10 13:16:08 +02:00
Craig Raw c0e44b2c55 show online docs and log file from help menu 2021-03-10 12:51:17 +02:00
Craig Raw f0c239d625 upgrade to hwi-2.0.0-rc.2, display addresses on usb devices for multisig wallets 2021-03-10 11:31:19 +02:00
Craig Raw 58a31f435e show tooltips on fee priority label, minor ui improvements 2021-03-10 11:30:06 +02:00
Craig Raw c49439d541 add menu item to show introduction again 2021-03-08 17:00:32 +02:00
Craig Raw 0070cbfb1e use and display fractional tx virtual size, indicate sizes on unfinalized txes may change 2021-03-08 16:22:22 +02:00
Craig Raw 7ed75fc83d add fee rate priority indicator 2021-03-08 15:25:03 +02:00
Craig Raw 13d701b0a7 indicate when initially loading a wallets transactions in status bar 2021-03-08 10:45:29 +02:00
Craig Raw 07ed9c857b improve placeholder text for private electrum url hostname 2021-03-08 09:36:21 +02:00
Craig Raw 32ab53ba03 append tor debug output to test results 2021-03-08 09:35:21 +02:00
Craig Raw 68344e981c set fiat value on scanning payment uri 2021-03-08 09:21:04 +02:00
Craig Raw 10c1574a4a provide framework for application logging 2021-03-08 09:18:48 +02:00
Craig Raw 3a853e2ab1 fix text width issue with overlay on linux 2021-03-05 13:18:24 +02:00
Craig Raw 0f639dc2c2 clear transport on a failed test connection, try ssl or vice-versa on electrum connection failure 2021-03-05 09:16:00 +02:00
Craig Raw a801773545 max button retains state to adjust amount as fee changes 2021-03-04 13:00:39 +02:00
Craig Raw ffbf4bb0db default to mempool size fee rates selection if not configured 2021-03-03 15:36:33 +02:00
Craig Raw 11e46155a1 indicate welcome dialog connections are demonstration only 2021-03-03 15:19:56 +02:00
Craig Raw f369b3ad00 add command line option to set log level 2021-03-01 16:32:11 +02:00
Craig Raw 6068666415 prompt to retry on invalid password 2021-03-01 15:48:47 +02:00
Craig Raw 6a58e8a799 dont lose labels when rescanning, even if app is restarted 2021-03-01 15:27:54 +02:00
Craig Raw 3bd2f69157 add change password button, delete backups option on changing password 2021-02-26 16:03:18 +02:00
98 changed files with 1629 additions and 367 deletions
+9 -1
View File
@@ -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/
+3
View File
@@ -43,6 +43,9 @@ Usage: sparrow [options]
Path to Sparrow home folder
--help, -h
Show usage
--level, -l
Set log level
Possible Values: [ERROR, WARN, INFO, DEBUG, TRACE]
--network, -n
Network to use
Possible Values: [mainnet, testnet, regtest, signet]
+28 -3
View File
@@ -5,7 +5,7 @@ plugins {
id 'org.beryx.jlink' version '2.22.0'
}
def sparrowVersion = '1.2.0'
def sparrowVersion = '1.3.1'
def os = org.gradle.internal.os.OperatingSystem.current()
def osName = os.getFamilyName()
if(os.macOsX) {
@@ -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.4')
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: 08acfe5ba1...e974c3f422
+1 -1
View File
@@ -21,7 +21,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.2.0</string>
<string>1.3.1</string>
<key>CFBundleSignature</key>
<string>????</string>
<!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories -->
@@ -21,9 +21,11 @@ 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;
import com.sparrowwallet.sparrow.preferences.PreferenceGroup;
import com.sparrowwallet.sparrow.preferences.PreferencesDialog;
import com.sparrowwallet.sparrow.transaction.TransactionController;
import com.sparrowwallet.sparrow.transaction.TransactionData;
@@ -52,6 +54,8 @@ import javafx.stage.*;
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;
@@ -69,6 +73,9 @@ 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...";
@FXML
private MenuItem saveTransaction;
@@ -79,6 +86,9 @@ public class AppController implements Initializable {
@FXML
private Menu fileMenu;
@FXML
private Menu toolsMenu;
@FXML
private Menu helpMenu;
@@ -119,13 +129,15 @@ public class AppController implements Initializable {
private Timeline statusTimeline;
private final Set<Wallet> loadingWallets = new LinkedHashSet<>();
@Override
public void initialize(URL location, ResourceBundle resources) {
EventManager.get().register(this);
}
void initializeView() {
setOsxApplicationMenu();
setPlatformApplicationMenu();
rootStack.setOnDragOver(event -> {
if(event.getGestureSource() != rootStack && event.getDragboard().hasFiles()) {
@@ -160,7 +172,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));
@@ -243,8 +259,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);
@@ -256,7 +273,32 @@ 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"));
}
}
public void showIntroduction(ActionEvent event) {
WelcomeDialog welcomeDialog = new WelcomeDialog();
Optional<Mode> optionalMode = welcomeDialog.showAndWait();
if(optionalMode.isPresent() && optionalMode.get().equals(Mode.ONLINE)) {
PreferencesDialog preferencesDialog = new PreferencesDialog(PreferenceGroup.SERVER);
preferencesDialog.showAndWait();
}
}
public void showDocumentation(ActionEvent event) throws IOException {
AppServices.get().getApplication().getHostServices().showDocument("https://sparrowwallet.com/docs");
}
public void showLogFile(ActionEvent event) throws IOException {
File logFile = new File(Storage.getSparrowHome(), "sparrow.log");
if(logFile.exists()) {
AppServices.get().getApplication().getHostServices().showDocument(logFile.toPath().toUri().toString());
} else {
AppServices.showErrorDialog("Log file unavailable", "Cannot find log file at " + logFile.getCanonicalPath());
}
}
@@ -288,6 +330,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();
@@ -569,7 +629,7 @@ public class AppController implements Initializable {
File walletFile = Storage.getWalletFile(nameAndBirthDate.getName());
Storage storage = new Storage(walletFile);
Wallet wallet = new Wallet(nameAndBirthDate.getName(), PolicyType.SINGLE, ScriptType.P2WPKH, nameAndBirthDate.getBirthDate());
addWalletTabOrWindow(storage, wallet, false);
addWalletTabOrWindow(storage, wallet, null, false);
}
}
@@ -594,13 +654,13 @@ public class AppController implements Initializable {
Storage storage = new Storage(file);
FileType fileType = IOUtils.getFileType(file);
if(FileType.JSON.equals(fileType)) {
Wallet wallet = storage.loadWallet();
checkWalletNetwork(wallet);
restorePublicKeysFromSeed(wallet, null);
if(!wallet.isValid()) {
Storage.WalletBackupAndKey walletBackupAndKey = storage.loadWallet();
checkWalletNetwork(walletBackupAndKey.wallet);
restorePublicKeysFromSeed(walletBackupAndKey.wallet, null);
if(!walletBackupAndKey.wallet.isValid()) {
throw new IllegalStateException("Wallet file is not valid.");
}
addWalletTabOrWindow(storage, wallet, forceSameWindow);
addWalletTabOrWindow(storage, walletBackupAndKey.wallet, walletBackupAndKey.backupWallet, forceSameWindow);
} else if(FileType.BINARY.equals(fileType)) {
WalletPasswordDialog dlg = new WalletPasswordDialog(file.getName(), WalletPasswordDialog.PasswordRequirement.LOAD);
Optional<SecureString> optionalPassword = dlg.showAndWait();
@@ -612,22 +672,25 @@ public class AppController implements Initializable {
Storage.LoadWalletService loadWalletService = new Storage.LoadWalletService(storage, password);
loadWalletService.setOnSucceeded(workerStateEvent -> {
EventManager.get().post(new StorageEvent(storage.getWalletFile(), TimedEvent.Action.END, "Done"));
Storage.WalletAndKey walletAndKey = loadWalletService.getValue();
Storage.WalletBackupAndKey walletBackupAndKey = loadWalletService.getValue();
try {
checkWalletNetwork(walletAndKey.wallet);
restorePublicKeysFromSeed(walletAndKey.wallet, walletAndKey.key);
addWalletTabOrWindow(storage, walletAndKey.wallet, forceSameWindow);
checkWalletNetwork(walletBackupAndKey.wallet);
restorePublicKeysFromSeed(walletBackupAndKey.wallet, walletBackupAndKey.key);
addWalletTabOrWindow(storage, walletBackupAndKey.wallet, walletBackupAndKey.backupWallet, forceSameWindow);
} catch(Exception e) {
showErrorDialog("Error Opening Wallet", e.getMessage());
} finally {
walletAndKey.key.clear();
walletBackupAndKey.key.clear();
}
});
loadWalletService.setOnFailed(workerStateEvent -> {
EventManager.get().post(new StorageEvent(storage.getWalletFile(), TimedEvent.Action.END, "Failed"));
Throwable exception = loadWalletService.getException();
if(exception instanceof InvalidPasswordException) {
showErrorDialog("Invalid Password", "The wallet password was invalid.");
Optional<ButtonType> optResponse = showErrorDialog("Invalid Password", "The wallet password was invalid. Try again?", ButtonType.CANCEL, ButtonType.OK);
if(optResponse.isPresent() && optResponse.get().equals(ButtonType.OK)) {
Platform.runLater(() -> openWalletFile(file, forceSameWindow));
}
} else {
if(!attemptImportWallet(file, password)) {
log.error("Error Opening Wallet", exception);
@@ -774,12 +837,12 @@ 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) {
storage.setEncryptionPubKey(Storage.NO_PASSWORD_KEY);
addWalletTabOrWindow(storage, wallet, false);
addWalletTabOrWindow(storage, wallet, null, false);
} else {
Storage.KeyDerivationService keyDerivationService = new Storage.KeyDerivationService(storage, password.get());
keyDerivationService.setOnSucceeded(workerStateEvent -> {
@@ -792,7 +855,7 @@ public class AppController implements Initializable {
key = new Key(encryptionFullKey.getPrivKeyBytes(), storage.getKeyDeriver().getSalt(), EncryptionType.Deriver.ARGON2);
wallet.encrypt(key);
storage.setEncryptionPubKey(encryptionPubKey);
addWalletTabOrWindow(storage, wallet, false);
addWalletTabOrWindow(storage, wallet, null, false);
} finally {
encryptionFullKey.clear();
if(key != null) {
@@ -856,12 +919,13 @@ public class AppController implements Initializable {
WalletTabData walletTabData = (WalletTabData) tabData;
Wallet wallet = walletTabData.getWallet();
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()));
}
}
public void addWalletTabOrWindow(Storage storage, Wallet wallet, boolean forceSameWindow) {
public void addWalletTabOrWindow(Storage storage, Wallet wallet, Wallet backupWallet, boolean forceSameWindow) {
Window existingWalletWindow = AppServices.get().getWindowForWallet(storage);
if(existingWalletWindow instanceof Stage) {
Stage existingWalletStage = (Stage)existingWalletWindow;
@@ -876,13 +940,13 @@ public class AppController implements Initializable {
AppController appController = AppServices.newAppWindow(stage);
stage.toFront();
stage.setX(AppServices.get().getWalletWindowMaxX() + 30);
appController.addWalletTab(storage, wallet);
appController.addWalletTab(storage, wallet, backupWallet);
} else {
addWalletTab(storage, wallet);
addWalletTab(storage, wallet, backupWallet);
}
}
public void addWalletTab(Storage storage, Wallet wallet) {
public void addWalletTab(Storage storage, Wallet wallet, Wallet backupWallet) {
try {
String name = storage.getWalletFile().getName();
if(name.endsWith(".json")) {
@@ -891,7 +955,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"));
@@ -901,7 +972,7 @@ public class AppController implements Initializable {
EventManager.get().post(new WalletOpeningEvent(storage, wallet));
//Note that only one WalletForm is created per wallet tab, and registered to listen for events. All wallet controllers (except SettingsController) share this instance.
WalletForm walletForm = new WalletForm(storage, wallet);
WalletForm walletForm = new WalletForm(storage, wallet, backupWallet);
EventManager.get().register(walletForm);
controller.setWalletForm(walletForm);
@@ -1028,7 +1099,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"));
@@ -1109,6 +1187,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,7 +1296,7 @@ public class AppController implements Initializable {
}
@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) {
@@ -1253,6 +1382,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");
});
@@ -1343,6 +1473,62 @@ 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_MESSAGE)) {
statusBar.setText("");
}
if(statusTimeline == null || statusTimeline.getStatus() != Animation.Status.RUNNING) {
statusBar.setProgress(0);
}
}
}
@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())) {
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_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);
@@ -2,6 +2,7 @@ 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;
@@ -139,7 +140,7 @@ public class AppServices {
restartService(ratesService);
}
if(config.isCheckNewVersions()) {
if(config.isCheckNewVersions() && Network.get() == Network.MAINNET) {
restartService(versionCheckService);
}
}
@@ -333,7 +334,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();
@@ -452,7 +453,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 +521,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 +540,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 +585,7 @@ public class AppServices {
}
@Subscribe
public void walletSettingsChanged(WalletSettingsChangedEvent event) {
public void walletAddressesChanged(WalletAddressesChangedEvent event) {
restartBwt(event.getWallet());
}
@@ -2,6 +2,7 @@ package com.sparrowwallet.sparrow;
import com.beust.jcommander.Parameter;
import com.sparrowwallet.drongo.Network;
import org.slf4j.event.Level;
public class Args {
@Parameter(names = { "--dir", "-d" }, description = "Path to Sparrow home folder")
@@ -10,6 +11,9 @@ public class Args {
@Parameter(names = { "--network", "-n" }, description = "Network to use")
public Network network;
@Parameter(names = { "--level", "-l" }, description = "Set log level")
public Level level;
@Parameter(names = { "--help", "-h" }, description = "Show usage", help = true)
public boolean help;
}
@@ -1,6 +1,7 @@
package com.sparrowwallet.sparrow;
import com.beust.jcommander.JCommander;
import com.sparrowwallet.drongo.Drongo;
import com.sparrowwallet.drongo.Network;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
@@ -18,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.*;
@@ -27,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.2.0";
public static final String APP_VERSION = "1.3.1";
public static final String APP_HOME_PROPERTY = "sparrow.home";
public static final String NETWORK_ENV_PROPERTY = "SPARROW_NETWORK";
@@ -77,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);
@@ -119,6 +126,10 @@ public class MainApp extends Application {
System.exit(0);
}
if(args.level != null) {
Drongo.setRootLogLevel(args.level);
}
if(args.dir != null) {
System.setProperty(APP_HOME_PROPERTY, args.dir);
getLogger().info("Using configured Sparrow home folder of " + args.dir);
@@ -151,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);
}
@@ -0,0 +1,12 @@
package com.sparrowwallet.sparrow;
import com.sparrowwallet.drongo.LogHandler;
import com.sparrowwallet.sparrow.event.TorStatusEvent;
import org.slf4j.event.Level;
public class TorLogHandler implements LogHandler {
@Override
public void handleLog(String threadName, Level level, String message, String loggerName, long timestamp, StackTraceElement[] callerData) {
EventManager.get().post(new TorStatusEvent(message));
}
}
@@ -49,7 +49,7 @@ public class WelcomeController {
welcomeBox.getStyleClass().add("offline");
serverStatus.setText("Offline");
serverToggle.selectedProperty().addListener((observable, oldValue, newValue) -> {
serverStatus.setText(newValue ? "Connected" : "Offline");
serverStatus.setText(newValue ? "Connected (demonstration only)" : "Offline");
});
}
@@ -62,7 +62,7 @@ public class WelcomeController {
PauseTransition wait = new PauseTransition(Duration.millis(200));
wait.setOnFinished((e) -> {
serverToggle.setSelected(true);
serverStatus.setText("Connected to a Public Server");
serverStatus.setText("Connected to a Public Server (demonstration only)");
});
wait.play();
return true;
@@ -74,7 +74,7 @@ public class WelcomeController {
welcomeBox.getStyleClass().clear();
welcomeBox.getStyleClass().add("bitcoin-core");
serverToggle.setSelected(true);
serverStatus.setText("Connected to Bitcoin Core");
serverStatus.setText("Connected to Bitcoin Core (demonstration only)");
return true;
}
@@ -84,7 +84,7 @@ public class WelcomeController {
welcomeBox.getStyleClass().clear();
welcomeBox.getStyleClass().add("private-electrum");
serverToggle.setSelected(true);
serverStatus.setText("Connected to a Private Electrum Server");
serverStatus.setText("Connected to a Private Electrum Server (demonstration only)");
}
return false;
@@ -111,7 +111,7 @@ public class WelcomeController {
welcomeBox.getStyleClass().clear();
welcomeBox.getStyleClass().add("public-electrum");
serverToggle.setSelected(true);
serverStatus.setText("Connected to a Public Server");
serverStatus.setText("Connected to a Public Server (demonstration only)");
return true;
}
@@ -121,7 +121,7 @@ public class WelcomeController {
welcomeBox.getStyleClass().clear();
welcomeBox.getStyleClass().add("bitcoin-core");
serverToggle.setSelected(true);
serverStatus.setText("Connected to Bitcoin Core");
serverStatus.setText("Connected to Bitcoin Core (demonstration only)");
return true;
}
@@ -27,7 +27,7 @@ public class WelcomeDialog extends Dialog<Mode> {
final ButtonType nextButtonType = new javafx.scene.control.ButtonType("Next", ButtonBar.ButtonData.OK_DONE);
final ButtonType backButtonType = new javafx.scene.control.ButtonType("Back", ButtonBar.ButtonData.LEFT);
final ButtonType onlineButtonType = new javafx.scene.control.ButtonType("Configure Server", ButtonBar.ButtonData.APPLY);
final ButtonType offlineButtonType = new javafx.scene.control.ButtonType("Later or Offline Mode", ButtonBar.ButtonData.CANCEL_CLOSE);
final ButtonType offlineButtonType = new javafx.scene.control.ButtonType(AppServices.isConnected() ? "Done" : "Later or Offline Mode", ButtonBar.ButtonData.CANCEL_CLOSE);
dialogPane.getButtonTypes().addAll(nextButtonType, backButtonType, onlineButtonType, offlineButtonType);
Button nextButton = (Button)dialogPane.lookupButton(nextButtonType);
@@ -1,5 +1,6 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.sparrow.wallet.SendController;
import javafx.beans.NamedArg;
import javafx.scene.Node;
import javafx.scene.chart.Axis;
@@ -27,9 +28,11 @@ public class BlockTargetFeeRatesChart extends LineChart<String, Number> {
for(Iterator<Integer> targetBlocksIter = targetBlocksFeeRates.keySet().iterator(); targetBlocksIter.hasNext(); ) {
Integer targetBlocks = targetBlocksIter.next();
String category = targetBlocks + (targetBlocksIter.hasNext() ? "" : "+");
XYChart.Data<String, Number> data = new XYChart.Data<>(category, targetBlocksFeeRates.get(targetBlocks));
feeRateSeries.getData().add(data);
if(SendController.TARGET_BLOCKS_RANGE.contains(targetBlocks)) {
String category = targetBlocks + (targetBlocksIter.hasNext() ? "" : "+");
XYChart.Data<String, Number> data = new XYChart.Data<>(category, targetBlocksFeeRates.get(targetBlocks));
feeRateSeries.getData().add(data);
}
}
if(selectedTargetBlocks != null) {
@@ -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;
@@ -84,16 +84,15 @@ public class CoinTreeTable extends TreeTableView<Entry> {
WalletBirthDateDialog dlg = new WalletBirthDateDialog(wallet.getBirthDate());
Optional<Date> optDate = dlg.showAndWait();
if(optDate.isPresent()) {
Wallet pastWallet = wallet.copy();
wallet.setBirthDate(optDate.get());
Storage storage = AppServices.get().getOpenWallets().get(wallet);
if(storage != null) {
//Trigger background save of birthdate
EventManager.get().post(new WalletDataChangedEvent(wallet));
//Trigger full wallet rescan
wallet.clearHistory();
EventManager.get().post(new WalletSettingsChangedEvent(wallet, pastWallet, storage.getWalletFile()));
}
Wallet pastWallet = wallet.copy();
storage.backupTempWallet();
wallet.setBirthDate(optDate.get());
//Trigger background save of birthdate
EventManager.get().post(new WalletDataChangedEvent(wallet));
//Trigger full wallet rescan
wallet.clearHistory();
EventManager.get().post(new WalletAddressesChangedEvent(wallet, pastWallet, storage.getWalletFile()));
}
});
if(wallet.getBirthDate() == null) {
@@ -1,22 +1,22 @@
package com.sparrowwallet.sparrow.control;
import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.OutputDescriptor;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.AddressDisplayedEvent;
import com.sparrowwallet.sparrow.io.Device;
import java.util.List;
import java.util.stream.Collectors;
public class DeviceAddressDialog extends DeviceDialog<String> {
private final Wallet wallet;
private final KeyDerivation keyDerivation;
private final OutputDescriptor outputDescriptor;
public DeviceAddressDialog(List<String> operationFingerprints, Wallet wallet, KeyDerivation keyDerivation) {
super(operationFingerprints);
public DeviceAddressDialog(Wallet wallet, OutputDescriptor outputDescriptor) {
super(outputDescriptor.getExtendedPublicKeys().stream().map(extKey -> outputDescriptor.getKeyDerivation(extKey).getMasterFingerprint()).collect(Collectors.toList()));
this.wallet = wallet;
this.keyDerivation = keyDerivation;
this.outputDescriptor = outputDescriptor;
EventManager.get().register(this);
setOnCloseRequest(event -> {
@@ -26,7 +26,7 @@ public class DeviceAddressDialog extends DeviceDialog<String> {
@Override
protected DevicePane getDevicePane(Device device) {
return new DevicePane(wallet, keyDerivation, device);
return new DevicePane(wallet, outputDescriptor, device);
}
@Subscribe
@@ -101,6 +101,7 @@ public abstract class DeviceDialog<R> extends Dialog<R> {
Platform.runLater(() -> EventManager.get().post(new UsbDeviceEvent(devices)));
});
enumerateService.setOnFailed(workerStateEvent -> {
deviceAccordion.getPanes().clear();
scanBox.setVisible(true);
scanLabel.setText(workerStateEvent.getSource().getException().getMessage());
});
@@ -2,20 +2,19 @@ package com.sparrowwallet.sparrow.control;
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.drongo.wallet.WalletModel;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
@@ -25,7 +24,6 @@ import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import org.controlsfx.control.textfield.CustomPasswordField;
import org.controlsfx.control.textfield.CustomTextField;
import org.controlsfx.control.textfield.TextFields;
import org.controlsfx.glyphfont.Glyph;
import org.controlsfx.validation.ValidationResult;
@@ -35,7 +33,9 @@ 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;
public class DevicePane extends TitledDescriptionPane {
private static final Logger log = LoggerFactory.getLogger(DevicePane.class);
@@ -43,6 +43,7 @@ public class DevicePane extends TitledDescriptionPane {
private final DeviceOperation deviceOperation;
private final Wallet wallet;
private final PSBT psbt;
private final OutputDescriptor outputDescriptor;
private final KeyDerivation keyDerivation;
private final String message;
private final Device device;
@@ -63,6 +64,7 @@ public class DevicePane extends TitledDescriptionPane {
this.deviceOperation = DeviceOperation.IMPORT;
this.wallet = wallet;
this.psbt = null;
this.outputDescriptor = null;
this.keyDerivation = null;
this.message = null;
this.device = device;
@@ -83,6 +85,7 @@ public class DevicePane extends TitledDescriptionPane {
this.deviceOperation = DeviceOperation.SIGN;
this.wallet = null;
this.psbt = psbt;
this.outputDescriptor = null;
this.keyDerivation = null;
this.message = null;
this.device = device;
@@ -98,12 +101,13 @@ public class DevicePane extends TitledDescriptionPane {
buttonBox.getChildren().addAll(setPassphraseButton, signButton);
}
public DevicePane(Wallet wallet, KeyDerivation keyDerivation, Device device) {
public DevicePane(Wallet wallet, OutputDescriptor outputDescriptor, Device device) {
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
this.deviceOperation = DeviceOperation.DISPLAY_ADDRESS;
this.wallet = wallet;
this.psbt = null;
this.keyDerivation = keyDerivation;
this.outputDescriptor = outputDescriptor;
this.keyDerivation = null;
this.message = null;
this.device = device;
@@ -123,6 +127,7 @@ public class DevicePane extends TitledDescriptionPane {
this.deviceOperation = DeviceOperation.SIGN_MESSAGE;
this.wallet = wallet;
this.psbt = null;
this.outputDescriptor = null;
this.keyDerivation = keyDerivation;
this.message = message;
this.device = device;
@@ -192,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);
@@ -233,7 +251,8 @@ public class DevicePane extends TitledDescriptionPane {
displayAddressButton.managedProperty().bind(displayAddressButton.visibleProperty());
displayAddressButton.setVisible(false);
if(device.getFingerprint() != null && !device.getFingerprint().equals(keyDerivation.getMasterFingerprint())) {
List<String> fingerprints = outputDescriptor.getExtendedPublicKeys().stream().map(extKey -> outputDescriptor.getKeyDerivation(extKey).getMasterFingerprint()).collect(Collectors.toList());
if(device.getFingerprint() != null && !fingerprints.contains(device.getFingerprint())) {
displayAddressButton.setDisable(true);
}
}
@@ -370,7 +389,7 @@ public class DevicePane extends TitledDescriptionPane {
}
});
sendPinService.setOnFailed(workerStateEvent -> {
setError(sendPinService.getException().getMessage(), null);
setError("Error", sendPinService.getException().getMessage());
enterPinButton.setDisable(false);
});
setDescription("Unlocking...");
@@ -436,14 +455,29 @@ public class DevicePane extends TitledDescriptionPane {
getXpubService.setOnSucceeded(workerStateEvent -> {
String xpub = getXpubService.getValue();
Keystore keystore = new Keystore();
keystore.setLabel(device.getModel().toDisplayString());
keystore.setSource(KeystoreSource.HW_USB);
keystore.setWalletModel(device.getModel());
keystore.setKeyDerivation(new KeyDerivation(device.getFingerprint(), derivationPath));
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(xpub));
try {
Keystore keystore = new Keystore();
keystore.setLabel(device.getModel().toDisplayString());
keystore.setSource(KeystoreSource.HW_USB);
keystore.setWalletModel(device.getModel());
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());
}
});
getXpubService.setOnFailed(workerStateEvent -> {
setError("Could not retrieve xpub", getXpubService.getException().getMessage());
@@ -471,7 +505,7 @@ public class DevicePane extends TitledDescriptionPane {
}
private void displayAddress() {
Hwi.DisplayAddressService displayAddressService = new Hwi.DisplayAddressService(device, passphrase.get(), wallet.getScriptType(), keyDerivation.getDerivationPath());
Hwi.DisplayAddressService displayAddressService = new Hwi.DisplayAddressService(device, passphrase.get(), wallet.getScriptType(), outputDescriptor);
displayAddressService.setOnSucceeded(successEvent -> {
String address = displayAddressService.getValue();
EventManager.get().post(new AddressDisplayedEvent(address));
@@ -503,7 +537,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);
@@ -552,6 +586,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();
@@ -183,7 +190,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
long changeTotal = ourOutputs.stream().mapToLong(TransactionOutput::getValue).sum();
Transaction tx = blockTransaction.getTransaction();
int vSize = tx.getVirtualSize();
double vSize = tx.getVirtualSize();
int inputSize = tx.getInputs().get(0).getLength() + (tx.getInputs().get(0).hasWitness() ? tx.getInputs().get(0).getWitness().getLength() / Transaction.WITNESS_SCALE_FACTOR : 0);
List<BlockTransactionHashIndex> walletUtxos = new ArrayList<>(transactionEntry.getWallet().getWalletUtxos().keySet());
Collections.shuffle(walletUtxos);
@@ -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)));
}
@@ -81,7 +81,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 +93,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 +103,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,19 +118,19 @@ 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);
@@ -138,8 +145,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());
@@ -312,7 +312,7 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
StackPane wordsPane = (StackPane)getContent();
StackPane confirmPane = new StackPane();
confirmPane.setMaxWidth(300);
confirmPane.setMaxWidth(350);
confirmPane.setMaxHeight(100);
Region region = new Region();
region.setMinWidth(confirmPane.getMaxWidth());
@@ -392,6 +392,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));
@@ -28,13 +28,16 @@ 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 org.controlsfx.glyphfont.Glyph;
import org.controlsfx.tools.Borders;
import org.slf4j.Logger;
@@ -59,13 +62,14 @@ 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 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();
@@ -83,8 +87,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 +130,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 +146,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 +161,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,6 +183,10 @@ 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 {
@@ -484,6 +507,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 +521,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) {
@@ -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);
@@ -397,7 +397,7 @@ public class TransactionDiagram extends GridPane {
String txDesc = "Transaction";
Label txLabel = new Label(txDesc);
Tooltip tooltip = new Tooltip(walletTx.getTransaction().getLength() + " bytes\n" + walletTx.getTransaction().getVirtualSize() + " vBytes");
Tooltip tooltip = new Tooltip(walletTx.getTransaction().getLength() + " bytes\n" + String.format("%.2f", walletTx.getTransaction().getVirtualSize()) + " vBytes");
tooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
tooltip.getStyleClass().add("transaction-tooltip");
txLabel.setTooltip(tooltip);
@@ -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 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();
}
}
@@ -22,17 +22,20 @@ public class WalletPasswordDialog extends Dialog<SecureString> {
private final CustomPasswordField passwordConfirm;
private final CheckBox backupExisting;
private final CheckBox changePassword;
public WalletPasswordDialog(PasswordRequirement requirement) {
this(null, requirement);
}
private final CheckBox deleteBackups;
private boolean addingPassword;
public WalletPasswordDialog(String walletName, PasswordRequirement requirement) {
this(walletName, requirement, false);
}
public WalletPasswordDialog(String walletName, PasswordRequirement requirement, boolean suggestChangePassword) {
this.requirement = requirement;
this.password = (CustomPasswordField)TextFields.createClearablePasswordField();
this.passwordConfirm = (CustomPasswordField)TextFields.createClearablePasswordField();
this.backupExisting = new CheckBox("Backup existing wallet first");
this.changePassword = new CheckBox("Change password");
this.deleteBackups = new CheckBox("Delete any backups");
final DialogPane dialogPane = getDialogPane();
setTitle("Wallet Password" + (walletName != null ? " - " + walletName : ""));
@@ -52,13 +55,21 @@ public class WalletPasswordDialog extends Dialog<SecureString> {
content.getChildren().add(password);
content.getChildren().add(passwordConfirm);
if(requirement == PasswordRequirement.UPDATE_EMPTY || requirement == PasswordRequirement.UPDATE_SET) {
content.getChildren().add(backupExisting);
backupExisting.setSelected(true);
}
if(requirement == PasswordRequirement.UPDATE_SET) {
content.getChildren().add(changePassword);
changePassword.selectedProperty().addListener((observable, oldValue, newValue) -> {
backupExisting.setVisible(!newValue);
});
changePassword.setSelected(suggestChangePassword);
}
if(requirement == PasswordRequirement.UPDATE_EMPTY || requirement == PasswordRequirement.UPDATE_SET) {
backupExisting.managedProperty().bind(backupExisting.visibleProperty());
deleteBackups.managedProperty().bind(deleteBackups.visibleProperty());
deleteBackups.visibleProperty().bind(backupExisting.visibleProperty().not());
content.getChildren().addAll(backupExisting, deleteBackups);
backupExisting.setSelected(true);
deleteBackups.setSelected(true);
}
dialogPane.setContent(content);
@@ -87,10 +98,14 @@ public class WalletPasswordDialog extends Dialog<SecureString> {
okButton.setText("No Password");
passwordConfirm.setVisible(false);
passwordConfirm.setManaged(false);
backupExisting.setVisible(true);
addingPassword = false;
} else {
okButton.setText("Set Password");
passwordConfirm.setVisible(true);
passwordConfirm.setManaged(true);
backupExisting.setVisible(false);
addingPassword = true;
}
});
}
@@ -103,19 +118,23 @@ public class WalletPasswordDialog extends Dialog<SecureString> {
}
public boolean isBackupExisting() {
return backupExisting.isSelected();
return !(addingPassword || isChangePassword()) && backupExisting.isSelected();
}
public boolean isChangePassword() {
return changePassword.isSelected();
}
public boolean isDeleteBackups() {
return (addingPassword || isChangePassword()) && deleteBackups.isSelected();
}
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;
@@ -6,7 +6,9 @@ import com.github.sarxos.webcam.WebcamResolution;
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.Task;
@@ -20,6 +22,7 @@ import java.util.concurrent.TimeUnit;
public class WebcamService extends Service<Image> {
private WebcamResolution resolution;
private final WebcamListener listener;
private BooleanProperty opening = new SimpleBooleanProperty(false);
private final ObjectProperty<Result> resultProperty = new SimpleObjectProperty<>(null);
@@ -41,7 +44,9 @@ public class WebcamService extends Service<Image> {
cam.addWebcamListener(listener);
}
opening.set(true);
cam.open();
opening.set(false);
while(!isCancelled()) {
if(cam.isImageNew()) {
BufferedImage bimg = cam.getImage();
@@ -49,10 +54,12 @@ public class WebcamService extends Service<Image> {
readQR(bimg);
}
}
cam.close();
return getValue();
} finally {
cam.close();
opening.set(false);
if(!cam.close()) {
cam.close();
}
}
}
};
@@ -89,4 +96,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,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;
}
@@ -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,14 +1,21 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.wallet.FeeRatesSelection;
public class FeeRatesSelectionChangedEvent {
private final Wallet wallet;
private final FeeRatesSelection feeRatesSelection;
public FeeRatesSelectionChangedEvent(FeeRatesSelection feeRatesSelection) {
public FeeRatesSelectionChangedEvent(Wallet wallet, FeeRatesSelection feeRatesSelection) {
this.wallet = wallet;
this.feeRatesSelection = feeRatesSelection;
}
public Wallet getWallet() {
return wallet;
}
public FeeRatesSelection getFeeRateSelection() {
return feeRatesSelection;
}
@@ -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,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;
}
}
@@ -0,0 +1,9 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet;
public class WalletHistoryFinishedEvent extends WalletHistoryStatusEvent {
public WalletHistoryFinishedEvent(Wallet wallet) {
super(wallet, false);
}
}
@@ -0,0 +1,17 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletNode;
public class WalletHistoryStartedEvent extends WalletHistoryStatusEvent {
private final WalletNode walletNode;
public WalletHistoryStartedEvent(Wallet wallet, WalletNode walletNode) {
super(wallet, true);
this.walletNode = walletNode;
}
public WalletNode getWalletNode() {
return walletNode;
}
}
@@ -24,7 +24,7 @@ public class WalletHistoryStatusEvent {
public WalletHistoryStatusEvent(Wallet wallet, String errorMessage) {
this.wallet = wallet;
this.loading = true;
this.loading = false;
this.statusMessage = null;
this.errorMessage = errorMessage;
}
@@ -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;
@@ -40,6 +40,7 @@ public class FontAwesome5 extends GlyphFont {
LOCK_OPEN('\uf3c1'),
PEN_FANCY('\uf5ac'),
PLUS('\uf067'),
PLUS_CIRCLE('\uf055'),
QRCODE('\uf029'),
QUESTION_CIRCLE('\uf059'),
RANDOM('\uf074'),
@@ -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;
@@ -80,7 +81,7 @@ public class Config {
if(config != null) {
return config;
}
} catch (IOException e) {
} catch(Exception e) {
log.error("Error opening " + configFile.getAbsolutePath(), e);
//Ignore and assume no config
}
@@ -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();
}
@@ -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;
@@ -4,6 +4,7 @@ import com.google.common.io.ByteStreams;
import com.google.common.io.CharStreams;
import com.google.gson.*;
import com.sparrowwallet.drongo.Network;
import com.sparrowwallet.drongo.OutputDescriptor;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.psbt.PSBTParseException;
@@ -29,7 +30,7 @@ import java.util.zip.ZipInputStream;
public class Hwi {
private static final Logger log = LoggerFactory.getLogger(Hwi.class);
private static final String TEMP_FILE_PREFIX = "hwi-1.2.1-";
private static final String VERSION_PREFIX = "hwi-2.0.0-final";
private static boolean isPromptActive = false;
@@ -42,11 +43,18 @@ public class Hwi {
command = List.of(getHwiExecutable(Command.ENUMERATE).getAbsolutePath(), Command.ENUMERATE.toString());
}
isPromptActive = true;
String output = execute(command);
Device[] devices = getGson().fromJson(output, Device[].class);
if(devices == null) {
throw new ImportException("Error scanning, check devices are ready");
}
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;
}
}
@@ -84,32 +92,32 @@ public class Hwi {
if(result.get("xpub") != null) {
return result.get("xpub").getAsString();
} else {
throw new ImportException("Could not retrieve xpub - reconnect your device and try again.");
JsonElement error = result.get("error");
if(error != null) {
throw new ImportException(error.getAsString());
} else {
throw new ImportException("Could not retrieve xpub - reconnect your device and try again.");
}
}
} catch(IOException e) {
throw new ImportException(e);
}
}
public String displayAddress(Device device, String passphrase, ScriptType scriptType, String derivationPath) throws DisplayAddressException {
public String displayAddress(Device device, String passphrase, ScriptType scriptType, OutputDescriptor outputDescriptor) throws DisplayAddressException {
try {
if(!List.of(ScriptType.P2PKH, ScriptType.P2SH_P2WPKH, ScriptType.P2WPKH).contains(scriptType)) {
throw new IllegalArgumentException("Cannot display address for script type " + scriptType + ": Only single sig types supported");
if(!Arrays.asList(ScriptType.SINGLE_HASH_TYPES).contains(scriptType)) {
throw new IllegalArgumentException("Cannot display address for script type " + scriptType + ": Only single hash types supported");
}
String type = null;
if(scriptType == ScriptType.P2SH_P2WPKH) {
type = "--sh_wpkh";
} else if(scriptType == ScriptType.P2WPKH) {
type = "--wpkh";
}
String descriptor = outputDescriptor.toString();
isPromptActive = false;
isPromptActive = true;
String output;
if(passphrase != null && !passphrase.isEmpty() && device.getModel().externalPassphraseEntry()) {
output = execute(getDeviceCommand(device, passphrase, Command.DISPLAY_ADDRESS, "--path", derivationPath, type));
output = execute(getDeviceCommand(device, passphrase, Command.DISPLAY_ADDRESS, "--desc", descriptor));
} else {
output = execute(getDeviceCommand(device, Command.DISPLAY_ADDRESS, "--path", derivationPath, type));
output = execute(getDeviceCommand(device, Command.DISPLAY_ADDRESS, "--desc", descriptor));
}
JsonObject result = JsonParser.parseString(output).getAsJsonObject();
@@ -125,12 +133,14 @@ public class Hwi {
}
} catch(IOException e) {
throw new DisplayAddressException(e);
} finally {
isPromptActive = false;
}
}
public String signMessage(Device device, String passphrase, String message, String derivationPath) throws SignMessageException {
try {
isPromptActive = false;
isPromptActive = true;
String output;
if(passphrase != null && !passphrase.isEmpty() && device.getModel().externalPassphraseEntry()) {
output = execute(getDeviceCommand(device, passphrase, Command.SIGN_MESSAGE, message, derivationPath));
@@ -151,6 +161,8 @@ public class Hwi {
}
} catch(IOException e) {
throw new SignMessageException(e);
} finally {
isPromptActive = false;
}
}
@@ -165,7 +177,6 @@ public class Hwi {
} else {
output = execute(getDeviceCommand(device, Command.SIGN_TX, psbtBase64));
}
isPromptActive = false;
JsonObject result = JsonParser.parseString(output).getAsJsonObject();
if(result.get("psbt") != null) {
@@ -187,6 +198,8 @@ public class Hwi {
throw new SignTransactionException("Could not sign PSBT", e);
} catch(PSBTParseException e) {
throw new SignTransactionException("Could not parsed signed PSBT", e);
} finally {
isPromptActive = false;
}
}
@@ -199,7 +212,7 @@ public class Hwi {
private synchronized File getHwiExecutable(Command command) {
File hwiExecutable = Config.get().getHwi();
if(hwiExecutable != null && hwiExecutable.exists()) {
if(command.isTestFirst() && (!hwiExecutable.getAbsolutePath().contains(TEMP_FILE_PREFIX) || !testHwi(hwiExecutable))) {
if(command.isTestFirst() && (!hwiExecutable.getAbsolutePath().contains(VERSION_PREFIX) || !testHwi(hwiExecutable))) {
if(Platform.getCurrent() == Platform.OSX) {
deleteDirectory(hwiExecutable.getParentFile());
} else {
@@ -218,8 +231,8 @@ public class Hwi {
//The check will still happen on first invocation, but will not thereafter
//See https://github.com/bitcoin-core/HWI/issues/327 for details
if(platform == Platform.OSX) {
InputStream inputStream = Hwi.class.getResourceAsStream("/native/osx/x64/hwi-1.2.1-mac-amd64-signed.zip");
Path tempHwiDirPath = Files.createTempDirectory(TEMP_FILE_PREFIX, PosixFilePermissions.asFileAttribute(ownerExecutableWritable));
InputStream inputStream = Hwi.class.getResourceAsStream("/native/osx/x64/" + VERSION_PREFIX + "-mac-amd64-signed.zip");
Path tempHwiDirPath = Files.createTempDirectory(VERSION_PREFIX, PosixFilePermissions.asFileAttribute(ownerExecutableWritable));
File tempHwiDir = tempHwiDirPath.toFile();
//tempHwiDir.deleteOnExit();
log.debug("Using temp HWI path: " + tempHwiDir.getAbsolutePath());
@@ -227,16 +240,20 @@ public class Hwi {
File tempExec = null;
ZipInputStream zis = new ZipInputStream(inputStream);
ZipEntry zipEntry = zis.getNextEntry();
while (zipEntry != null) {
File newFile = newFile(tempHwiDir, zipEntry, ownerExecutableWritable);
//newFile.deleteOnExit();
FileOutputStream fos = new FileOutputStream(newFile);
ByteStreams.copy(zis, new FileOutputStream(newFile));
fos.flush();
fos.close();
while(zipEntry != null) {
if(zipEntry.isDirectory()) {
newDirectory(tempHwiDir, zipEntry, ownerExecutableWritable);
} else {
File newFile = newFile(tempHwiDir, zipEntry, ownerExecutableWritable);
//newFile.deleteOnExit();
FileOutputStream fos = new FileOutputStream(newFile);
ByteStreams.copy(zis, new FileOutputStream(newFile));
fos.flush();
fos.close();
if (zipEntry.getName().equals("hwi")) {
tempExec = newFile;
if(zipEntry.getName().equals("hwi")) {
tempExec = newFile;
}
}
zipEntry = zis.getNextEntry();
@@ -250,10 +267,10 @@ public class Hwi {
Path tempExecPath;
if(platform == Platform.WINDOWS) {
inputStream = Hwi.class.getResourceAsStream("/native/windows/x64/hwi.exe");
tempExecPath = Files.createTempFile(TEMP_FILE_PREFIX, null);
tempExecPath = Files.createTempFile(VERSION_PREFIX, null);
} else {
inputStream = Hwi.class.getResourceAsStream("/native/linux/x64/hwi");
tempExecPath = Files.createTempFile(TEMP_FILE_PREFIX, null, PosixFilePermissions.asFileAttribute(ownerExecutableWritable));
tempExecPath = Files.createTempFile(VERSION_PREFIX, null, PosixFilePermissions.asFileAttribute(ownerExecutableWritable));
}
File tempExec = tempExecPath.toFile();
@@ -299,6 +316,20 @@ public class Hwi {
return directoryToBeDeleted.delete();
}
public static File newDirectory(File destinationDir, ZipEntry zipEntry, Set<PosixFilePermission> setFilePermissions) throws IOException {
String destDirPath = destinationDir.getCanonicalPath();
Path path = Path.of(destDirPath, zipEntry.getName());
File destDir = Files.createDirectory(path, PosixFilePermissions.asFileAttribute(setFilePermissions)).toFile();
String destSubDirPath = destDir.getCanonicalPath();
if(!destSubDirPath.startsWith(destDirPath + File.separator)) {
throw new IOException("Entry is outside of the target dir: " + zipEntry.getName());
}
return destDir;
}
public static File newFile(File destinationDir, ZipEntry zipEntry, Set<PosixFilePermission> setFilePermissions) throws IOException {
String destDirPath = destinationDir.getCanonicalPath();
@@ -306,7 +337,7 @@ public class Hwi {
File destFile = Files.createFile(path, PosixFilePermissions.asFileAttribute(setFilePermissions)).toFile();
String destFilePath = destFile.getCanonicalPath();
if (!destFilePath.startsWith(destDirPath + File.separator)) {
if(!destFilePath.startsWith(destDirPath + File.separator)) {
throw new IOException("Entry is outside of the target dir: " + zipEntry.getName());
}
@@ -324,30 +355,41 @@ public class Hwi {
private List<String> getDeviceCommand(Device device, Command command) throws IOException {
List<String> elements = new ArrayList<>(List.of(getHwiExecutable(command).getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType(), command.toString()));
if(Network.get() != Network.MAINNET) {
elements.add(elements.size() - 1, "--testnet");
}
addChainType(elements);
return elements;
}
private List<String> getDeviceCommand(Device device, Command command, String... commandData) throws IOException {
List<String> elements = new ArrayList<>(List.of(getHwiExecutable(command).getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType(), command.toString()));
if(Network.get() != Network.MAINNET) {
elements.add(elements.size() - 1, "--testnet");
}
addChainType(elements);
elements.addAll(Arrays.stream(commandData).filter(Objects::nonNull).collect(Collectors.toList()));
return elements;
}
private List<String> getDeviceCommand(Device device, String passphrase, Command command, String... commandData) throws IOException {
List<String> elements = new ArrayList<>(List.of(getHwiExecutable(command).getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType(), "--password", passphrase, command.toString()));
if(Network.get() != Network.MAINNET) {
elements.add(elements.size() - 1, "--testnet");
}
addChainType(elements);
elements.addAll(Arrays.stream(commandData).filter(Objects::nonNull).collect(Collectors.toList()));
return elements;
}
private void addChainType(List<String> elements) {
if(Network.get() != Network.MAINNET) {
elements.add(elements.size() - 1, "--chain");
elements.add(elements.size() - 1, getChainName(Network.get()));
}
}
private String getChainName(Network network) {
if(network == Network.MAINNET) {
return "main";
} else if(network == Network.TESTNET) {
return "test";
}
return network.toString();
}
public static class EnumerateService extends Service<List<Device>> {
private final String passphrase;
@@ -430,13 +472,13 @@ public class Hwi {
private final Device device;
private final String passphrase;
private final ScriptType scriptType;
private final String derivationPath;
private final OutputDescriptor outputDescriptor;
public DisplayAddressService(Device device, String passphrase, ScriptType scriptType, String derivationPath) {
public DisplayAddressService(Device device, String passphrase, ScriptType scriptType, OutputDescriptor outputDescriptor) {
this.device = device;
this.passphrase = passphrase;
this.scriptType = scriptType;
this.derivationPath = derivationPath;
this.outputDescriptor = outputDescriptor;
}
@Override
@@ -444,7 +486,7 @@ public class Hwi {
return new Task<>() {
protected String call() throws DisplayAddressException {
Hwi hwi = new Hwi();
return hwi.displayAddress(device, passphrase, scriptType, derivationPath);
return hwi.displayAddress(device, passphrase, scriptType, outputDescriptor);
}
};
}
@@ -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;
@@ -10,13 +10,14 @@ import com.sparrowwallet.drongo.crypto.*;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.MnemonicException;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletNode;
import com.sparrowwallet.sparrow.MainApp;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import org.controlsfx.tools.Platform;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.lang.reflect.Type;
@@ -26,14 +27,18 @@ import java.security.SecureRandom;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.*;
import static com.sparrowwallet.drongo.crypto.Argon2KeyDeriver.SPRW1_PARAMETERS;
public class Storage {
private static final Logger log = LoggerFactory.getLogger(Storage.class);
public static final ECKey NO_PASSWORD_KEY = ECKey.fromPublicOnly(ECKey.fromPrivate(Utils.hexToBytes("885e5a09708a167ea356a252387aa7c4893d138d632e296df8fbf5c12798bd28")));
private static final DateFormat BACKUP_DATE_FORMAT = new SimpleDateFormat("yyyyMMddHHmmss");
private static final Pattern DATE_PATTERN = Pattern.compile(".+-([0-9]{14}?).*");
public static final String SPARROW_DIR = ".sparrow";
public static final String WINDOWS_SPARROW_DIR = "Sparrow";
@@ -41,6 +46,7 @@ public class Storage {
public static final String WALLETS_BACKUP_DIR = "backup";
public static final String HEADER_MAGIC_1 = "SPRW1";
private static final int BINARY_HEADER_LENGTH = 28;
public static final String TEMP_BACKUP_EXTENSION = "tmp";
private File walletFile;
private final Gson gson;
@@ -81,17 +87,49 @@ public class Storage {
return gsonBuilder.setPrettyPrinting().disableHtmlEscaping().create();
}
public Wallet loadWallet() throws IOException {
Reader reader = new FileReader(walletFile);
public WalletBackupAndKey loadWallet() throws IOException {
Wallet wallet = loadWallet(walletFile);
Wallet backupWallet = null;
File[] backups = getBackups("json." + TEMP_BACKUP_EXTENSION);
if(backups.length > 0) {
try {
backupWallet = loadWallet(backups[0]);
} catch(Exception e) {
log.error("Error loading backup wallet " + TEMP_BACKUP_EXTENSION, e);
}
}
encryptionPubKey = NO_PASSWORD_KEY;
return new WalletBackupAndKey(wallet, backupWallet, null);
}
public Wallet loadWallet(File jsonFile) throws IOException {
Reader reader = new FileReader(jsonFile);
Wallet wallet = gson.fromJson(reader, Wallet.class);
reader.close();
encryptionPubKey = NO_PASSWORD_KEY;
return wallet;
}
public WalletAndKey loadWallet(CharSequence password) throws IOException, StorageException {
InputStream fileStream = new FileInputStream(walletFile);
public WalletBackupAndKey loadWallet(CharSequence password) throws IOException, StorageException {
WalletAndKey walletAndKey = loadWallet(walletFile, password);
WalletAndKey backupAndKey = new WalletAndKey(null, null);
File[] backups = getBackups(TEMP_BACKUP_EXTENSION, "json." + TEMP_BACKUP_EXTENSION);
if(backups.length > 0) {
try {
backupAndKey = loadWallet(backups[0], password);
} catch(Exception e) {
log.error("Error loading backup wallet " + TEMP_BACKUP_EXTENSION, e);
}
}
return new WalletBackupAndKey(walletAndKey.wallet, backupAndKey.wallet, walletAndKey.key);
}
public WalletAndKey loadWallet(File encryptedFile, CharSequence password) throws IOException, StorageException {
InputStream fileStream = new FileInputStream(encryptedFile);
ECKey encryptionKey = getEncryptionKey(password, fileStream);
InputStream inputStream = new InflaterInputStream(new ECIESInputStream(fileStream, encryptionKey, getEncryptionMagic()));
@@ -168,6 +206,18 @@ public class Storage {
}
public void backupWallet() throws IOException {
backupWallet(null);
}
public void backupTempWallet() {
try {
backupWallet(TEMP_BACKUP_EXTENSION);
} catch(IOException e) {
log.error("Error creating ." + TEMP_BACKUP_EXTENSION + " backup wallet", e);
}
}
public void backupWallet(String extension) throws IOException {
File backupDir = getWalletsBackupDir();
Date backupDate = new Date();
@@ -180,10 +230,52 @@ public class Storage {
backupName += dateSuffix;
}
if(extension != null) {
backupName += "." + extension;
}
File backupFile = new File(backupDir, backupName);
Files.copy(walletFile, backupFile);
}
public void deleteBackups() {
deleteBackups(null);
}
public void deleteBackups(String extension) {
File[] backups = getBackups(extension);
for(File backup : backups) {
backup.delete();
}
}
private File[] getBackups(String extension) {
return getBackups(extension, null);
}
private File[] getBackups(String extension, String notExtension) {
File backupDir = getWalletsBackupDir();
File[] backups = backupDir.listFiles((dir, name) -> {
return name.startsWith(Files.getNameWithoutExtension(walletFile.getName()) + "-") &&
getBackupDate(name) != null &&
(extension == null || name.endsWith("." + extension)) &&
(notExtension == null || !name.endsWith("." + notExtension));
});
Arrays.sort(backups, Comparator.comparing(o -> getBackupDate(((File)o).getName())).reversed());
return backups;
}
private String getBackupDate(String backupFileName) {
Matcher matcher = DATE_PATTERN.matcher(backupFileName);
if(matcher.matches()) {
return matcher.group(1);
}
return null;
}
public ECKey getEncryptionPubKey() {
return encryptionPubKey;
}
@@ -238,6 +330,8 @@ public class Storage {
}
keyDeriver = new Argon2KeyDeriver(salt);
} else if(inputStream != null) {
inputStream.skip(BINARY_HEADER_LENGTH);
}
return keyDeriver;
@@ -465,7 +559,16 @@ public class Storage {
}
}
public static class LoadWalletService extends Service<WalletAndKey> {
public static class WalletBackupAndKey extends WalletAndKey {
public final Wallet backupWallet;
public WalletBackupAndKey(Wallet wallet, Wallet backupWallet, Key key) {
super(wallet, key);
this.backupWallet = backupWallet;
}
}
public static class LoadWalletService extends Service<WalletBackupAndKey> {
private final Storage storage;
private final SecureString password;
@@ -475,12 +578,12 @@ public class Storage {
}
@Override
protected Task<WalletAndKey> createTask() {
protected Task<WalletBackupAndKey> createTask() {
return new Task<>() {
protected WalletAndKey call() throws IOException, StorageException, MnemonicException {
WalletAndKey walletAndKey = storage.loadWallet(password);
protected WalletBackupAndKey call() throws IOException, StorageException {
WalletBackupAndKey walletBackupAndKey = storage.loadWallet(password);
password.clear();
return walletAndKey;
return walletBackupAndKey;
}
};
}
@@ -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()));
}
}
@@ -40,6 +40,9 @@ public enum FeeRatesSource {
};
private static final Logger log = LoggerFactory.getLogger(FeeRatesSource.class);
public static final int BLOCKS_IN_HALF_HOUR = 3;
public static final int BLOCKS_IN_HOUR = 6;
public static final int BLOCKS_IN_TWO_HOURS = 12;
private final String name;
@@ -60,16 +63,28 @@ 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 < 3) {
if(blockTarget < BLOCKS_IN_HALF_HOUR) {
blockTargetFeeRates.put(blockTarget, threeTierRates.fastestFee);
} else if(blockTarget < 6) {
} else if(blockTarget < BLOCKS_IN_HOUR) {
blockTargetFeeRates.put(blockTarget, threeTierRates.halfHourFee);
} else if(blockTarget <= 10 || defaultblockTargetFeeRates.get(blockTarget) > threeTierRates.hourFee) {
} 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);
@@ -98,5 +113,6 @@ public enum FeeRatesSource {
Double fastestFee;
Double halfHourFee;
Double hourFee;
Double minimumFee;
}
}
@@ -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) {
@@ -32,10 +32,10 @@ public class SubscriptionService {
Set<String> existingStatuses = ElectrumServer.getSubscribedScriptHashes().get(scriptHash);
if(existingStatuses == null) {
log.warn("Received script hash status update for unsubscribed script hash: " + scriptHash);
log.debug("Received script hash status update for unsubscribed script hash: " + scriptHash);
ElectrumServer.updateSubscribedScriptHashStatus(scriptHash, status);
} else if(existingStatuses.contains(status)) {
log.warn("Received script hash status update, but status has not changed");
log.debug("Received script hash status update, but status has not changed");
return;
} else {
String oldStatus = Iterables.getLast(existingStatuses);
@@ -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 -> {
@@ -149,6 +149,10 @@ public class ServerPreferencesController extends PreferencesDetailController {
private ElectrumServer.ConnectionService connectionService;
private Boolean useSslOriginal;
private Boolean useProxyOriginal;
@Override
public void initializeView(Config config) {
EventManager.get().register(this);
@@ -412,18 +416,28 @@ public class ServerPreferencesController extends PreferencesDetailController {
torService.setPeriod(Duration.hours(1000));
torService.setRestartOnFailure(false);
torService.setOnRunning(workerStateEvent -> {
testResults.setText(testResults.getText() + "\nStarting Tor...");
});
torService.setOnSucceeded(workerStateEvent -> {
Tor.setDefault(torService.getValue());
torService.cancel();
testResults.setText(testResults.getText() + "\nTor started");
testResults.appendText("\nTor running, connecting to " + Config.get().getServerAddress() + "...");
startElectrumConnection();
});
torService.setOnFailed(workerStateEvent -> {
testResults.setText(testResults.getText() + "\nTor failed to start");
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.getCause() != null && exception.getCause() instanceof TorControlError && exception.getCause().getMessage().contains("Failed to bind") &&
useProxyOriginal == null && !useProxy.isSelected() && proxyHost.getText().isEmpty() && proxyPort.getText().isEmpty()) {
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();
@@ -439,6 +453,8 @@ public class ServerPreferencesController extends PreferencesDetailController {
connectionService.setRestartOnFailure(false);
EventManager.get().register(connectionService);
useSslOriginal = null;
connectionService.setOnSucceeded(successEvent -> {
EventManager.get().unregister(connectionService);
ConnectionEvent connectionEvent = (ConnectionEvent)connectionService.getValue();
@@ -446,10 +462,41 @@ 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);
showConnectionFailure(workerStateEvent.getSource().getException());
connectionService.cancel();
if(Config.get().getServerType() == ServerType.ELECTRUM_SERVER) {
if(useSslOriginal == null) {
Integer portAsInteger = getPort(electrumPort.getText());
if(!electrumUseSsl.isSelected() && portAsInteger != null && portAsInteger == TcpOverTlsTransport.DEFAULT_PORT) {
useSslOriginal = false;
electrumUseSsl.setSelected(true);
} else if(electrumUseSsl.isSelected() && portAsInteger != null && portAsInteger == TcpTransport.DEFAULT_PORT) {
useSslOriginal = true;
electrumUseSsl.setSelected(false);
}
if(useSslOriginal != null) {
EventManager.get().register(connectionService);
connectionService.reset();
connectionService.start();
}
} else {
electrumUseSsl.setSelected(useSslOriginal);
useSslOriginal = null;
}
}
if(useProxyOriginal != null && !useProxyOriginal) {
useProxy.setSelected(false);
proxyHost.setText("");
proxyPort.setText("");
useProxyOriginal = null;
}
});
connectionService.start();
}
@@ -739,4 +786,13 @@ public class ServerPreferencesController extends PreferencesDetailController {
connectionService.cancel();
}
}
@Subscribe
public void torStatus(TorStatusEvent event) {
Platform.runLater(() -> {
if(torService != null && torService.isRunning()) {
testResults.appendText("\n" + event.getStatus());
}
});
}
}
@@ -29,6 +29,7 @@ import javafx.collections.ObservableMap;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
@@ -451,7 +452,7 @@ public class HeadersController extends TransactionFormController implements Init
private void updateSize() {
size.setText(headersForm.getTransaction().getSize() + " B");
virtualSize.setText(headersForm.getTransaction().getVirtualSize() + " vB");
virtualSize.setText(String.format("%.2f", headersForm.getTransaction().getVirtualSize()) + " vB");
}
private Long calculateFee(Map<Sha256Hash, BlockTransaction> inputTransactions) {
@@ -599,11 +600,21 @@ public class HeadersController extends TransactionFormController implements Init
private void updateTxId() {
id.setText(headersForm.getTransaction().calculateTxId(false).toString());
if(!headersForm.isTransactionFinalized()) {
if(!id.getStyleClass().contains(UNFINALIZED_TXID_CLASS)) {
id.getStyleClass().add(UNFINALIZED_TXID_CLASS);
}
addStyleClass(id, UNFINALIZED_TXID_CLASS);
addStyleClass(size, UNFINALIZED_TXID_CLASS);
addStyleClass(virtualSize, UNFINALIZED_TXID_CLASS);
addStyleClass(feeRate, UNFINALIZED_TXID_CLASS);
} else {
id.getStyleClass().remove(UNFINALIZED_TXID_CLASS);
size.getStyleClass().remove(UNFINALIZED_TXID_CLASS);
virtualSize.getStyleClass().remove(UNFINALIZED_TXID_CLASS);
feeRate.getStyleClass().remove(UNFINALIZED_TXID_CLASS);
}
}
private void addStyleClass(Node node, String styleClass) {
if(!node.getStyleClass().contains(styleClass)) {
node.getStyleClass().add(styleClass);
}
}
@@ -705,7 +716,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());
@@ -835,10 +846,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();
}
});
@@ -1121,10 +1135,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();
}
}
@@ -2,6 +2,7 @@ package com.sparrowwallet.sparrow.wallet;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.io.Storage;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.ButtonBar;
import javafx.scene.control.ButtonType;
@@ -11,10 +12,11 @@ import org.controlsfx.tools.Borders;
import java.io.IOException;
public class AdvancedDialog extends Dialog<Void> {
public AdvancedDialog(Wallet wallet) {
public class AdvancedDialog extends Dialog<Boolean> {
public AdvancedDialog(WalletForm walletForm) {
final DialogPane dialogPane = getDialogPane();
AppServices.setStageIcon(dialogPane.getScene().getWindow());
Wallet wallet = walletForm.getWallet();
try {
FXMLLoader advancedLoader = new FXMLLoader(AppServices.class.getResource("wallet/advanced.fxml"));
@@ -22,11 +24,18 @@ public class AdvancedDialog extends Dialog<Void> {
AdvancedController settingsAdvancedController = advancedLoader.getController();
settingsAdvancedController.initializeView(wallet);
boolean noPassword = Storage.NO_PASSWORD_KEY.equals(walletForm.getStorage().getEncryptionPubKey());
final ButtonType closeButtonType = new javafx.scene.control.ButtonType("Close", ButtonBar.ButtonData.CANCEL_CLOSE);
dialogPane.getButtonTypes().addAll(closeButtonType);
final ButtonType passwordButtonType = new javafx.scene.control.ButtonType(noPassword ? "Add Password..." : "Change Password...", ButtonBar.ButtonData.LEFT);
dialogPane.getButtonTypes().add(closeButtonType);
if(wallet.isValid()) {
dialogPane.getButtonTypes().add(passwordButtonType);
}
dialogPane.setPrefWidth(400);
dialogPane.setPrefHeight(300);
setResultConverter(dialogButton -> dialogButton == passwordButtonType);
}
catch(IOException e) {
throw new RuntimeException(e);
@@ -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());
@@ -60,7 +60,7 @@ public class PaymentController extends WalletFormController implements Initializ
private FiatLabel fiatAmount;
@FXML
private Button maxButton;
private ToggleButton maxButton;
@FXML
private Button addPaymentButton;
@@ -72,6 +72,11 @@ public class PaymentController extends WalletFormController implements Initializ
sendController.utxoSelectorProperty().setValue(null);
}
for(Tab tab : sendController.getPaymentTabs().getTabs()) {
PaymentController controller = (PaymentController) tab.getUserData();
controller.setSendMax(false);
}
Long recipientValueSats = getRecipientValueSats();
if(recipientValueSats != null) {
setFiatAmount(AppServices.getFiatCurrencyExchangeRate(), recipientValueSats);
@@ -235,7 +240,7 @@ public class PaymentController extends WalletFormController implements Initializ
}
public Payment getPayment() {
return getPayment(false);
return getPayment(isSendMax());
}
public Payment getPayment(boolean sendAll) {
@@ -273,6 +278,7 @@ public class PaymentController extends WalletFormController implements Initializ
amount.textProperty().addListener(amountListener);
fiatAmount.setText("");
setSendMax(false);
}
public void setMaxInput(ActionEvent event) {
@@ -287,9 +293,11 @@ public class PaymentController extends WalletFormController implements Initializ
for(Tab tab : sendController.getPaymentTabs().getTabs()) {
PaymentController controller = (PaymentController)tab.getUserData();
if(controller != this) {
controller.setSendMax(false);
payments.add(controller.getPayment());
} else {
payments.add(getPayment(true));
setSendMax(true);
payments.add(getPayment());
}
}
sendController.updateTransaction(payments);
@@ -318,6 +326,7 @@ public class PaymentController extends WalletFormController implements Initializ
}
if(bitcoinURI.getAmount() != null) {
setRecipientValueSats(bitcoinURI.getAmount());
setFiatAmount(AppServices.getFiatCurrencyExchangeRate(), bitcoinURI.getAmount());
}
if(bitcoinURI.getPayjoinUrl() != null) {
AppServices.addPayjoinURI(bitcoinURI);
@@ -333,6 +342,14 @@ public class PaymentController extends WalletFormController implements Initializ
return addPaymentButton;
}
public boolean isSendMax() {
return maxButton.isSelected();
}
public void setSendMax(boolean sendMax) {
maxButton.setSelected(sendMax);
}
@Subscribe
public void bitcoinUnitChanged(BitcoinUnitChangedEvent event) {
BitcoinUnit unit = sendController.getBitcoinUnit(event.getBitcoinUnit());
@@ -9,7 +9,7 @@ import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.OutputDescriptor;
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.KeystoreSource;
@@ -17,10 +17,7 @@ import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.*;
import com.sparrowwallet.sparrow.event.ReceiveToEvent;
import com.sparrowwallet.sparrow.event.UsbDeviceEvent;
import com.sparrowwallet.sparrow.event.WalletHistoryChangedEvent;
import com.sparrowwallet.sparrow.event.WalletNodesChangedEvent;
import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.Device;
import com.sparrowwallet.sparrow.io.Hwi;
@@ -153,26 +150,26 @@ public class ReceiveController extends WalletFormController implements Initializ
}
private void updateDisplayAddress(List<Device> devices) {
//Can only display address for single sig wallets. See https://github.com/bitcoin-core/HWI/issues/224
Wallet wallet = getWalletForm().getWallet();
if(wallet.getPolicyType().equals(PolicyType.SINGLE)) {
List<Device> addressDevices = devices.stream().filter(device -> wallet.getKeystores().get(0).getKeyDerivation().getMasterFingerprint().equals(device.getFingerprint())).collect(Collectors.toList());
if(addressDevices.isEmpty()) {
addressDevices = devices.stream().filter(device -> device.getNeedsPinSent() || device.getNeedsPassphraseSent()).collect(Collectors.toList());
}
OutputDescriptor walletDescriptor = OutputDescriptor.getOutputDescriptor(walletForm.getWallet());
List<String> walletFingerprints = walletDescriptor.getExtendedPublicKeys().stream().map(extKey -> walletDescriptor.getKeyDerivation(extKey).getMasterFingerprint()).collect(Collectors.toList());
if(!addressDevices.isEmpty()) {
if(currentEntry != null) {
displayAddress.setVisible(true);
}
List<Device> addressDevices = devices.stream().filter(device -> walletFingerprints.contains(device.getFingerprint())).collect(Collectors.toList());
if(addressDevices.isEmpty()) {
addressDevices = devices.stream().filter(device -> device.getNeedsPinSent() || device.getNeedsPassphraseSent()).collect(Collectors.toList());
}
displayAddress.setUserData(addressDevices);
return;
} else if(currentEntry != null && wallet.getKeystores().stream().anyMatch(keystore -> keystore.getSource().equals(KeystoreSource.HW_USB))) {
if(!addressDevices.isEmpty()) {
if(currentEntry != null) {
displayAddress.setVisible(true);
displayAddress.setUserData(null);
return;
}
displayAddress.setUserData(addressDevices);
return;
} else if(currentEntry != null && wallet.getKeystores().stream().anyMatch(keystore -> keystore.getSource().equals(KeystoreSource.HW_USB))) {
displayAddress.setVisible(true);
displayAddress.setUserData(null);
return;
}
displayAddress.setVisible(false);
@@ -204,28 +201,27 @@ public class ReceiveController extends WalletFormController implements Initializ
@SuppressWarnings("unchecked")
public void displayAddress(ActionEvent event) {
Wallet wallet = getWalletForm().getWallet();
if(wallet.getPolicyType() == PolicyType.SINGLE && currentEntry != null) {
Keystore keystore = wallet.getKeystores().get(0);
KeyDerivation fullDerivation = keystore.getKeyDerivation().extend(currentEntry.getNode().getDerivation());
if(currentEntry != null) {
OutputDescriptor addressDescriptor = OutputDescriptor.getOutputDescriptor(walletForm.getWallet(), currentEntry.getNode().getKeyPurpose(), currentEntry.getNode().getIndex());
List<Device> possibleDevices = (List<Device>)displayAddress.getUserData();
if(possibleDevices != null && !possibleDevices.isEmpty()) {
if(possibleDevices.size() > 1 || possibleDevices.get(0).getNeedsPinSent() || possibleDevices.get(0).getNeedsPassphraseSent()) {
DeviceAddressDialog dlg = new DeviceAddressDialog(List.of(keystore.getKeyDerivation().getMasterFingerprint()), wallet, fullDerivation);
DeviceAddressDialog dlg = new DeviceAddressDialog(wallet, addressDescriptor);
dlg.showAndWait();
} else {
Device actualDevice = possibleDevices.get(0);
Hwi.DisplayAddressService displayAddressService = new Hwi.DisplayAddressService(actualDevice, "", wallet.getScriptType(), fullDerivation.getDerivationPath());
Hwi.DisplayAddressService displayAddressService = new Hwi.DisplayAddressService(actualDevice, "", wallet.getScriptType(), addressDescriptor);
displayAddressService.setOnFailed(failedEvent -> {
Platform.runLater(() -> {
DeviceAddressDialog dlg = new DeviceAddressDialog(List.of(keystore.getKeyDerivation().getMasterFingerprint()), wallet, fullDerivation);
DeviceAddressDialog dlg = new DeviceAddressDialog(wallet, addressDescriptor);
dlg.showAndWait();
});
});
displayAddressService.start();
}
} else {
DeviceAddressDialog dlg = new DeviceAddressDialog(List.of(keystore.getKeyDerivation().getMasterFingerprint()), wallet, fullDerivation);
DeviceAddressDialog dlg = new DeviceAddressDialog(wallet, addressDescriptor);
dlg.showAndWait();
}
}
@@ -261,6 +257,11 @@ public class ReceiveController extends WalletFormController implements Initializ
return duplicateGlyph;
}
@Subscribe
public void walletAddressesChanged(WalletAddressesChangedEvent event) {
displayAddress.setUserData(null);
}
@Subscribe
public void receiveTo(ReceiveToEvent event) {
if(event.getReceiveEntry().getWallet().equals(getWalletForm().getWallet())) {
@@ -11,9 +11,11 @@ import com.sparrowwallet.sparrow.CurrencyRate;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.*;
import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.net.ExchangeSource;
import com.sparrowwallet.sparrow.net.ElectrumServer;
import com.sparrowwallet.sparrow.net.FeeRatesSource;
import com.sparrowwallet.sparrow.net.MempoolRateSize;
import javafx.application.Platform;
import javafx.beans.property.*;
@@ -28,6 +30,7 @@ import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.layout.StackPane;
import javafx.util.StringConverter;
import org.controlsfx.glyphfont.Glyph;
import org.controlsfx.validation.ValidationResult;
import org.controlsfx.validation.ValidationSupport;
import org.controlsfx.validation.Validator;
@@ -47,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;
@@ -79,6 +82,12 @@ public class SendController extends WalletFormController implements Initializabl
@FXML
private CopyableLabel feeRate;
@FXML
private Label feeRatePriority;
@FXML
private Glyph feeRatePriorityGlyph;
@FXML
private TextField fee;
@@ -272,14 +281,14 @@ public class SendController extends WalletFormController implements Initializabl
}
FeeRatesSelection feeRatesSelection = Config.get().getFeeRatesSelection();
feeRatesSelection = (feeRatesSelection == null ? FeeRatesSelection.BLOCK_TARGET : feeRatesSelection);
feeRatesSelection = (feeRatesSelection == null ? FeeRatesSelection.MEMPOOL_SIZE : feeRatesSelection);
setDefaultFeeRate();
updateFeeRateSelection(feeRatesSelection);
feeSelectionToggleGroup.selectToggle(feeRatesSelection == FeeRatesSelection.BLOCK_TARGET ? targetBlocksToggle : mempoolSizeToggle);
feeSelectionToggleGroup.selectedToggleProperty().addListener((observable, oldValue, newValue) -> {
FeeRatesSelection newFeeRatesSelection = (FeeRatesSelection)newValue.getUserData();
Config.get().setFeeRatesSelection(newFeeRatesSelection);
EventManager.get().post(new FeeRatesSelectionChangedEvent(newFeeRatesSelection));
EventManager.get().post(new FeeRatesSelectionChangedEvent(getWalletForm().getWallet(), newFeeRatesSelection));
});
fee.setTextFormatter(new CoinTextFormatter());
@@ -327,6 +336,10 @@ public class SendController extends WalletFormController implements Initializabl
if(userFeeSet.get()) {
setTargetBlocks(getTargetBlocks(feeRate));
setFeeRangeRate(feeRate);
if(walletTransaction.getFee() != getFeeValueSats()) {
setFeeValueSats(walletTransaction.getFee());
}
} else {
setFeeValueSats(walletTransaction.getFee());
}
@@ -346,6 +359,8 @@ public class SendController extends WalletFormController implements Initializabl
});
}
});
addFeeRangeTrackHighlight(0);
}
private void initializeTabHeader(int count) {
@@ -445,7 +460,8 @@ public class SendController extends WalletFormController implements Initializabl
try {
if(paymentTabs.getTabs().size() == 1) {
PaymentController controller = (PaymentController)paymentTabs.getTabs().get(0).getUserData();
updateTransaction(List.of(controller.getPayment(sendAll)));
controller.setSendMax(sendAll);
updateTransaction(List.of(controller.getPayment()));
} else {
updateTransaction(null);
}
@@ -553,10 +569,12 @@ public class SendController extends WalletFormController implements Initializabl
Map<Integer, Double> targetBlocksFeeRates = getTargetBlocksFeeRates();
int maxTargetBlocks = 1;
for(Integer targetBlocks : targetBlocksFeeRates.keySet()) {
maxTargetBlocks = Math.max(maxTargetBlocks, targetBlocks);
Double candidate = targetBlocksFeeRates.get(targetBlocks);
if(Math.round(feeRate) >= Math.round(candidate)) {
return targetBlocks;
if(TARGET_BLOCKS_RANGE.contains(targetBlocks)) {
maxTargetBlocks = Math.max(maxTargetBlocks, targetBlocks);
Double candidate = targetBlocksFeeRates.get(targetBlocks);
if(Math.round(feeRate) >= Math.round(candidate)) {
return targetBlocks;
}
}
}
@@ -618,6 +636,67 @@ public class SendController extends WalletFormController implements Initializabl
private void setFeeRate(Double feeRateAmt) {
feeRate.setText(String.format("%.2f", feeRateAmt) + " sats/vByte");
setFeeRatePriority(feeRateAmt);
}
private void setFeeRatePriority(Double feeRateAmt) {
Map<Integer, Double> targetBlocksFeeRates = getTargetBlocksFeeRates();
Integer targetBlocks = getTargetBlocks(feeRateAmt);
if(targetBlocksFeeRates.get(Integer.MAX_VALUE) != null) {
Double minFeeRate = targetBlocksFeeRates.get(Integer.MAX_VALUE);
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");
feeRatePriorityGlyph.setIcon(FontAwesome5.Glyph.EXCLAMATION_CIRCLE);
return;
}
Double lowestBlocksRate = targetBlocksFeeRates.get(TARGET_BLOCKS_RANGE.get(TARGET_BLOCKS_RANGE.size() - 1));
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");
feeRatePriorityGlyph.setIcon(FontAwesome5.Glyph.PLUS_CIRCLE);
return;
}
}
if(targetBlocks != null) {
if(targetBlocks < FeeRatesSource.BLOCKS_IN_HALF_HOUR) {
Double maxFeeRate = FEE_RATES_RANGE.get(FEE_RATES_RANGE.size() - 1).doubleValue();
Double highestBlocksRate = targetBlocksFeeRates.get(TARGET_BLOCKS_RANGE.get(0));
if(highestBlocksRate < maxFeeRate && feeRateAmt > (highestBlocksRate + ((maxFeeRate - highestBlocksRate) / 10))) {
feeRatePriority.setText("Overpaid");
feeRatePriority.setTooltip(new Tooltip("Transaction fees at this rate are likely higher than necessary"));
feeRatePriorityGlyph.setStyle("-fx-text-fill: #c8416499");
feeRatePriorityGlyph.setIcon(FontAwesome5.Glyph.EXCLAMATION_CIRCLE);
} else {
feeRatePriority.setText("High Priority");
feeRatePriority.setTooltip(new Tooltip("Typically confirms within minutes"));
feeRatePriorityGlyph.setStyle("-fx-text-fill: #c8416499");
feeRatePriorityGlyph.setIcon(FontAwesome5.Glyph.CIRCLE);
}
} else if(targetBlocks < FeeRatesSource.BLOCKS_IN_HOUR) {
feeRatePriority.setText("Medium Priority");
feeRatePriority.setTooltip(new Tooltip("Typically confirms within an hour or two"));
feeRatePriorityGlyph.setStyle("-fx-text-fill: #fba71b99");
feeRatePriorityGlyph.setIcon(FontAwesome5.Glyph.CIRCLE);
} else {
feeRatePriority.setText("Low Priority");
feeRatePriority.setTooltip(new Tooltip("Typically confirms in a day or longer"));
feeRatePriorityGlyph.setStyle("-fx-text-fill: #41a9c999");
feeRatePriorityGlyph.setIcon(FontAwesome5.Glyph.CIRCLE);
}
}
}
private boolean isPayjoinTx() {
if(walletTransactionProperty.get() != null) {
return walletTransactionProperty.get().getPayments().stream().anyMatch(payment -> AppServices.getPayjoinURI(payment.getAddress()) != null);
}
return false;
}
private Node getSliderThumb() {
@@ -630,6 +709,47 @@ public class SendController extends WalletFormController implements Initializabl
}
}
private void addFeeRangeTrackHighlight(int count) {
Platform.runLater(() -> {
Node track = feeRange.lookup(".track");
if(track != null) {
Map<Integer, Double> targetBlocksFeeRates = getTargetBlocksFeeRates();
String highlight = "";
if(targetBlocksFeeRates.get(Integer.MAX_VALUE) != null) {
highlight += "#a0a1a766 " + getPercentageOfFeeRange(targetBlocksFeeRates.get(Integer.MAX_VALUE)) + "%, ";
}
highlight += "#41a9c966 " + getPercentageOfFeeRange(targetBlocksFeeRates, FeeRatesSource.BLOCKS_IN_TWO_HOURS - 1) + "%, ";
highlight += "#fba71b66 " + getPercentageOfFeeRange(targetBlocksFeeRates, FeeRatesSource.BLOCKS_IN_HOUR - 1) + "%, ";
highlight += "#c8416466 " + getPercentageOfFeeRange(targetBlocksFeeRates, FeeRatesSource.BLOCKS_IN_HALF_HOUR - 1) + "%";
track.setStyle("-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, " + highlight + ")");
} else if(count < 20) {
addFeeRangeTrackHighlight(count+1);
}
});
}
private int getPercentageOfFeeRange(Map<Integer, Double> targetBlocksFeeRates, Integer minTargetBlocks) {
List<Integer> rates = new ArrayList<>(targetBlocksFeeRates.keySet());
Collections.reverse(rates);
for(Integer targetBlocks : rates) {
if(targetBlocks < minTargetBlocks) {
return getPercentageOfFeeRange(targetBlocksFeeRates.get(targetBlocks));
}
}
return 100;
}
private int getPercentageOfFeeRange(Double feeRate) {
double index = Math.log(feeRate) / Math.log(2);
return (int)Math.round(index * 10.0);
}
private void updateMaxClearButtons(UtxoSelector utxoSelector, UtxoFilter utxoFilter) {
if(utxoSelector instanceof PresetUtxoSelector) {
PresetUtxoSelector presetUtxoSelector = (PresetUtxoSelector)utxoSelector;
@@ -726,12 +846,16 @@ public class SendController extends WalletFormController implements Initializabl
public void createTransaction(ActionEvent event) {
if(log.isDebugEnabled()) {
Map<WalletNode, String> nodeHashes = walletTransactionProperty.get().getSelectedUtxos().values().stream().collect(Collectors.toMap(Function.identity(), node -> ElectrumServer.getScriptHash(walletForm.getWallet(), node)));
Map<WalletNode, String> changeHash = Collections.emptyMap();
if(walletTransactionProperty.get().getChangeNode() != null) {
changeHash = Map.of(walletTransactionProperty.get().getChangeNode(), ElectrumServer.getScriptHash(walletForm.getWallet(), walletTransactionProperty.get().getChangeNode()));
Map<WalletNode, List<String>> inputHashes = new LinkedHashMap<>();
for(WalletNode node : walletTransactionProperty.get().getSelectedUtxos().values()) {
List<String> nodeHashes = inputHashes.computeIfAbsent(node, k -> new ArrayList<>());
nodeHashes.add(ElectrumServer.getScriptHash(walletForm.getWallet(), node));
}
log.debug("Creating tx " + walletTransactionProperty.get().getTransaction().getTxId() + ", expecting notifications for \ninputs \n" + nodeHashes + " and \nchange \n" + changeHash);
Map<WalletNode, List<String>> changeHash = Collections.emptyMap();
if(walletTransactionProperty.get().getChangeNode() != null) {
changeHash = Map.of(walletTransactionProperty.get().getChangeNode(), List.of(ElectrumServer.getScriptHash(walletForm.getWallet(), walletTransactionProperty.get().getChangeNode())));
}
log.debug("Creating tx " + walletTransactionProperty.get().getTransaction().getTxId() + ", expecting notifications for \ninputs \n" + inputHashes + " and \nchange \n" + changeHash);
}
addWalletTransactionNodes();
@@ -805,12 +929,17 @@ public class SendController extends WalletFormController implements Initializabl
mempoolSizeFeeRatesChart.update(getMempoolHistogram());
if(targetBlocksField.isVisible()) {
setFeeRate(event.getTargetBlockFeeRates().get(getTargetBlocks()));
} else {
setFeeRatePriority(getFeeRangeRate());
}
addFeeRangeTrackHighlight(0);
}
@Subscribe
public void feeRateSelectionChanged(FeeRatesSelectionChangedEvent event) {
updateFeeRateSelection(event.getFeeRateSelection());
if(event.getWallet() == getWalletForm().getWallet()) {
updateFeeRateSelection(event.getFeeRateSelection());
}
}
@Subscribe
@@ -65,6 +65,8 @@ public class SettingsController extends WalletFormController implements Initiali
private TabPane keystoreTabs;
@FXML Button export;
@FXML
private Button apply;
@@ -183,7 +185,7 @@ public class SettingsController extends WalletFormController implements Initiali
apply.setOnAction(event -> {
revert.setDisable(true);
apply.setDisable(true);
saveWallet(false);
saveWallet(false, false);
});
setFieldsFromWallet(walletForm.getWallet());
@@ -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());
@@ -303,8 +306,18 @@ public class SettingsController extends WalletFormController implements Initiali
}
public void showAdvanced(ActionEvent event) {
AdvancedDialog advancedDialog = new AdvancedDialog(walletForm.getWallet());
advancedDialog.showAndWait();
AdvancedDialog advancedDialog = new AdvancedDialog(walletForm);
Optional<Boolean> optApply = advancedDialog.showAndWait();
if(optApply.isPresent() && optApply.get() && walletForm.getWallet().isValid()) {
revert.setDisable(true);
apply.setDisable(true);
saveWallet(false, true);
}
}
public void exportWallet(ActionEvent event) {
WalletExportDialog dlg = new WalletExportDialog(walletForm.getWallet());
dlg.showAndWait();
}
@Override
@@ -336,11 +349,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());
}
@@ -355,7 +376,7 @@ public class SettingsController extends WalletFormController implements Initiali
}
}
private void saveWallet(boolean changePassword) {
private void saveWallet(boolean changePassword, boolean suggestChangePassword) {
ECKey existingPubKey = walletForm.getStorage().getEncryptionPubKey();
WalletPasswordDialog.PasswordRequirement requirement;
@@ -380,7 +401,7 @@ public class SettingsController extends WalletFormController implements Initiali
}
}
WalletPasswordDialog dlg = new WalletPasswordDialog(requirement);
WalletPasswordDialog dlg = new WalletPasswordDialog(null, requirement, suggestChangePassword);
Optional<SecureString> password = dlg.showAndWait();
if(password.isPresent()) {
if(dlg.isBackupExisting()) {
@@ -399,9 +420,7 @@ public class SettingsController extends WalletFormController implements Initiali
try {
walletForm.getStorage().setEncryptionPubKey(Storage.NO_PASSWORD_KEY);
walletForm.saveAndRefresh();
if(requirement == WalletPasswordDialog.PasswordRequirement.UPDATE_NEW) {
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());
@@ -425,20 +444,27 @@ public class SettingsController extends WalletFormController implements Initiali
return;
}
key = new Key(encryptionFullKey.getPrivKeyBytes(), walletForm.getStorage().getKeyDeriver().getSalt(), EncryptionType.Deriver.ARGON2);
if(dlg.isChangePassword()) {
if(dlg.isDeleteBackups()) {
walletForm.deleteBackups();
}
walletForm.getStorage().setEncryptionPubKey(null);
saveWallet(true);
walletForm.getWallet().decrypt(key);
saveWallet(true, false);
return;
}
key = new Key(encryptionFullKey.getPrivKeyBytes(), walletForm.getStorage().getKeyDeriver().getSalt(), EncryptionType.Deriver.ARGON2);
walletForm.getWallet().encrypt(key);
if(dlg.isDeleteBackups()) {
walletForm.deleteBackups();
}
walletForm.getWallet().encrypt(key);
walletForm.getStorage().setEncryptionPubKey(encryptionPubKey);
walletForm.saveAndRefresh();
if(requirement == WalletPasswordDialog.PasswordRequirement.UPDATE_NEW) {
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;
@@ -17,7 +19,7 @@ public class SettingsWalletForm extends WalletForm {
private Wallet walletCopy;
public SettingsWalletForm(Storage storage, Wallet currentWallet) {
super(storage, currentWallet);
super(storage, currentWallet, null, false);
this.walletCopy = currentWallet.copy();
}
@@ -38,10 +40,13 @@ public class SettingsWalletForm extends WalletForm {
@Override
public void saveAndRefresh() throws IOException {
Wallet pastWallet = wallet.copy();
Wallet pastWallet = null;
boolean refreshAll = isRefreshNecessary(wallet, walletCopy);
if(refreshAll) {
pastWallet = wallet.copy();
save(); //Save here for the temp backup in case password has been changed
getStorage().backupTempWallet();
walletCopy.clearNodes();
}
@@ -49,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()));
}
}
@@ -88,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;
}
@@ -107,4 +118,8 @@ public class SettingsWalletForm extends WalletForm {
return false;
}
private Integer getNumSignaturesRequired(Policy policy) {
return policy == null ? null : policy.getNumSignaturesRequired();
}
}
@@ -114,7 +114,7 @@ public class TransactionsController extends WalletFormController implements Init
writer.writeRecord(new String[] {"Date", "Label", "Value", "Balance"});
for(Entry entry : walletTransactionsEntry.getChildren()) {
TransactionEntry txEntry = (TransactionEntry)entry;
writer.write(EntryCell.DATE_FORMAT.format(txEntry.getBlockTransaction().getDate()));
writer.write(txEntry.getBlockTransaction().getDate() == null ? "Unconfirmed" : EntryCell.DATE_FORMAT.format(txEntry.getBlockTransaction().getDate()));
writer.write(txEntry.getLabel());
writer.write(getCoinValue(txEntry.getValue()));
writer.write(getCoinValue(txEntry.getBalance()));
@@ -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());
}
@@ -34,10 +34,20 @@ public class WalletForm {
private final List<NodeEntry> accountEntries = new ArrayList<>();
private final List<Set<WalletNode>> walletTransactionNodes = new ArrayList<>();
public WalletForm(Storage storage, Wallet currentWallet) {
public WalletForm(Storage storage, Wallet currentWallet, Wallet backupWallet) {
this(storage, currentWallet, backupWallet, true);
}
public WalletForm(Storage storage, Wallet currentWallet, Wallet backupWallet, boolean refreshHistory) {
this.storage = storage;
this.wallet = currentWallet;
refreshHistory(AppServices.getCurrentBlockHeight(), null);
//Unencrypted wallets load before isConnected is true, waiting for the ConnectionEvent to refresh history - save the backup for this event
savedPastWallet = backupWallet;
if(refreshHistory) {
refreshHistory(AppServices.getCurrentBlockHeight(), backupWallet);
}
}
public Wallet getWallet() {
@@ -66,6 +76,7 @@ public class WalletForm {
public void saveAndRefresh() throws IOException {
Wallet pastWallet = wallet.copy();
storage.backupTempWallet();
wallet.clearHistory();
save();
refreshHistory(AppServices.getCurrentBlockHeight(), pastWallet);
@@ -75,6 +86,10 @@ public class WalletForm {
storage.backupWallet();
}
public void deleteBackups() {
storage.deleteBackups();
}
public void refreshHistory(Integer blockHeight, Wallet pastWallet) {
refreshHistory(blockHeight, pastWallet, null);
}
@@ -85,14 +100,15 @@ public class WalletForm {
log.debug(node == null ? wallet.getName() + " refreshing full wallet history" : wallet.getName() + " requesting node wallet history for " + node.getDerivationPath());
ElectrumServer.TransactionHistoryService historyService = new ElectrumServer.TransactionHistoryService(wallet, getWalletTransactionNodes(node));
historyService.setOnSucceeded(workerStateEvent -> {
EventManager.get().post(new WalletHistoryStatusEvent(wallet, false));
EventManager.get().post(new WalletHistoryFinishedEvent(wallet));
updateWallet(blockHeight, pastWallet, previousWallet);
});
historyService.setOnFailed(workerStateEvent -> {
log.error("Error retrieving wallet history", workerStateEvent.getSource().getException());
EventManager.get().post(new WalletHistoryStatusEvent(wallet, workerStateEvent.getSource().getException().getMessage()));
EventManager.get().post(new WalletHistoryFailedEvent(wallet, workerStateEvent.getSource().getException()));
});
EventManager.get().post(new WalletHistoryStatusEvent(wallet, true));
EventManager.get().post(new WalletHistoryStartedEvent(wallet, node));
historyService.start();
}
}
@@ -102,31 +118,36 @@ public class WalletForm {
wallet.setStoredBlockHeight(blockHeight);
}
boolean labelsChanged = false;
if(pastWallet != null) {
copyLabels(pastWallet);
labelsChanged = copyLabels(pastWallet);
}
notifyIfChanged(blockHeight, previousWallet);
notifyIfChanged(blockHeight, previousWallet, labelsChanged);
}
private void copyLabels(Wallet pastWallet) {
wallet.getNode(KeyPurpose.RECEIVE).copyLabels(pastWallet.getNode(KeyPurpose.RECEIVE));
wallet.getNode(KeyPurpose.CHANGE).copyLabels(pastWallet.getNode(KeyPurpose.CHANGE));
private boolean copyLabels(Wallet pastWallet) {
boolean changed = wallet.getNode(KeyPurpose.RECEIVE).copyLabels(pastWallet.getNode(KeyPurpose.RECEIVE));
changed |= wallet.getNode(KeyPurpose.CHANGE).copyLabels(pastWallet.getNode(KeyPurpose.CHANGE));
for(Map.Entry<Sha256Hash, BlockTransaction> txEntry : wallet.getTransactions().entrySet()) {
BlockTransaction pastBlockTransaction = pastWallet.getTransactions().get(txEntry.getKey());
if(pastBlockTransaction != null && txEntry.getValue() != null && txEntry.getValue().getLabel() == null && pastBlockTransaction.getLabel() != null) {
txEntry.getValue().setLabel(pastBlockTransaction.getLabel());
changed = true;
}
}
storage.deleteBackups(Storage.TEMP_BACKUP_EXTENSION);
return changed;
}
private void notifyIfChanged(Integer blockHeight, Wallet previousWallet) {
private void notifyIfChanged(Integer blockHeight, Wallet previousWallet, boolean labelsChanged) {
List<WalletNode> historyChangedNodes = new ArrayList<>();
historyChangedNodes.addAll(getHistoryChangedNodes(previousWallet.getNode(KeyPurpose.RECEIVE).getChildren(), wallet.getNode(KeyPurpose.RECEIVE).getChildren()));
historyChangedNodes.addAll(getHistoryChangedNodes(previousWallet.getNode(KeyPurpose.CHANGE).getChildren(), wallet.getNode(KeyPurpose.CHANGE).getChildren()));
boolean changed = false;
boolean changed = labelsChanged;
if(!historyChangedNodes.isEmpty()) {
Platform.runLater(() -> EventManager.get().post(new WalletHistoryChangedEvent(wallet, storage, historyChangedNodes)));
changed = true;
@@ -244,18 +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
//The savedPastWallet variable can be removed once bwt supports dynamic loading of wallets without needing to disconnect/reconnect
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
View File
@@ -28,4 +28,5 @@ open module com.sparrowwallet.sparrow {
requires bwt.jni;
requires jtorctl;
requires javacsv;
requires jul.to.slf4j;
}
@@ -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;
@@ -88,8 +88,12 @@
</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"/>
<MenuItem mnemonicParsing="false" text="Show Online Documentation" onAction="#showDocumentation"/>
<MenuItem mnemonicParsing="false" text="Show Log File" onAction="#showLogFile"/>
<MenuItem styleClass="osxHide" mnemonicParsing="false" text="About Sparrow" onAction="#showAbout"/>
</Menu>
</menus>
@@ -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;
}
@@ -163,4 +167,8 @@
.box-overlay {
-fx-background-color: -fx-box-border, derive(-fx-background, 10%);
-fx-effect: dropshadow(three-pass-box, #a0a1a7, 14, 0, 0, 0);
}
.alert .content.label {
-fx-padding: 20px 20px 20px 20px;
}
@@ -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."/>
@@ -102,7 +102,7 @@
</Field>
<Field text="Proxy URL:">
<TextField fx:id="publicProxyHost" />
<TextField fx:id="publicProxyPort" prefWidth="80" />
<TextField fx:id="publicProxyPort" maxWidth="100" />
</Field>
</Fieldset>
</Form>
@@ -111,7 +111,7 @@
<Fieldset inputGrow="SOMETIMES" text="Bitcoin Core RPC">
<Field text="URL:">
<TextField fx:id="coreHost" promptText="e.g. 127.0.0.1"/>
<TextField fx:id="corePort" promptText="e.g. 8332" prefWidth="80" />
<TextField fx:id="corePort" promptText="e.g. 8332" maxWidth="100" />
</Field>
<Field text="Authentication:">
<SegmentedButton>
@@ -156,8 +156,8 @@
<Form fx:id="electrumForm" GridPane.columnIndex="0" GridPane.rowIndex="1">
<Fieldset inputGrow="SOMETIMES" text="Private Electrum Server">
<Field text="URL:">
<TextField fx:id="electrumHost" promptText="e.g. 127.0.0.1"/>
<TextField fx:id="electrumPort" promptText="e.g. 50002" prefWidth="80" />
<TextField fx:id="electrumHost" promptText="e.g. 127.0.0.1 or Tor hostname (.onion)"/>
<TextField fx:id="electrumPort" promptText="e.g. 50002" maxWidth="100" />
</Field>
<Field text="Use SSL:">
<UnlabeledToggleSwitch fx:id="electrumUseSsl"/>
@@ -175,7 +175,7 @@
</Field>
<Field text="Proxy URL:">
<TextField fx:id="proxyHost" />
<TextField fx:id="proxyPort" prefWidth="80" />
<TextField fx:id="proxyPort" maxWidth="100" />
</Field>
</Fieldset>
</Form>
@@ -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." />
@@ -49,7 +49,7 @@
<Label style="-fx-pref-width: 15" />
<FiatLabel fx:id="fiatAmount" />
<Region style="-fx-pref-width: 20" />
<Button fx:id="maxButton" text="Max" onAction="#setMaxInput" />
<ToggleButton fx:id="maxButton" text="Max" onAction="#setMaxInput" />
</Field>
</Fieldset>
</Form>
@@ -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%);
}
@@ -91,6 +91,12 @@
</Field>
<Field fx:id="feeRateField" text="Rate:">
<CopyableLabel fx:id="feeRate" />
<Region HBox.hgrow="ALWAYS" />
<Label fx:id="feeRatePriority" graphicTextGap="5" contentDisplay="RIGHT">
<graphic>
<Glyph fx:id="feeRatePriorityGlyph" fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="CIRCLE" />
</graphic>
</Label>
</Field>
<Field text="Fee:">
<TextField fx:id="fee" styleClass="amount-field"/>
@@ -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" />
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

+13
View File
@@ -4,6 +4,11 @@
<logger name="com.github.sarxos.webcam.Webcam" level="OFF"/>
<logger name="com.github.sarxos.webcam.ds.cgt.WebcamOpenTask" level="OFF"/>
<logger name="com.github.sarxos.webcam.ds.cgt.WebcamCloseTask" level="OFF"/>
<logger name="javafx.css" level="ERROR"/>
<logger name="javafx.scene.focus" level="INFO"/>
<logger name="sun.net.www.protocol.http.HttpURLConnection" level="INFO" />
<contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator"/>
<define name="appDir" class="com.sparrowwallet.drongo.PropertyDefiner">
<application>sparrow</application>
@@ -22,6 +27,14 @@
</encoder>
</appender>
<appender name="APPLICATION_TOR" class="com.sparrowwallet.drongo.ApplicationAppender">
<callback>com.sparrowwallet.sparrow.TorLogHandler</callback>
</appender>
<logger name="org.berndpruenster.netlayer.tor" level="debug" additivity="false">
<appender-ref ref="APPLICATION_TOR" />
</logger>
<root level="info">
<appender-ref ref="FILE" />
<appender-ref ref="STDOUT" />
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.