mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2026-07-31 03:56:16 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9872fc9221 | ||
|
|
84d2db427c | ||
|
|
cfbf9187e6 | ||
|
|
5478ac05b8 | ||
|
|
836bd6bc40 | ||
|
|
1bab07fc5f | ||
|
|
4b3de044c3 | ||
|
|
f2c2fc6071 | ||
|
|
4dd52eb9dd | ||
|
|
e45fdc72a0 | ||
|
|
d67dd515c4 | ||
|
|
ff962aa5f0 | ||
|
|
c0e44b2c55 | ||
|
|
f0c239d625 | ||
|
|
58a31f435e | ||
|
|
c49439d541 | ||
|
|
0070cbfb1e | ||
|
|
7ed75fc83d | ||
|
|
13d701b0a7 | ||
|
|
07ed9c857b | ||
|
|
32ab53ba03 | ||
|
|
68344e981c | ||
|
|
10c1574a4a | ||
|
|
3a853e2ab1 | ||
|
|
0f639dc2c2 | ||
|
|
a801773545 | ||
|
|
ffbf4bb0db | ||
|
|
11e46155a1 | ||
|
|
f369b3ad00 | ||
|
|
6068666415 | ||
|
|
6a58e8a799 | ||
|
|
3bd2f69157 |
@@ -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]
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ plugins {
|
||||
id 'org.beryx.jlink' version '2.22.0'
|
||||
}
|
||||
|
||||
def sparrowVersion = '1.2.0'
|
||||
def sparrowVersion = '1.3.0'
|
||||
def os = org.gradle.internal.os.OperatingSystem.current()
|
||||
def osName = os.getFamilyName()
|
||||
if(os.macOsX) {
|
||||
|
||||
+1
-1
Submodule drongo updated: 08acfe5ba1...e974c3f422
@@ -21,7 +21,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.2.0</string>
|
||||
<string>1.3.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories -->
|
||||
|
||||
@@ -24,6 +24,7 @@ import com.sparrowwallet.sparrow.event.*;
|
||||
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;
|
||||
@@ -119,6 +120,8 @@ 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);
|
||||
@@ -260,6 +263,28 @@ public class AppController implements Initializable {
|
||||
}
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
public void showAbout(ActionEvent event) {
|
||||
Stage aboutStage = getAboutStage();
|
||||
aboutStage.show();
|
||||
@@ -569,7 +594,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 +619,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 +637,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);
|
||||
@@ -779,7 +807,7 @@ public class AppController implements Initializable {
|
||||
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 +820,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 +884,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()));
|
||||
}
|
||||
}
|
||||
|
||||
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 +905,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")) {
|
||||
@@ -901,7 +930,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);
|
||||
|
||||
@@ -1343,6 +1372,42 @@ 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...")) {
|
||||
statusBar.setText("");
|
||||
}
|
||||
if(statusTimeline == null || statusTimeline.getStatus() != Animation.Status.RUNNING) {
|
||||
statusBar.setProgress(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletHistoryStarted(WalletHistoryStartedEvent event) {
|
||||
if(AppServices.isConnected() && getOpenWallets().containsKey(event.getWallet())) {
|
||||
statusUpdated(new StatusEvent("Loading transactions...", 120));
|
||||
if(statusTimeline == null || statusTimeline.getStatus() != Animation.Status.RUNNING) {
|
||||
statusBar.setProgress(-1);
|
||||
loadingWallets.add(event.getWallet());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletHistoryFinished(WalletHistoryFinishedEvent event) {
|
||||
if(getOpenWallets().containsKey(event.getWallet())) {
|
||||
if(statusBar.getText().equals("Loading transactions...")) {
|
||||
statusBar.setText("");
|
||||
}
|
||||
if(statusTimeline == null || statusTimeline.getStatus() != Animation.Status.RUNNING) {
|
||||
statusBar.setProgress(0);
|
||||
}
|
||||
loadingWallets.remove(event.getWallet());
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void bwtBootStatus(BwtBootStatusEvent event) {
|
||||
serverToggle.setDisable(true);
|
||||
|
||||
@@ -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;
|
||||
@@ -27,7 +28,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.0";
|
||||
public static final String APP_HOME_PROPERTY = "sparrow.home";
|
||||
public static final String NETWORK_ENV_PROPERTY = "SPARROW_NETWORK";
|
||||
|
||||
@@ -119,6 +120,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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 WalletSettingsChangedEvent(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,6 +2,7 @@ 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.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
@@ -15,7 +16,6 @@ import com.sparrowwallet.sparrow.event.MessageSignedEvent;
|
||||
import com.sparrowwallet.sparrow.event.PSBTSignedEvent;
|
||||
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 +25,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;
|
||||
@@ -36,6 +35,7 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
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;
|
||||
@@ -233,7 +238,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 +376,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 +442,18 @@ 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));
|
||||
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 +481,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));
|
||||
|
||||
@@ -183,7 +183,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);
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -22,17 +22,24 @@ public class WalletPasswordDialog extends Dialog<SecureString> {
|
||||
private final CustomPasswordField passwordConfirm;
|
||||
private final CheckBox backupExisting;
|
||||
private final CheckBox changePassword;
|
||||
private final CheckBox deleteBackups;
|
||||
private boolean addingPassword;
|
||||
|
||||
public WalletPasswordDialog(PasswordRequirement requirement) {
|
||||
this(null, requirement);
|
||||
}
|
||||
|
||||
public WalletPasswordDialog(String walletName, PasswordRequirement requirement) {
|
||||
this(null, 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 +59,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 +102,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,13 +122,17 @@ 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"),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
public class WalletHistoryFinishedEvent extends WalletHistoryStatusEvent {
|
||||
public WalletHistoryFinishedEvent(Wallet wallet) {
|
||||
super(wallet, false);
|
||||
}
|
||||
|
||||
public WalletHistoryFinishedEvent(Wallet wallet, String errorMessage) {
|
||||
super(wallet, errorMessage);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
public class WalletHistoryStartedEvent extends WalletHistoryStatusEvent {
|
||||
public WalletHistoryStartedEvent(Wallet wallet) {
|
||||
super(wallet, true);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -80,7 +80,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
|
||||
}
|
||||
|
||||
@@ -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-rc.2";
|
||||
|
||||
private static boolean isPromptActive = false;
|
||||
|
||||
@@ -42,11 +43,17 @@ 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) {
|
||||
throw new ImportException(e);
|
||||
} finally {
|
||||
isPromptActive = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,32 +91,33 @@ 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";
|
||||
}
|
||||
//Remove replace once HWI-2.0.0 is released - see https://github.com/bitcoin-core/HWI/pull/488
|
||||
String descriptor = outputDescriptor.toString().replace("sortedmulti", "multi");
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -61,16 +64,20 @@ public enum FeeRatesSource {
|
||||
Gson gson = new Gson();
|
||||
ThreeTierRates threeTierRates = gson.fromJson(reader, ThreeTierRates.class);
|
||||
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 {
|
||||
blockTargetFeeRates.put(blockTarget, defaultblockTargetFeeRates.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 +105,6 @@ public enum FeeRatesSource {
|
||||
Double fastestFee;
|
||||
Double halfHourFee;
|
||||
Double hourFee;
|
||||
Double minimumFee;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
+39
-5
@@ -149,6 +149,8 @@ public class ServerPreferencesController extends PreferencesDetailController {
|
||||
|
||||
private ElectrumServer.ConnectionService connectionService;
|
||||
|
||||
private Boolean useSslOriginal;
|
||||
|
||||
@Override
|
||||
public void initializeView(Config config) {
|
||||
EventManager.get().register(this);
|
||||
@@ -412,17 +414,15 @@ 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());
|
||||
});
|
||||
|
||||
@@ -439,6 +439,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();
|
||||
@@ -450,6 +452,29 @@ public class ServerPreferencesController extends PreferencesDetailController {
|
||||
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;
|
||||
}
|
||||
}
|
||||
});
|
||||
connectionService.start();
|
||||
}
|
||||
@@ -739,4 +764,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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 walletSettingsChanged(WalletSettingsChangedEvent 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;
|
||||
@@ -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,59 @@ 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))) {
|
||||
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 Node getSliderThumb() {
|
||||
@@ -630,6 +701,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 +838,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();
|
||||
@@ -806,11 +922,14 @@ public class SendController extends WalletFormController implements Initializabl
|
||||
if(targetBlocksField.isVisible()) {
|
||||
setFeeRate(event.getTargetBlockFeeRates().get(getTargetBlocks()));
|
||||
}
|
||||
addFeeRangeTrackHighlight(0);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void feeRateSelectionChanged(FeeRatesSelectionChangedEvent event) {
|
||||
updateFeeRateSelection(event.getFeeRateSelection());
|
||||
if(event.getWallet() == getWalletForm().getWallet()) {
|
||||
updateFeeRateSelection(event.getFeeRateSelection());
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
|
||||
@@ -183,7 +183,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());
|
||||
@@ -303,8 +303,13 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -355,7 +360,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 +385,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,7 +404,7 @@ public class SettingsController extends WalletFormController implements Initiali
|
||||
try {
|
||||
walletForm.getStorage().setEncryptionPubKey(Storage.NO_PASSWORD_KEY);
|
||||
walletForm.saveAndRefresh();
|
||||
if(requirement == WalletPasswordDialog.PasswordRequirement.UPDATE_NEW) {
|
||||
if(requirement == WalletPasswordDialog.PasswordRequirement.UPDATE_NEW || requirement == WalletPasswordDialog.PasswordRequirement.UPDATE_CHANGE) {
|
||||
EventManager.get().post(new RequestOpenWalletsEvent());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
@@ -425,18 +430,28 @@ 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) {
|
||||
|
||||
if(requirement == WalletPasswordDialog.PasswordRequirement.UPDATE_NEW || requirement == WalletPasswordDialog.PasswordRequirement.UPDATE_EMPTY) {
|
||||
EventManager.get().post(new RequestOpenWalletsEvent());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
|
||||
@@ -17,7 +17,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 +38,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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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()));
|
||||
|
||||
@@ -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,19 @@ 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 WalletHistoryFinishedEvent(wallet, workerStateEvent.getSource().getException().getMessage()));
|
||||
});
|
||||
EventManager.get().post(new WalletHistoryStatusEvent(wallet, true));
|
||||
if(node == null && wallet.getTransactions().isEmpty()) {
|
||||
EventManager.get().post(new WalletHistoryStartedEvent(wallet));
|
||||
} else {
|
||||
EventManager.get().post(new WalletHistoryStatusEvent(wallet, true));
|
||||
}
|
||||
|
||||
historyService.start();
|
||||
}
|
||||
}
|
||||
@@ -102,31 +122,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;
|
||||
@@ -250,7 +275,6 @@ public class WalletForm {
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -90,6 +90,9 @@
|
||||
<MenuItem mnemonicParsing="false" text="Sign/Verify Message" accelerator="Shortcut+M" onAction="#signVerifyMessage"/>
|
||||
</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>
|
||||
|
||||
@@ -163,4 +163,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;
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -22,6 +22,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.
BIN
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user