Compare commits

..
24 Commits
Author SHA1 Message Date
Craig Raw 9a395f5dd1 v0.9.4 2020-09-21 10:00:35 +02:00
Craig Raw a99114cc6f dynamic blockchain form updating, minor tweaks 2020-09-21 09:06:01 +02:00
Craig Raw ea36c63ebb version check: test against predefined address 2020-09-20 15:09:54 +02:00
Craig Raw 77d3c848f9 add dark theme 2020-09-20 15:04:53 +02:00
Craig Raw f22312e04f add software update check 2020-09-18 15:48:05 +02:00
Craig Raw b9db4421df import transaction labels from electrum 2020-09-18 12:53:05 +02:00
Craig Raw b7aa73e99f show wallet loading progress 2020-09-18 10:50:41 +02:00
Craig Raw 4bad46c9e3 optimize electrum server queries 2020-09-17 15:10:56 +02:00
Craig Raw 084a3c76af start with simple RPC for maximum compatibility 2020-09-15 12:07:37 +02:00
Craig Raw 2e0ca1b4fa add built in tor 2020-09-14 10:51:38 +02:00
Craig Raw 4bc446724d fix keepkey hww 2020-09-10 11:40:21 +02:00
Craig Raw 0c56f5a9a1 add change password functionality 2020-09-09 16:15:33 +02:00
Craig Raw 38b27bb091 configurable wallet gap limit 2020-09-09 14:56:50 +02:00
Craig Raw b8c3bf1bea improve wallet descriptor and add output descriptor retrieval 2020-09-09 11:18:13 +02:00
Craig Raw e65f1ef3cc export specter with checksum 2020-09-08 12:42:15 +02:00
Craig Raw f25d2618b6 descriptor change multi to sortedmulti 2020-09-08 08:31:42 +02:00
Craig Raw 0adf94bf62 initial specter wallet import and export functionality 2020-09-07 17:23:12 +02:00
Craig Raw 7d7c5e5064 Support replacing existing keystore with watch only keystore 2020-09-04 13:00:40 +02:00
Craig Raw 10e4d8843d change bip39 confirmation wording 2020-09-03 20:54:46 +02:00
Craig Raw 6e10784e49 support usb message signing 2020-09-03 20:46:44 +02:00
Craig Raw 885efd7985 support bip 137 for message signing, but handle electrum approach as well when verifying 2020-09-03 19:26:54 +02:00
Craig Raw ec9534f8ae version 0.9.3 release 2020-09-03 13:41:12 +02:00
Craig Raw 6c90a7e0ba add message signing/verifying functionality 2020-09-03 13:36:43 +02:00
Craig Raw 9dd52f120f add ur toString 2020-09-03 09:38:16 +02:00
97 changed files with 2249 additions and 312 deletions
+11 -6
View File
@@ -5,11 +5,11 @@ plugins {
id 'org.beryx.jlink' version '2.17.4'
}
def sparrowVersion = '0.9.2'
def sparrowVersion = '0.9.4'
def os = org.gradle.internal.os.OperatingSystem.current()
def osName = os.getFamilyName()
if(os.macOsX) {
osName = "mac"
osName = "osx"
}
group "com.sparrowwallet"
@@ -19,6 +19,7 @@ repositories {
mavenCentral()
maven { url 'https://oss.sonatype.org/content/groups/public' }
maven { url 'https://mymavenrepo.com/repo/29EACwkkGcoOKnbx3bxN/' }
maven { url 'https://jitpack.io' }
}
tasks.withType(AbstractArchiveTask) {
@@ -54,6 +55,7 @@ dependencies {
implementation('com.github.sarxos:webcam-capture:0.3.13-SNAPSHOT') {
exclude group: 'com.nativelibs4java', module: 'bridj'
}
implementation("com.sparrowwallet:netlayer-jpms-${osName}:0.6.8")
implementation('de.codecentric.centerdevice:centerdevice-nsmenufx:2.1.7')
implementation('org.controlsfx:controlsfx:11.0.2' ) {
exclude group: 'org.openjfx', module: 'javafx-base'
@@ -80,8 +82,8 @@ compileJava {
processResources {
doLast {
delete fileTree("$buildDir/resources/main/external").matching {
exclude "$osName/**"
delete fileTree("$buildDir/resources/main/native").matching {
exclude "${osName}/**"
}
}
}
@@ -101,7 +103,8 @@ run {
"--add-opens=javafx.controls/com.sun.javafx.scene.control=centerdevice.nsmenufx",
"--add-opens=javafx.graphics/com.sun.javafx.menu=centerdevice.nsmenufx",
"--add-opens=javafx.graphics/com.sun.glass.ui=com.sparrowwallet.sparrow",
"--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow"]
"--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow",
"--add-opens=java.base/java.net=com.sparrowwallet.sparrow"]
if(os.macOsX) {
applicationDefaultJvmArgs += ["-Xdock:name=Sparrow", "-Xdock:icon=/Users/scy/git/sparrow/src/main/resources/sparrow.png",
@@ -118,6 +121,7 @@ jlink {
requires 'javafx.base'
requires 'com.fasterxml.jackson.databind'
requires 'jdk.crypto.cryptoki'
requires 'java.management'
}
options = ['--strip-debug', '--compress', '2', '--no-header-files', '--no-man-pages', '--ignore-signing-information', '--exclude-files', '**.png']
@@ -139,6 +143,7 @@ jlink {
"--add-opens=javafx.graphics/com.sun.glass.ui=com.sparrowwallet.sparrow",
"--add-opens=javafx.graphics/com.sun.glass.ui.mac=com.sparrowwallet.merged.module",
"--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow",
"--add-opens=java.base/java.net=com.sparrowwallet.sparrow",
"--add-reads=com.sparrowwallet.merged.module=java.desktop"]
}
addExtraDependencies("javafx")
@@ -160,7 +165,7 @@ jlink {
}
if(os.macOsX) {
installerOptions += ['--mac-sign', '--mac-signing-key-user-name', 'Craig Raw (UPLVMSK9D7)']
imageOptions += ['--icon', 'src/main/deploy/package/mac/sparrow.icns', '--resource-dir', 'src/main/deploy/package/mac/']
imageOptions += ['--icon', 'src/main/deploy/package/osx/sparrow.icns', '--resource-dir', 'src/main/deploy/package/osx/']
installerType = "dmg"
}
}
+1 -1
Submodule drongo updated: c7e16a29e3...5e281982cb
@@ -21,7 +21,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.9.2</string>
<string>0.9.4</string>
<key>CFBundleSignature</key>
<string>????</string>
<!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories -->
@@ -19,6 +19,7 @@ import com.sparrowwallet.sparrow.control.*;
import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.io.*;
import com.sparrowwallet.sparrow.net.ElectrumServer;
import com.sparrowwallet.sparrow.net.VersionCheckService;
import com.sparrowwallet.sparrow.preferences.PreferencesDialog;
import com.sparrowwallet.sparrow.transaction.TransactionController;
import com.sparrowwallet.sparrow.transaction.TransactionData;
@@ -70,8 +71,8 @@ public class AppController implements Initializable {
private static final int SERVER_PING_PERIOD = 10 * 1000;
private static final int ENUMERATE_HW_PERIOD = 30 * 1000;
private static final int RATES_PERIOD = 5 * 60 * 1000;
private static final int VERSION_CHECK_PERIOD_HOURS = 24;
private static final ExchangeSource DEFAULT_EXCHANGE_SOURCE = ExchangeSource.COINGECKO;
private static final Currency DEFAULT_FIAT_CURRENCY = Currency.getInstance("USD");
@@ -97,6 +98,9 @@ public class AppController implements Initializable {
@FXML
private ToggleGroup bitcoinUnit;
@FXML
private ToggleGroup theme;
@FXML
private CheckMenuItem showTxHex;
@@ -125,6 +129,8 @@ public class AppController implements Initializable {
private Hwi.ScheduledEnumerateService deviceEnumerateService;
private VersionCheckService versionCheckService;
private static Integer currentBlockHeight;
public static boolean showTxHexProperty;
@@ -221,8 +227,18 @@ public class AppController implements Initializable {
Config.get().setBitcoinUnit(unit);
}
final BitcoinUnit selectedUnit = unit;
Optional<Toggle> selectedToggle = bitcoinUnit.getToggles().stream().filter(toggle -> selectedUnit.equals(toggle.getUserData())).findFirst();
selectedToggle.ifPresent(toggle -> bitcoinUnit.selectToggle(toggle));
Optional<Toggle> selectedUnitToggle = bitcoinUnit.getToggles().stream().filter(toggle -> selectedUnit.equals(toggle.getUserData())).findFirst();
selectedUnitToggle.ifPresent(toggle -> bitcoinUnit.selectToggle(toggle));
Theme configTheme = Config.get().getTheme();
if(configTheme == null) {
configTheme = Theme.LIGHT;
Config.get().setTheme(Theme.LIGHT);
}
final Theme selectedTheme = configTheme;
Optional<Toggle> selectedThemeToggle = theme.getToggles().stream().filter(toggle -> selectedTheme.equals(toggle.getUserData())).findFirst();
selectedThemeToggle.ifPresent(toggle -> theme.selectToggle(toggle));
setTheme(null);
showTxHex.setSelected(true);
showTxHexProperty = true;
@@ -247,9 +263,18 @@ public class AppController implements Initializable {
if(!ratesService.isRunning() && ratesService.getExchangeSource() != ExchangeSource.NONE) {
ratesService.start();
}
if(versionCheckService.getState() == Worker.State.CANCELLED) {
versionCheckService.reset();
}
if(!versionCheckService.isRunning() && Config.get().isCheckNewVersions()) {
versionCheckService.start();
}
} else {
connectionService.cancel();
ratesService.cancel();
versionCheckService.cancel();
}
}
});
@@ -268,10 +293,15 @@ public class AppController implements Initializable {
ExchangeSource source = config.getExchangeSource() != null ? config.getExchangeSource() : DEFAULT_EXCHANGE_SOURCE;
Currency currency = config.getFiatCurrency() != null ? config.getFiatCurrency() : DEFAULT_FIAT_CURRENCY;
ratesService = createRatesService(source, currency);
if (config.getMode() == Mode.ONLINE && source != ExchangeSource.NONE) {
if(config.getMode() == Mode.ONLINE && source != ExchangeSource.NONE) {
ratesService.start();
}
versionCheckService = createVersionCheckService();
if(config.getMode() == Mode.ONLINE && config.isCheckNewVersions()) {
versionCheckService.start();
}
openTransactionIdItem.disableProperty().bind(onlineProperty.not());
}
@@ -280,6 +310,13 @@ public class AppController implements Initializable {
connectionService.setPeriod(new Duration(SERVER_PING_PERIOD));
connectionService.setRestartOnFailure(true);
EventManager.get().register(connectionService);
connectionService.statusProperty().addListener((observable, oldValue, newValue) -> {
if(connectionService.isRunning()) {
EventManager.get().post(new StatusEvent(newValue));
}
});
connectionService.setOnSucceeded(successEvent -> {
changeMode = false;
onlineProperty.setValue(true);
@@ -316,6 +353,22 @@ public class AppController implements Initializable {
return ratesService;
}
private VersionCheckService createVersionCheckService() {
VersionCheckService versionCheckService = new VersionCheckService();
versionCheckService.setDelay(Duration.seconds(10));
versionCheckService.setPeriod(Duration.hours(VERSION_CHECK_PERIOD_HOURS));
versionCheckService.setRestartOnFailure(true);
versionCheckService.setOnSucceeded(successEvent -> {
VersionUpdatedEvent event = versionCheckService.getValue();
if(event != null) {
EventManager.get().post(event);
}
});
return versionCheckService;
}
private Hwi.ScheduledEnumerateService createDeviceEnumerateService() {
Hwi.ScheduledEnumerateService enumerateService = new Hwi.ScheduledEnumerateService(null);
enumerateService.setPeriod(new Duration(ENUMERATE_HW_PERIOD));
@@ -350,8 +403,8 @@ public class AppController implements Initializable {
tk.createQuitMenuItem(MainApp.APP_NAME));
tk.setApplicationMenu(defaultApplicationMenu);
fileMenu.getItems().removeIf(item -> item.getStyleClass().contains("macHide"));
helpMenu.getItems().removeIf(item -> item.getStyleClass().contains("macHide"));
fileMenu.getItems().removeIf(item -> item.getStyleClass().contains("osxHide"));
helpMenu.getItems().removeIf(item -> item.getStyleClass().contains("osxHide"));
}
}
@@ -369,11 +422,11 @@ public class AppController implements Initializable {
Stage stage = new Stage();
stage.setTitle("About " + MainApp.APP_NAME);
stage.initStyle(org.controlsfx.tools.Platform.getCurrent() == org.controlsfx.tools.Platform.OSX ? StageStyle.UNDECORATED : StageStyle.DECORATED);
setStageIcon(stage);
stage.setResizable(false);
stage.setScene(new Scene(root));
controller.setStage(stage);
controller.initializeView();
setStageIcon(stage);
return stage;
} catch(IOException e) {
@@ -615,6 +668,7 @@ public class AppController implements Initializable {
public static void showErrorDialog(String title, String content) {
Alert alert = new Alert(Alert.AlertType.ERROR);
setStageIcon(alert.getDialogPane().getScene().getWindow());
alert.getDialogPane().getScene().getStylesheets().add(AppController.class.getResource("general.css").toExternalForm());
alert.setTitle(title);
alert.setHeaderText(title);
alert.setContentText(content);
@@ -624,6 +678,10 @@ public class AppController implements Initializable {
public static void setStageIcon(Window window) {
Stage stage = (Stage)window;
stage.getIcons().add(new Image(AppController.class.getResourceAsStream("/image/sparrow.png")));
if(stage.getScene() != null && Config.get().getTheme() == Theme.DARK) {
stage.getScene().getStylesheets().add(AppController.class.getResource("darktheme.css").toExternalForm());
}
}
public static Font getMonospaceFont() {
@@ -848,6 +906,27 @@ public class AppController implements Initializable {
preferencesDialog.showAndWait();
}
public void signVerifyMessage(ActionEvent event) {
MessageSignDialog messageSignDialog = null;
Tab tab = tabs.getSelectionModel().getSelectedItem();
if(tab != null && tab.getUserData() instanceof WalletTabData) {
WalletTabData walletTabData = (WalletTabData)tab.getUserData();
Wallet wallet = walletTabData.getWallet();
if(wallet.getKeystores().size() == 1 &&
(wallet.getKeystores().get(0).hasSeed() || wallet.getKeystores().get(0).getSource() == KeystoreSource.HW_USB)) {
//Can sign and verify
messageSignDialog = new MessageSignDialog(wallet);
}
}
if(messageSignDialog == null) {
//Can verify only
messageSignDialog = new MessageSignDialog();
}
messageSignDialog.showAndWait();
}
public Tab addWalletTab(Storage storage, Wallet wallet) {
for(Tab tab : tabs.getTabs()) {
TabData tabData = (TabData)tab.getUserData();
@@ -1031,6 +1110,19 @@ public class AppController implements Initializable {
return contextMenu;
}
public void setTheme(ActionEvent event) {
Theme selectedTheme = (Theme)theme.getSelectedToggle().getUserData();
if(Config.get().getTheme() != selectedTheme) {
Config.get().setTheme(selectedTheme);
}
if(selectedTheme == Theme.DARK) {
tabs.getScene().getStylesheets().add(getClass().getResource("darktheme.css").toExternalForm());
} else {
tabs.getScene().getStylesheets().remove(getClass().getResource("darktheme.css").toExternalForm());
}
}
@Subscribe
public void tabSelected(TabSelectedEvent event) {
String tabName = event.getTabName();
@@ -1134,7 +1226,7 @@ public class AppController implements Initializable {
public void statusUpdated(StatusEvent event) {
statusBar.setText(event.getStatus());
PauseTransition wait = new PauseTransition(Duration.seconds(10));
PauseTransition wait = new PauseTransition(Duration.seconds(20));
wait.setOnFinished((e) -> {
if(statusBar.getText().equals(event.getStatus())) {
statusBar.setText("");
@@ -1143,6 +1235,20 @@ public class AppController implements Initializable {
wait.play();
}
@Subscribe
public void versionUpdated(VersionUpdatedEvent event) {
Hyperlink versionUpdateLabel = new Hyperlink("Sparrow " + event.getVersion() + " available");
versionUpdateLabel.setOnAction(event1 -> {
application.getHostServices().showDocument("https://www.sparrowwallet.com/download");
});
if(statusBar.getRightItems().size() > 0 && statusBar.getRightItems().get(0) instanceof Hyperlink) {
statusBar.getRightItems().remove(0);
}
statusBar.getRightItems().add(0, versionUpdateLabel);
}
@Subscribe
public void timedWorker(TimedEvent event) {
if(event.getTimeMills() == 0) {
@@ -1186,7 +1292,7 @@ public class AppController implements Initializable {
} else {
if(usbStatus == null) {
usbStatus = new UsbStatusButton();
statusBar.getRightItems().add(0, usbStatus);
statusBar.getRightItems().add(Math.max(statusBar.getRightItems().size() - 1, 0), usbStatus);
} else {
usbStatus.getItems().remove(0, usbStatus.getItems().size());
}
@@ -1257,6 +1363,16 @@ public class AppController implements Initializable {
fiatCurrencyExchangeRate = event.getCurrencyRate();
}
@Subscribe
public void versionCheckStatus(VersionCheckStatusEvent event) {
versionCheckService.cancel();
if(Config.get().getMode() != Mode.OFFLINE && event.isEnabled()) {
versionCheckService = createVersionCheckService();
versionCheckService.start();
}
}
@Subscribe
public void openWallets(OpenWalletsEvent event) {
List<File> walletFiles = event.getWalletsMap().values().stream().map(storage -> storage.getWalletFile()).collect(Collectors.toList());
@@ -1,6 +1,9 @@
package com.sparrowwallet.sparrow;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptChunk;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.sparrow.control.DescriptorArea;
import com.sparrowwallet.sparrow.control.ScriptArea;
import javafx.geometry.Point2D;
import javafx.scene.control.Label;
@@ -39,4 +42,42 @@ public abstract class BaseController {
protected String describeScriptChunk(ScriptChunk chunk) {
return chunk.toString();
}
protected void initializeDescriptorField(DescriptorArea descriptorArea) {
Popup popup = new Popup();
Label popupMsg = new Label();
popupMsg.getStyleClass().add("tooltip");
popup.getContent().add(popupMsg);
descriptorArea.setMouseOverTextDelay(Duration.ofMillis(150));
descriptorArea.addEventHandler(MouseOverTextEvent.MOUSE_OVER_TEXT_BEGIN, e -> {
TwoDimensional.Position position = descriptorArea.getParagraph(0).getStyleSpans().offsetToPosition(e.getCharacterIndex(), Backward);
int index = descriptorArea.getWallet().getPolicyType() == PolicyType.SINGLE ? position.getMajor() - 1 : ((position.getMajor() - 1) / 2);
if(position.getMajor() > 0 && index >= 0 && index < descriptorArea.getWallet().getKeystores().size()) {
Keystore hoverKeystore = descriptorArea.getWallet().getKeystores().get(index);
Point2D pos = e.getScreenPosition();
popupMsg.setText(describeKeystore(hoverKeystore));
popup.show(descriptorArea, pos.getX(), pos.getY() + 10);
}
});
descriptorArea.addEventHandler(MouseOverTextEvent.MOUSE_OVER_TEXT_END, e -> {
popup.hide();
});
}
protected String describeKeystore(Keystore keystore) {
if(keystore.isValid()) {
StringBuilder builder = new StringBuilder();
builder.append("[");
builder.append(keystore.getKeyDerivation().getMasterFingerprint());
builder.append("/");
builder.append(keystore.getKeyDerivation().getDerivationPath().replaceFirst("^m?/", ""));
builder.append("]");
builder.append(keystore.getExtendedPublicKey().toString());
return builder.toString();
}
return "Invalid";
}
}
@@ -28,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 = "0.9.2";
public static final String APP_VERSION = "0.9.4";
private Stage mainStage;
@@ -0,0 +1,5 @@
package com.sparrowwallet.sparrow;
public enum Theme {
LIGHT, DARK
}
@@ -32,7 +32,7 @@ public class AddressCell extends TreeTableCell<Entry, Entry> {
UtxoEntry utxoEntry = (UtxoEntry)entry;
Address address = utxoEntry.getAddress();
setText(address.toString());
setContextMenu(new EntryCell.AddressContextMenu(address, utxoEntry.getOutputDescriptor()));
setContextMenu(new EntryCell.AddressContextMenu(address, utxoEntry.getOutputDescriptor(), null));
Tooltip tooltip = new Tooltip();
tooltip.setText(getTooltipText(utxoEntry));
setTooltip(tooltip);
@@ -2,8 +2,11 @@ package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.event.WalletHistoryStatusEvent;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.wallet.Entry;
import javafx.application.Platform;
import javafx.scene.control.Label;
import javafx.scene.control.TreeTableView;
public class CoinTreeTable extends TreeTableView<Entry> {
@@ -33,4 +36,20 @@ public class CoinTreeTable extends TreeTableView<Entry> {
refresh();
}
}
public void updateHistoryStatus(WalletHistoryStatusEvent event) {
Platform.runLater(() -> {
if(event.getErrorMessage() != null) {
setPlaceholder(new Label("Error loading transactions: " + event.getErrorMessage()));
} else if(event.isLoading()) {
if(event.getStatusMessage() != null) {
setPlaceholder(new Label(event.getStatusMessage() + "..."));
} else {
setPlaceholder(new Label("Loading transactions..."));
}
} else {
setPlaceholder(new Label("No transactions"));
}
});
}
}
@@ -0,0 +1,89 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.OutputDescriptor;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.Wallet;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import org.fxmisc.richtext.CodeArea;
import java.util.List;
import static com.sparrowwallet.drongo.policy.PolicyType.MULTI;
import static com.sparrowwallet.drongo.policy.PolicyType.SINGLE;
import static com.sparrowwallet.drongo.protocol.ScriptType.MULTISIG;
public class DescriptorArea extends CodeArea {
private Wallet wallet;
public void setWallet(Wallet wallet) {
clear();
this.wallet = wallet;
DescriptorContextMenu contextMenu = new DescriptorContextMenu(wallet, this);
setContextMenu(contextMenu);
PolicyType policyType = wallet.getPolicyType();
ScriptType scriptType = wallet.getScriptType();
List<Keystore> keystores = wallet.getKeystores();
int threshold = wallet.getDefaultPolicy().getNumSignaturesRequired();
if(SINGLE.equals(policyType)) {
append(scriptType.getDescriptor(), "descriptor-text");
replace(getLength(), getLength(), keystores.get(0).getScriptName(), List.of(keystores.get(0).isValid() ? "descriptor-text" : "descriptor-error", keystores.get(0).getScriptName()));
append(scriptType.getCloseDescriptor(), "descriptor-text");
}
if(MULTI.equals(policyType)) {
append(scriptType.getDescriptor(), "descriptor-text");
append(MULTISIG.getDescriptor(), "descriptor-text");
append(Integer.toString(threshold), "descriptor-text");
for(Keystore keystore : keystores) {
append(",", "descriptor-text");
replace(getLength(), getLength(), keystore.getScriptName(), List.of(keystore.isValid() ? "descriptor-text" : "descriptor-error", keystore.getScriptName()));
}
append(MULTISIG.getCloseDescriptor(), "descriptor-text");
append(scriptType.getCloseDescriptor(), "descriptor-text");
}
}
public Wallet getWallet() {
return wallet;
}
public void clear() {
super.clear();
this.wallet = null;
setDisable(false);
setContextMenu(null);
}
private static class DescriptorContextMenu extends ContextMenu {
public DescriptorContextMenu(Wallet wallet, DescriptorArea descriptorArea) {
MenuItem copyvalue = new MenuItem("Copy Value");
copyvalue.setOnAction(AE -> {
hide();
ClipboardContent content = new ClipboardContent();
content.putString(descriptorArea.getText());
Clipboard.getSystemClipboard().setContent(content);
});
getItems().add(copyvalue);
MenuItem copyOutputDescriptor = new MenuItem("Copy Output Descriptor");
copyOutputDescriptor.setOnAction(AE -> {
hide();
ClipboardContent content = new ClipboardContent();
content.putString(OutputDescriptor.getOutputDescriptor(wallet).toString(true));
Clipboard.getSystemClipboard().setContent(content);
});
getItems().add(copyOutputDescriptor);
this.setStyle("-fx-background-color: -fx-color; -fx-font-family: System; -fx-font-size: 1em;");
}
}
}
@@ -10,6 +10,7 @@ import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.AddressDisplayedEvent;
import com.sparrowwallet.sparrow.event.KeystoreImportEvent;
import com.sparrowwallet.sparrow.event.MessageSignedEvent;
import com.sparrowwallet.sparrow.event.PSBTSignedEvent;
import com.sparrowwallet.sparrow.io.Device;
import com.sparrowwallet.sparrow.io.Hwi;
@@ -38,6 +39,7 @@ public class DevicePane extends TitledDescriptionPane {
private final Wallet wallet;
private final PSBT psbt;
private final KeyDerivation keyDerivation;
private final String message;
private final Device device;
private CustomPasswordField pinField;
@@ -47,6 +49,7 @@ public class DevicePane extends TitledDescriptionPane {
private SplitMenuButton importButton;
private Button signButton;
private Button displayAddressButton;
private Button signMessageButton;
private final SimpleStringProperty passphrase = new SimpleStringProperty("");
@@ -56,6 +59,7 @@ public class DevicePane extends TitledDescriptionPane {
this.wallet = wallet;
this.psbt = null;
this.keyDerivation = null;
this.message = null;
this.device = device;
setDefaultStatus();
@@ -81,6 +85,7 @@ public class DevicePane extends TitledDescriptionPane {
this.wallet = null;
this.psbt = psbt;
this.keyDerivation = null;
this.message = null;
this.device = device;
setDefaultStatus();
@@ -106,6 +111,7 @@ public class DevicePane extends TitledDescriptionPane {
this.wallet = wallet;
this.psbt = null;
this.keyDerivation = keyDerivation;
this.message = null;
this.device = device;
setDefaultStatus();
@@ -125,6 +131,32 @@ public class DevicePane extends TitledDescriptionPane {
buttonBox.getChildren().addAll(setPassphraseButton, displayAddressButton);
}
public DevicePane(Wallet wallet, String message, KeyDerivation keyDerivation, Device device) {
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
this.deviceOperation = DeviceOperation.SIGN_MESSAGE;
this.wallet = wallet;
this.psbt = null;
this.keyDerivation = keyDerivation;
this.message = message;
this.device = device;
setDefaultStatus();
showHideLink.setVisible(false);
createSetPassphraseButton();
createSignMessageButton();
if (device.getNeedsPinSent() != null && device.getNeedsPinSent()) {
unlockButton.setVisible(true);
} else if(device.getNeedsPassphraseSent() != null && device.getNeedsPassphraseSent()) {
setPassphraseButton.setVisible(true);
} else {
showOperationButton();
}
buttonBox.getChildren().addAll(setPassphraseButton, signMessageButton);
}
@Override
protected Control createButton() {
createUnlockButton();
@@ -209,8 +241,24 @@ public class DevicePane extends TitledDescriptionPane {
}
}
private void createSignMessageButton() {
signMessageButton = new Button("Sign Message");
signMessageButton.setDefaultButton(true);
signMessageButton.setAlignment(Pos.CENTER_RIGHT);
signMessageButton.setOnAction(event -> {
signMessageButton.setDisable(true);
signMessage();
});
signMessageButton.managedProperty().bind(signMessageButton.visibleProperty());
signMessageButton.setVisible(false);
if(device.getFingerprint() != null && !device.getFingerprint().equals(keyDerivation.getMasterFingerprint())) {
signMessageButton.setDisable(true);
}
}
private void unlock(Device device) {
if(device.getModel().equals(WalletModel.TREZOR_1)) {
if(device.getModel().requiresPinPrompt()) {
promptPin();
}
}
@@ -438,6 +486,20 @@ public class DevicePane extends TitledDescriptionPane {
displayAddressService.start();
}
private void signMessage() {
Hwi.SignMessageService signMessageService = new Hwi.SignMessageService(device, passphrase.get(), message, keyDerivation.getDerivationPath());
signMessageService.setOnSucceeded(successEvent -> {
String signature = signMessageService.getValue();
EventManager.get().post(new MessageSignedEvent(wallet, signature));
});
signMessageService.setOnFailed(failedEvent -> {
setError("Could not sign message", signMessageService.getException().getMessage());
signMessageButton.setDisable(false);
});
setDescription("Signing message...");
signMessageService.start();
}
private void showOperationButton() {
if(deviceOperation.equals(DeviceOperation.IMPORT)) {
importButton.setVisible(true);
@@ -450,6 +512,9 @@ public class DevicePane extends TitledDescriptionPane {
} else if(deviceOperation.equals(DeviceOperation.DISPLAY_ADDRESS)) {
displayAddressButton.setVisible(true);
showHideLink.setVisible(false);
} else if(deviceOperation.equals(DeviceOperation.SIGN_MESSAGE)) {
signMessageButton.setVisible(true);
showHideLink.setVisible(false);
}
}
@@ -490,6 +555,6 @@ public class DevicePane extends TitledDescriptionPane {
}
public enum DeviceOperation {
IMPORT, SIGN, DISPLAY_ADDRESS;
IMPORT, SIGN, DISPLAY_ADDRESS, SIGN_MESSAGE;
}
}
@@ -0,0 +1,39 @@
package com.sparrowwallet.sparrow.control;
import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.MessageSignedEvent;
import com.sparrowwallet.sparrow.io.Device;
import java.util.List;
public class DeviceSignMessageDialog extends DeviceDialog<String> {
private final Wallet wallet;
private final String message;
private final KeyDerivation keyDerivation;
public DeviceSignMessageDialog(List<String> operationFingerprints, Wallet wallet, String message, KeyDerivation keyDerivation) {
super(operationFingerprints);
this.wallet = wallet;
this.message = message;
this.keyDerivation = keyDerivation;
EventManager.get().register(this);
setOnCloseRequest(event -> {
EventManager.get().unregister(this);
});
}
@Override
protected DevicePane getDevicePane(Device device) {
return new DevicePane(wallet, message, keyDerivation, device);
}
@Subscribe
public void messageSigned(MessageSignedEvent event) {
if(wallet == event.getWallet()) {
setResult(event.getSignature());
}
}
}
@@ -0,0 +1,24 @@
package com.sparrowwallet.sparrow.control;
import tornadofx.control.Form;
public class DynamicForm extends Form {
private DynamicUpdate dynamicUpdate;
public DynamicForm() {
super();
}
public void setDynamicUpdate(DynamicUpdate dynamicUpdate) {
this.dynamicUpdate = dynamicUpdate;
}
@Override
protected void layoutChildren() {
if(dynamicUpdate != null) {
dynamicUpdate.update();
}
super.layoutChildren();
}
}
@@ -0,0 +1,5 @@
package com.sparrowwallet.sparrow.control;
public interface DynamicUpdate {
void update();
}
@@ -3,6 +3,7 @@ package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.wallet.BlockTransaction;
import com.sparrowwallet.drongo.wallet.KeystoreSource;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
@@ -71,12 +72,13 @@ class EntryCell extends TreeTableCell<Entry, Entry> {
NodeEntry nodeEntry = (NodeEntry)entry;
Address address = nodeEntry.getAddress();
setText(address.toString());
setContextMenu(new AddressContextMenu(address, nodeEntry.getOutputDescriptor()));
setContextMenu(new AddressContextMenu(address, nodeEntry.getOutputDescriptor(), null));
Tooltip tooltip = new Tooltip();
tooltip.setText(nodeEntry.getNode().getDerivationPath());
setTooltip(tooltip);
getStyleClass().add("address-cell");
HBox actionBox = new HBox();
Button receiveButton = new Button("");
Glyph receiveGlyph = new Glyph("FontAwesome", FontAwesome.Glyph.ARROW_DOWN);
receiveGlyph.setFontSize(12);
@@ -85,7 +87,23 @@ class EntryCell extends TreeTableCell<Entry, Entry> {
EventManager.get().post(new ReceiveActionEvent(nodeEntry));
Platform.runLater(() -> EventManager.get().post(new ReceiveToEvent(nodeEntry)));
});
setGraphic(receiveButton);
actionBox.getChildren().add(receiveButton);
if(nodeEntry.getWallet().getKeystores().size() == 1 &&
(nodeEntry.getWallet().getKeystores().get(0).hasSeed() || nodeEntry.getWallet().getKeystores().get(0).getSource() == KeystoreSource.HW_USB)) {
Button signMessageButton = new Button("");
Glyph signMessageGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.PEN_FANCY);
signMessageGlyph.setFontSize(12);
signMessageButton.setGraphic(signMessageGlyph);
signMessageButton.setOnAction(event -> {
MessageSignDialog messageSignDialog = new MessageSignDialog(nodeEntry.getWallet(), nodeEntry.getNode());
messageSignDialog.showAndWait();
});
actionBox.getChildren().add(signMessageButton);
setContextMenu(new AddressContextMenu(address, nodeEntry.getOutputDescriptor(), nodeEntry));
}
setGraphic(actionBox);
} else if(entry instanceof HashIndexEntry) {
HashIndexEntry hashIndexEntry = (HashIndexEntry)entry;
setText(hashIndexEntry.getDescription());
@@ -178,7 +196,7 @@ class EntryCell extends TreeTableCell<Entry, Entry> {
}
public static class AddressContextMenu extends ContextMenu {
public AddressContextMenu(Address address, String outputDescriptor) {
public AddressContextMenu(Address address, String outputDescriptor, NodeEntry nodeEntry) {
MenuItem copyAddress = new MenuItem("Copy Address");
copyAddress.setOnAction(AE -> {
hide();
@@ -204,6 +222,17 @@ class EntryCell extends TreeTableCell<Entry, Entry> {
});
getItems().addAll(copyAddress, copyHex, copyOutputDescriptor);
if(nodeEntry != null) {
MenuItem signVerifyMessage = new MenuItem("Sign/Verify Message");
signVerifyMessage.setOnAction(AE -> {
hide();
MessageSignDialog messageSignDialog = new MessageSignDialog(nodeEntry.getWallet(), nodeEntry.getNode());
messageSignDialog.showAndWait();
});
getItems().add(signVerifyMessage);
}
}
}
@@ -0,0 +1,329 @@
package com.sparrowwallet.sparrow.control;
import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.SecureString;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.address.InvalidAddressException;
import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.KeystoreSource;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletNode;
import com.sparrowwallet.sparrow.AppController;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.OpenWalletsEvent;
import com.sparrowwallet.sparrow.event.RequestOpenWalletsEvent;
import com.sparrowwallet.sparrow.event.StorageEvent;
import com.sparrowwallet.sparrow.event.TimedEvent;
import com.sparrowwallet.sparrow.io.Storage;
import javafx.application.Platform;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.VBox;
import org.controlsfx.validation.ValidationResult;
import org.controlsfx.validation.ValidationSupport;
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import tornadofx.control.Field;
import tornadofx.control.Fieldset;
import tornadofx.control.Form;
import java.security.SignatureException;
import java.util.List;
import java.util.Optional;
import static com.sparrowwallet.sparrow.AppController.setStageIcon;
public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
private static final Logger log = LoggerFactory.getLogger(MessageSignDialog.class);
private final TextField address;
private final TextArea message;
private final TextArea signature;
private final Wallet wallet;
private WalletNode walletNode;
/**
* Verification only constructor
*/
public MessageSignDialog() {
this(null, null);
}
/**
* Sign and verify with user entered address
*
* @param wallet Wallet to sign with
*/
public MessageSignDialog(Wallet wallet) {
this(wallet, null);
}
/**
* Sign and verify with preset address
*
* @param wallet Wallet to sign with
* @param walletNode Wallet node to derive address from
*/
public MessageSignDialog(Wallet wallet, WalletNode walletNode) {
if(wallet != null) {
if(wallet.getKeystores().size() != 1) {
throw new IllegalArgumentException("Cannot sign messages using a wallet with multiple keystores - a single key is required");
}
if(!wallet.getKeystores().get(0).hasSeed() && wallet.getKeystores().get(0).getSource() != KeystoreSource.HW_USB) {
throw new IllegalArgumentException("Cannot sign messages using a wallet without a seed or USB keystore");
}
}
this.wallet = wallet;
this.walletNode = walletNode;
final DialogPane dialogPane = getDialogPane();
dialogPane.getStylesheets().add(AppController.class.getResource("general.css").toExternalForm());
AppController.setStageIcon(dialogPane.getScene().getWindow());
dialogPane.setHeaderText(wallet == null ? "Verify Message" : "Sign/Verify Message");
Image image = new Image("image/seed.png", 50, 50, false, false);
if (!image.isError()) {
ImageView imageView = new ImageView();
imageView.setSmooth(false);
imageView.setImage(image);
dialogPane.setGraphic(imageView);
}
VBox vBox = new VBox();
vBox.setSpacing(20);
Form form = new Form();
Fieldset fieldset = new Fieldset();
fieldset.setText("");
Field addressField = new Field();
addressField.setText("Address:");
address = new TextField();
address.getStyleClass().add("id");
address.setEditable(walletNode == null);
addressField.getInputs().add(address);
if(walletNode != null) {
address.setText(wallet.getAddress(walletNode).toString());
}
Field messageField = new Field();
messageField.setText("Message:");
message = new TextArea();
message.setWrapText(true);
message.setPrefRowCount(10);
message.setStyle("-fx-pref-height: 180px");
messageField.getInputs().add(message);
Field signatureField = new Field();
signatureField.setText("Signature:");
signature = new TextArea();
signature.getStyleClass().add("id");
signature.setPrefRowCount(2);
signature.setStyle("-fx-pref-height: 60px");
signature.setWrapText(true);
signatureField.getInputs().add(signature);
fieldset.getChildren().addAll(addressField, messageField, signatureField);
form.getChildren().add(fieldset);
dialogPane.setContent(form);
ButtonType signButtonType = new javafx.scene.control.ButtonType("Sign", ButtonBar.ButtonData.BACK_PREVIOUS);
ButtonType verifyButtonType = new javafx.scene.control.ButtonType("Verify", ButtonBar.ButtonData.NEXT_FORWARD);
ButtonType doneButtonType = new javafx.scene.control.ButtonType("Done", ButtonBar.ButtonData.OK_DONE);
dialogPane.getButtonTypes().addAll(signButtonType, verifyButtonType, doneButtonType);
Button signButton = (Button)dialogPane.lookupButton(signButtonType);
signButton.setDisable(wallet == null);
signButton.setOnAction(event -> {
signMessage();
});
Button verifyButton = (Button)dialogPane.lookupButton(verifyButtonType);
verifyButton.setDefaultButton(false);
verifyButton.setOnAction(event -> {
verifyMessage();
});
boolean validAddress = isValidAddress();
signButton.setDisable(!validAddress || (wallet == null));
verifyButton.setDisable(!validAddress);
ValidationSupport validationSupport = new ValidationSupport();
Platform.runLater(() -> {
validationSupport.registerValidator(address, (Control c, String newValue) -> ValidationResult.fromErrorIf(c, "Invalid address", !isValidAddress()));
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
});
address.textProperty().addListener((observable, oldValue, newValue) -> {
boolean valid = isValidAddress();
signButton.setDisable(!valid || (wallet == null));
verifyButton.setDisable(!valid);
if(valid && wallet != null) {
try {
Address address = getAddress();
setWalletNodeFromAddress(wallet, address);
} catch(InvalidAddressException e) {
//can't happen
}
}
});
EventManager.get().register(this);
setOnCloseRequest(event -> {
if(ButtonBar.ButtonData.APPLY.equals(getResult())) {
event.consume();
return;
}
EventManager.get().unregister(this);
});
setResultConverter(dialogButton -> dialogButton == signButtonType || dialogButton == verifyButtonType ? ButtonBar.ButtonData.APPLY : ButtonBar.ButtonData.OK_DONE);
}
private Address getAddress()throws InvalidAddressException {
return Address.fromString(address.getText());
}
private boolean isValidAddress() {
try {
getAddress();
return true;
} catch (InvalidAddressException e) {
return false;
}
}
private void setWalletNodeFromAddress(Wallet wallet, Address address) {
walletNode = wallet.getWalletAddresses().get(address);
}
private void signMessage() {
if(walletNode == null) {
AppController.showErrorDialog("Address not in wallet", "The provided address is not present in the currently selected wallet.");
return;
}
if(wallet.containsSeeds()) {
if(wallet.isEncrypted()) {
EventManager.get().post(new RequestOpenWalletsEvent());
} else {
signUnencryptedKeystore(wallet);
}
} else if(wallet.containsSource(KeystoreSource.HW_USB)) {
signUsbKeystore(wallet);
}
}
private void signUnencryptedKeystore(Wallet decryptedWallet) {
try {
//Note we can expect a single keystore due to the check above
Keystore keystore = decryptedWallet.getKeystores().get(0);
ECKey privKey = keystore.getKey(walletNode);
String signatureText = privKey.signMessage(message.getText().trim(), decryptedWallet.getScriptType(), null);
signature.clear();
signature.appendText(signatureText);
} catch(Exception e) {
log.error("Could not sign message", e);
AppController.showErrorDialog("Could not sign message", e.getMessage());
}
}
private void signUsbKeystore(Wallet usbWallet) {
List<String> fingerprints = List.of(usbWallet.getKeystores().get(0).getKeyDerivation().getMasterFingerprint());
KeyDerivation fullDerivation = usbWallet.getKeystores().get(0).getKeyDerivation().extend(walletNode.getDerivation());
DeviceSignMessageDialog deviceSignMessageDialog = new DeviceSignMessageDialog(fingerprints, usbWallet, message.getText().trim(), fullDerivation);
Optional<String> optSignature = deviceSignMessageDialog.showAndWait();
if(optSignature.isPresent()) {
signature.clear();
signature.appendText(optSignature.get());
}
}
private void verifyMessage() {
try {
//Find ECKey from message and signature
//http://www.secg.org/download/aid-780/sec1-v2.pdf section 4.1.6
boolean verified = false;
try {
ECKey signedMessageKey = ECKey.signedMessageToKey(message.getText().trim(), signature.getText().trim(), false);
verified = verifyMessage(signedMessageKey);
} catch(SignatureException e) {
//ignore
}
if(!verified) {
try {
ECKey electrumSignedMessageKey = ECKey.signedMessageToKey(message.getText(), signature.getText(), true);
verified = verifyMessage(electrumSignedMessageKey);
} catch(SignatureException e) {
//ignore
}
}
if(verified) {
Alert alert = new Alert(Alert.AlertType.INFORMATION);
setStageIcon(alert.getDialogPane().getScene().getWindow());
alert.setTitle("Verification Succeeded");
alert.setHeaderText("Verification Succeeded");
alert.setContentText("The signature verified against the message.");
alert.showAndWait();
} else {
AppController.showErrorDialog("Verification failed", "The provided signature did not match the message for this address.");
}
} catch(IllegalArgumentException e) {
AppController.showErrorDialog("Could not verify message", e.getMessage());
} catch(Exception e) {
log.error("Could not verify message", e);
AppController.showErrorDialog("Could not verify message", e.getMessage());
}
}
private boolean verifyMessage(ECKey signedMessageKey) throws InvalidAddressException, SignatureException {
Address providedAddress = getAddress();
ScriptType scriptType = providedAddress.getScriptType();
if(scriptType == ScriptType.P2SH) {
scriptType = ScriptType.P2SH_P2WPKH;
}
if(!ScriptType.getScriptTypesForPolicyType(PolicyType.SINGLE).contains(scriptType)) {
throw new IllegalArgumentException("Only single signature P2PKH, P2SH-P2WPKH or P2WPKH addresses can verify messages.");
}
Address signedMessageAddress = scriptType.getAddress(signedMessageKey);
return providedAddress.equals(signedMessageAddress);
}
@Subscribe
public void openWallets(OpenWalletsEvent event) {
Storage storage = event.getStorage(wallet);
if(storage == null) {
throw new IllegalStateException("Wallet " + wallet + " without Storage");
}
WalletPasswordDialog dlg = new WalletPasswordDialog(WalletPasswordDialog.PasswordRequirement.LOAD);
Optional<SecureString> password = dlg.showAndWait();
if(password.isPresent()) {
Storage.DecryptWalletService decryptWalletService = new Storage.DecryptWalletService(wallet.copy(), password.get());
decryptWalletService.setOnSucceeded(workerStateEvent -> {
EventManager.get().post(new StorageEvent(storage.getWalletFile(), TimedEvent.Action.END, "Done"));
Wallet decryptedWallet = decryptWalletService.getValue();
signUnencryptedKeystore(decryptedWallet);
});
decryptWalletService.setOnFailed(workerStateEvent -> {
EventManager.get().post(new StorageEvent(storage.getWalletFile(), TimedEvent.Action.END, "Failed"));
AppController.showErrorDialog("Incorrect Password", decryptWalletService.getException().getMessage());
});
EventManager.get().post(new StorageEvent(storage.getWalletFile(), TimedEvent.Action.START, "Decrypting wallet..."));
decryptWalletService.start();
}
}
}
@@ -188,7 +188,7 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
AnchorPane.setTopAnchor(invalidLabel, 5.0);
AnchorPane.setLeftAnchor(invalidLabel, 0.0);
confirmButton = new Button("Confirm Written Backup");
confirmButton = new Button("Re-enter Words...");
confirmButton.setOnAction(event -> {
confirmBackup();
});
@@ -269,7 +269,7 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
}
private void displayMnemonicCode() {
setDescription("Write down word list to confirm backup");
setDescription("Write down words before re-entering");
showHideLink.setVisible(false);
calculateButton.setVisible(false);
@@ -54,7 +54,7 @@ public class QRDisplayDialog extends Dialog<UR> {
qrImageView = new ImageView();
stackPane.getChildren().add(qrImageView);
dialogPane.setContent(Borders.wrap(stackPane).lineBorder().outerPadding(0).innerPadding(0).buildAll());
dialogPane.setContent(Borders.wrap(stackPane).lineBorder().buildAll());
nextPart();
if(encoder.isSinglePart()) {
@@ -40,7 +40,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
StackPane stackPane = new StackPane();
stackPane.getChildren().add(webcamView.getView());
dialogPane.setContent(Borders.wrap(stackPane).lineBorder().outerPadding(0).innerPadding(0).buildAll());
dialogPane.setContent(Borders.wrap(stackPane).lineBorder().buildAll());
webcamService.resultProperty().addListener(new QRResultListener());
webcamService.setOnFailed(failedEvent -> {
@@ -2,7 +2,6 @@ package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.protocol.Script;
import com.sparrowwallet.drongo.protocol.ScriptChunk;
import com.sparrowwallet.sparrow.transaction.ScriptContextMenu;
import javafx.geometry.Pos;
import org.controlsfx.control.decoration.Decorator;
import org.controlsfx.control.decoration.GraphicDecoration;
@@ -1,4 +1,4 @@
package com.sparrowwallet.sparrow.transaction;
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.protocol.Script;
import com.sparrowwallet.drongo.protocol.ScriptChunk;
@@ -33,7 +33,7 @@ public class ScriptContextMenu extends ContextMenu {
});
getItems().add(copyvalue);
this.setStyle("-fx-background-color: -fx-color; -fx-font-family: sans-serif; -fx-font-size: 1em;");
this.setStyle("-fx-background-color: -fx-color; -fx-font-family: System; -fx-font-size: 1em;");
area.addEventHandler(ContextMenuEvent.CONTEXT_MENU_REQUESTED, event -> {
hoverChunk = null;
@@ -26,6 +26,7 @@ public class TextAreaDialog extends Dialog<String> {
this.defaultValue = defaultValue;
dialogPane.setContent(hbox);
dialogPane.getStylesheets().add(AppController.class.getResource("general.css").toExternalForm());
AppController.setStageIcon(dialogPane.getScene().getWindow());
dialogPane.getStyleClass().add("text-input-dialog");
@@ -8,6 +8,7 @@ import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.WalletExportEvent;
import com.sparrowwallet.sparrow.io.ColdcardMultisig;
import com.sparrowwallet.sparrow.io.Electrum;
import com.sparrowwallet.sparrow.io.Specter;
import com.sparrowwallet.sparrow.io.WalletExport;
import javafx.scene.control.*;
import javafx.scene.layout.AnchorPane;
@@ -43,9 +44,9 @@ public class WalletExportDialog extends Dialog<Wallet> {
List<WalletExport> exporters;
if(wallet.getPolicyType() == PolicyType.SINGLE) {
exporters = List.of(new Electrum());
exporters = List.of(new Electrum(), new Specter());
} else if(wallet.getPolicyType() == PolicyType.MULTI) {
exporters = List.of(new ColdcardMultisig(), new Electrum());
exporters = List.of(new ColdcardMultisig(), new Electrum(), new Specter());
} else {
throw new UnsupportedOperationException("Cannot export wallet with policy type " + wallet.getPolicyType());
}
@@ -31,7 +31,7 @@ public class WalletImportDialog extends Dialog<Wallet> {
stackPane.getChildren().add(anchorPane);
ScrollPane scrollPane = new ScrollPane();
scrollPane.setPrefHeight(320);
scrollPane.setPrefHeight(400);
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
anchorPane.getChildren().add(scrollPane);
scrollPane.setFitToWidth(true);
@@ -45,7 +45,7 @@ public class WalletImportDialog extends Dialog<Wallet> {
importAccordion.getPanes().add(importPane);
}
List<WalletImport> walletImporters = List.of(new ColdcardMultisig(), new Electrum());
List<WalletImport> walletImporters = List.of(new ColdcardMultisig(), new Electrum(), new Specter());
for(WalletImport importer : walletImporters) {
FileWalletImportPane importPane = new FileWalletImportPane(importer);
importAccordion.getPanes().add(importPane);
@@ -55,7 +55,7 @@ public class WalletImportDialog extends Dialog<Wallet> {
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
dialogPane.getButtonTypes().addAll(cancelButtonType);
dialogPane.setPrefWidth(500);
dialogPane.setPrefHeight(400);
dialogPane.setPrefHeight(480);
setResultConverter(dialogButton -> dialogButton != cancelButtonType ? wallet : null);
}
@@ -21,6 +21,7 @@ public class WalletPasswordDialog extends Dialog<SecureString> {
private final CustomPasswordField password;
private final CustomPasswordField passwordConfirm;
private final CheckBox backupExisting;
private final CheckBox changePassword;
public WalletPasswordDialog(PasswordRequirement requirement) {
this(null, requirement);
@@ -31,6 +32,7 @@ public class WalletPasswordDialog extends Dialog<SecureString> {
this.password = (CustomPasswordField)TextFields.createClearablePasswordField();
this.passwordConfirm = (CustomPasswordField)TextFields.createClearablePasswordField();
this.backupExisting = new CheckBox("Backup existing wallet first");
this.changePassword = new CheckBox("Change password");
final DialogPane dialogPane = getDialogPane();
setTitle("Wallet Password" + (walletName != null ? " - " + walletName : ""));
@@ -55,6 +57,10 @@ public class WalletPasswordDialog extends Dialog<SecureString> {
backupExisting.setSelected(true);
}
if(requirement == PasswordRequirement.UPDATE_SET) {
content.getChildren().add(changePassword);
}
dialogPane.setContent(content);
ValidationSupport validationSupport = new ValidationSupport();
@@ -70,12 +76,12 @@ public class WalletPasswordDialog extends Dialog<SecureString> {
BooleanBinding isInvalid = Bindings.createBooleanBinding(() -> passwordConfirm.isVisible() && !password.getText().equals(passwordConfirm.getText()), password.textProperty(), passwordConfirm.textProperty());
okButton.disableProperty().bind(isInvalid);
if(requirement != PasswordRequirement.UPDATE_NEW) {
if(requirement != PasswordRequirement.UPDATE_NEW && requirement != PasswordRequirement.UPDATE_CHANGE) {
passwordConfirm.setVisible(false);
passwordConfirm.setManaged(false);
}
if(requirement == PasswordRequirement.UPDATE_NEW || requirement == PasswordRequirement.UPDATE_EMPTY) {
if(requirement == PasswordRequirement.UPDATE_NEW || requirement == PasswordRequirement.UPDATE_EMPTY || requirement == PasswordRequirement.UPDATE_CHANGE) {
password.textProperty().addListener((observable, oldValue, newValue) -> {
if(newValue.isEmpty()) {
okButton.setText("No Password");
@@ -100,11 +106,16 @@ public class WalletPasswordDialog extends Dialog<SecureString> {
return backupExisting.isSelected();
}
public boolean isChangePassword() {
return changePassword.isSelected();
}
public enum PasswordRequirement {
LOAD("Please enter the wallet password:", "Unlock"),
UPDATE_NEW("Add a password to the wallet?\nLeave empty for none:", "No Password"),
UPDATE_EMPTY("This wallet has no password.\nAdd a password to the wallet?\nLeave empty for none:", "No Password"),
UPDATE_SET("Please re-enter the wallet password:", "Verify Password");
UPDATE_SET("Please re-enter the wallet password:", "Verify Password"),
UPDATE_CHANGE("Enter the new wallet password.\nLeave empty for none:", "No Password");
private final String description;
private final String okButtonText;
@@ -15,8 +15,8 @@ import org.controlsfx.control.ToggleSwitch;
public class WelcomeDialog extends Dialog<Mode> {
private static final String[] ELECTRUM_SERVERS = new String[]{
"ElectrumX (Recommended)", "https://github.com/spesmilo/electrumx",
"electrs (Experimental)", "https://github.com/romanz/electrs",
"esplora-electrs (Experimental)", "https://github.com/Blockstream/electrs"};
"electrs", "https://github.com/romanz/electrs",
"esplora-electrs", "https://github.com/Blockstream/electrs"};
private final HostServices hostServices;
@@ -0,0 +1,25 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet;
/**
* This event is used by the DeviceSignMessageDialog to indicate that a USB device has signed a message
*
*/
public class MessageSignedEvent {
private final Wallet wallet;
private final String signature;
public MessageSignedEvent(Wallet wallet, String signature) {
this.wallet = wallet;
this.signature = signature;
}
public Wallet getWallet() {
return wallet;
}
public String getSignature() {
return signature;
}
}
@@ -20,6 +20,6 @@ public class SettingsChangedEvent {
}
public enum Type {
POLICY, SCRIPT_TYPE, MUTLISIG_THRESHOLD, MULTISIG_TOTAL, KEYSTORE_LABEL, KEYSTORE_FINGERPRINT, KEYSTORE_DERIVATION, KEYSTORE_XPUB;
POLICY, SCRIPT_TYPE, MUTLISIG_THRESHOLD, MULTISIG_TOTAL, KEYSTORE_LABEL, KEYSTORE_FINGERPRINT, KEYSTORE_DERIVATION, KEYSTORE_XPUB, GAP_LIMIT;
}
}
@@ -0,0 +1,13 @@
package com.sparrowwallet.sparrow.event;
public class TorStatusEvent {
private final String status;
public TorStatusEvent(String status) {
this.status = status;
}
public String getStatus() {
return status;
}
}
@@ -0,0 +1,13 @@
package com.sparrowwallet.sparrow.event;
public class VersionCheckStatusEvent {
private final boolean enabled;
public VersionCheckStatusEvent(boolean enabled) {
this.enabled = enabled;
}
public boolean isEnabled() {
return enabled;
}
}
@@ -0,0 +1,13 @@
package com.sparrowwallet.sparrow.event;
public class VersionUpdatedEvent {
private final String version;
public VersionUpdatedEvent(String version) {
this.version = version;
}
public String getVersion() {
return version;
}
}
@@ -0,0 +1,41 @@
package com.sparrowwallet.sparrow.event;
public class WalletHistoryStatusEvent {
private final boolean loaded;
private final String statusMessage;
private final String errorMessage;
public WalletHistoryStatusEvent(boolean loaded) {
this.loaded = loaded;
this.statusMessage = null;
this.errorMessage = null;
}
public WalletHistoryStatusEvent(boolean loaded, String statusMessage) {
this.loaded = false;
this.statusMessage = statusMessage;
this.errorMessage = null;
}
public WalletHistoryStatusEvent(String errorMessage) {
this.loaded = false;
this.statusMessage = null;
this.errorMessage = errorMessage;
}
public boolean isLoading() {
return !loaded;
}
public boolean isLoaded() {
return loaded;
}
public String getStatusMessage() {
return statusMessage;
}
public String getErrorMessage() {
return errorMessage;
}
}
@@ -3,6 +3,7 @@ package com.sparrowwallet.sparrow.io;
import com.google.gson.*;
import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.sparrow.Mode;
import com.sparrowwallet.sparrow.Theme;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -23,6 +24,8 @@ public class Config {
private boolean groupByAddress = true;
private boolean includeMempoolChange = true;
private boolean notifyNewTransactions = true;
private boolean checkNewVersions = true;
private Theme theme;
private List<File> recentWalletFiles;
private Integer keyDerivationPeriod;
private File hwi;
@@ -135,6 +138,24 @@ public class Config {
flush();
}
public boolean isCheckNewVersions() {
return checkNewVersions;
}
public void setCheckNewVersions(boolean checkNewVersions) {
this.checkNewVersions = checkNewVersions;
flush();
}
public Theme getTheme() {
return theme;
}
public void setTheme(Theme theme) {
this.theme = theme;
flush();
}
public List<File> getRecentWalletFiles() {
return recentWalletFiles;
}
@@ -9,6 +9,8 @@ import com.sparrowwallet.drongo.crypto.*;
import com.sparrowwallet.drongo.policy.Policy;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.wallet.*;
import java.io.*;
@@ -81,6 +83,38 @@ public class Electrum implements KeystoreFileImport, WalletImport, WalletExport
}
ew.keystores.put(key, ek);
}
if(key.equals("labels")) {
JsonObject jsonObject = (JsonObject)map.get(key);
for(String labelKey : jsonObject.keySet()) {
ew.labels.put(labelKey, jsonObject.get(labelKey).getAsString());
}
}
if(key.equals("verified_tx3")) {
JsonObject jsonObject = (JsonObject)map.get(key);
for(String txKey : jsonObject.keySet()) {
Sha256Hash txHash = Sha256Hash.wrap(txKey);
JsonArray array = jsonObject.getAsJsonArray(txKey);
if(array != null && array.size() > 3) {
int height = array.get(0).getAsInt();
Date date = new Date(array.get(1).getAsLong() * 1000);
long fee = array.get(2).getAsLong();
Sha256Hash blockHash = Sha256Hash.wrap(array.get(3).getAsString());
JsonObject transactions = (JsonObject)map.get("transactions");
if(transactions != null) {
String txhex = transactions.get(txKey).getAsString();
if(txhex != null) {
Transaction transaction = new Transaction(Utils.hexToBytes(txhex));
BlockTransaction blockTransaction = new BlockTransaction(txHash, height, date, fee, transaction, blockHash);
ew.transactions.put(txHash, blockTransaction);
}
}
}
}
}
}
Wallet wallet = new Wallet();
@@ -150,6 +184,20 @@ public class Electrum implements KeystoreFileImport, WalletImport, WalletExport
throw new ImportException("Unknown Electrum wallet type of " + ew.wallet_type);
}
for(String key : ew.labels.keySet()) {
try {
Sha256Hash txHash = Sha256Hash.wrap(key);
BlockTransaction blockTransaction = ew.transactions.get(txHash);
if(blockTransaction != null) {
blockTransaction.setLabel(ew.labels.get(key));
}
} catch(IllegalArgumentException e) {
//not a tx, probably an address
}
}
wallet.updateTransactions(ew.transactions);
if(!wallet.isValid()) {
throw new IllegalStateException("Electrum wallet is in an inconsistent state.");
}
@@ -279,6 +327,8 @@ public class Electrum implements KeystoreFileImport, WalletImport, WalletExport
public String wallet_type;
public String seed_type;
public Boolean use_encryption;
public Map<String, String> labels = new LinkedHashMap<>();
public Map<Sha256Hash, BlockTransaction> transactions = new HashMap<>();
}
public static class ElectrumKeystore {
@@ -23,7 +23,6 @@ import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.*;
import java.util.stream.Collectors;
import java.util.zip.GZIPInputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
@@ -73,7 +72,7 @@ public class Hwi {
public String getXpub(Device device, String passphrase, String derivationPath) throws ImportException {
try {
String output;
if(passphrase != null && !passphrase.isEmpty() && device.getModel().equals(WalletModel.TREZOR_1)) {
if(passphrase != null && !passphrase.isEmpty() && device.getModel().externalPassphraseEntry()) {
output = execute(getDeviceCommand(device, passphrase, Command.GET_XPUB, derivationPath));
} else {
output = execute(getDeviceCommand(device, Command.GET_XPUB, derivationPath));
@@ -106,7 +105,7 @@ public class Hwi {
isPromptActive = false;
String output;
if(passphrase != null && !passphrase.isEmpty() && device.getModel().equals(WalletModel.TREZOR_1)) {
if(passphrase != null && !passphrase.isEmpty() && device.getModel().externalPassphraseEntry()) {
output = execute(getDeviceCommand(device, passphrase, Command.DISPLAY_ADDRESS, "--path", derivationPath, type));
} else {
output = execute(getDeviceCommand(device, Command.DISPLAY_ADDRESS, "--path", derivationPath, type));
@@ -116,20 +115,51 @@ public class Hwi {
if(result.get("address") != null) {
return result.get("address").getAsString();
} else {
throw new DisplayAddressException("Could not retrieve address");
JsonElement error = result.get("error");
if(error != null) {
throw new DisplayAddressException(error.getAsString());
} else {
throw new DisplayAddressException("Could not retrieve address");
}
}
} catch(IOException e) {
throw new DisplayAddressException(e);
}
}
public String signMessage(Device device, String passphrase, String message, String derivationPath) throws SignMessageException {
try {
isPromptActive = false;
String output;
if(passphrase != null && !passphrase.isEmpty() && device.getModel().externalPassphraseEntry()) {
output = execute(getDeviceCommand(device, passphrase, Command.SIGN_MESSAGE, message, derivationPath));
} else {
output = execute(getDeviceCommand(device, Command.SIGN_MESSAGE, message, derivationPath));
}
JsonObject result = JsonParser.parseString(output).getAsJsonObject();
if(result.get("signature") != null) {
return result.get("signature").getAsString();
} else {
JsonElement error = result.get("error");
if(error != null) {
throw new SignMessageException(error.getAsString());
} else {
throw new SignMessageException("Could not sign message");
}
}
} catch(IOException e) {
throw new SignMessageException(e);
}
}
public PSBT signPSBT(Device device, String passphrase, PSBT psbt) throws SignTransactionException {
try {
String psbtBase64 = psbt.toBase64String();
isPromptActive = true;
String output;
if(passphrase != null && !passphrase.isEmpty() && device.getModel().equals(WalletModel.TREZOR_1)) {
if(passphrase != null && !passphrase.isEmpty() && device.getModel().externalPassphraseEntry()) {
output = execute(getDeviceCommand(device, passphrase, Command.SIGN_TX, psbtBase64));
} else {
output = execute(getDeviceCommand(device, Command.SIGN_TX, psbtBase64));
@@ -169,7 +199,7 @@ public class Hwi {
File hwiExecutable = Config.get().getHwi();
if(hwiExecutable != null && hwiExecutable.exists()) {
if(command.isTestFirst() && (!hwiExecutable.getAbsolutePath().contains(TEMP_FILE_PREFIX) || !testHwi(hwiExecutable))) {
if(Platform.getCurrent().getPlatformId().toLowerCase().equals("mac")) {
if(Platform.getCurrent() == Platform.OSX) {
deleteDirectory(hwiExecutable.getParentFile());
} else {
hwiExecutable.delete();
@@ -187,7 +217,7 @@ 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("/external/mac/hwi-1.1.2-mac-amd64-signed.zip");
InputStream inputStream = Hwi.class.getResourceAsStream("/native/osx/x64/hwi-1.1.2-mac-amd64-signed.zip");
Path tempHwiDirPath = Files.createTempDirectory(TEMP_FILE_PREFIX, PosixFilePermissions.asFileAttribute(ownerExecutableWritable));
File tempHwiDir = tempHwiDirPath.toFile();
//tempHwiDir.deleteOnExit();
@@ -218,10 +248,10 @@ public class Hwi {
InputStream inputStream;
Path tempExecPath;
if(platform == Platform.WINDOWS) {
inputStream = Hwi.class.getResourceAsStream("/external/windows/hwi.exe");
inputStream = Hwi.class.getResourceAsStream("/native/windows/x64/hwi.exe");
tempExecPath = Files.createTempFile(TEMP_FILE_PREFIX, null);
} else {
inputStream = Hwi.class.getResourceAsStream("/external/linux/hwi");
inputStream = Hwi.class.getResourceAsStream("/native/linux/x64/hwi");
tempExecPath = Files.createTempFile(TEMP_FILE_PREFIX, null, PosixFilePermissions.asFileAttribute(ownerExecutableWritable));
}
@@ -409,6 +439,30 @@ public class Hwi {
}
}
public static class SignMessageService extends Service<String> {
private final Device device;
private final String passphrase;
private final String message;
private final String derivationPath;
public SignMessageService(Device device, String passphrase, String message, String derivationPath) {
this.device = device;
this.passphrase = passphrase;
this.message = message;
this.derivationPath = derivationPath;
}
@Override
protected Task<String> createTask() {
return new Task<>() {
protected String call() throws SignMessageException {
Hwi hwi = new Hwi();
return hwi.signMessage(device, passphrase, message, derivationPath);
}
};
}
}
public static class GetXpubService extends Service<String> {
private final Device device;
private final String passphrase;
@@ -479,6 +533,7 @@ public class Hwi {
PROMPT_PIN("promptpin", true),
SEND_PIN("sendpin", false),
DISPLAY_ADDRESS("displayaddress", true),
SIGN_MESSAGE("signmessage", true),
GET_XPUB("getxpub", true),
SIGN_TX("signtx", true);
@@ -0,0 +1,19 @@
package com.sparrowwallet.sparrow.io;
public class SignMessageException extends Exception {
public SignMessageException() {
super();
}
public SignMessageException(String message) {
super(message);
}
public SignMessageException(Throwable cause) {
super(cause);
}
public SignMessageException(String message, Throwable cause) {
super(message, cause);
}
}
@@ -0,0 +1,93 @@
package com.sparrowwallet.sparrow.io;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.sparrowwallet.drongo.OutputDescriptor;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletModel;
import java.io.File;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
public class Specter implements WalletImport, WalletExport {
@Override
public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException {
try {
SpecterWallet specterWallet = new SpecterWallet();
specterWallet.label = wallet.getName();
specterWallet.blockheight = wallet.getStoredBlockHeight();
specterWallet.descriptor = OutputDescriptor.getOutputDescriptor(wallet).toString(true);
Gson gson = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
String json = gson.toJson(specterWallet);
outputStream.write(json.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
outputStream.close();
} catch(Exception e) {
throw new ExportException(e);
}
}
@Override
public String getWalletExportDescription() {
return "Export a Specter wallet that can be read by Specter Desktop using Add new wallet > Import from wallet software";
}
@Override
public String getExportFileExtension() {
return "json";
}
@Override
public String getWalletImportDescription() {
return "Import a Specter wallet created by using the Wallets > Settings > Export to Wallet Software in Specter Desktop";
}
@Override
public Wallet importWallet(InputStream inputStream, String password) throws ImportException {
try {
Gson gson = new Gson();
SpecterWallet specterWallet = gson.fromJson(new InputStreamReader(inputStream), SpecterWallet.class);
if(specterWallet.descriptor != null) {
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(specterWallet.descriptor);
Wallet wallet = outputDescriptor.toWallet();
wallet.setName(specterWallet.label);
if(!wallet.isValid()) {
throw new ImportException("Specter wallet file did not contain a valid wallet");
}
return wallet;
}
} catch(Exception e) {
throw new ImportException(e);
}
throw new ImportException("File was not a valid Specter wallet");
}
@Override
public boolean isEncrypted(File file) {
return false;
}
@Override
public String getName() {
return "Specter";
}
@Override
public WalletModel getWalletModel() {
return WalletModel.SPECTER;
}
public static class SpecterWallet {
public String label;
public Integer blockheight;
public String descriptor;
}
}
@@ -84,6 +84,7 @@ public class Storage {
Wallet wallet = gson.fromJson(reader, Wallet.class);
reader.close();
encryptionPubKey = NO_PASSWORD_KEY;
return wallet;
}
@@ -1,17 +1,16 @@
package com.sparrowwallet.sparrow.keystoreimport;
import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.KeystoreSource;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletModel;
import com.sparrowwallet.sparrow.AppController;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.KeystoreImportEvent;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.ButtonBar;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Dialog;
import javafx.scene.control.DialogPane;
import javafx.scene.control.*;
import org.controlsfx.tools.Borders;
import java.io.IOException;
@@ -36,17 +35,18 @@ public class KeystoreImportDialog extends Dialog<Keystore> {
try {
FXMLLoader ksiLoader = new FXMLLoader(AppController.class.getResource("keystoreimport/keystoreimport.fxml"));
dialogPane.setContent(Borders.wrap(ksiLoader.load()).lineBorder().outerPadding(0).innerPadding(0).buildAll());
dialogPane.setContent(Borders.wrap(ksiLoader.load()).emptyBorder().buildAll());
keystoreImportController = ksiLoader.getController();
keystoreImportController.initializeView(wallet);
keystoreImportController.selectSource(initialSource);
final ButtonType watchOnlyButtonType = new javafx.scene.control.ButtonType("xPub / Watch Only Wallet", ButtonBar.ButtonData.LEFT);
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
dialogPane.getButtonTypes().addAll(cancelButtonType);
dialogPane.getButtonTypes().addAll(watchOnlyButtonType, cancelButtonType);
dialogPane.setPrefWidth(650);
dialogPane.setPrefHeight(620);
setResultConverter(dialogButton -> dialogButton != cancelButtonType ? keystore : null);
setResultConverter(dialogButton -> dialogButton != cancelButtonType ? getWatchOnlyKeystore() : null);
} catch(IOException e) {
throw new RuntimeException(e);
}
@@ -56,6 +56,14 @@ public class KeystoreImportDialog extends Dialog<Keystore> {
return List.of(KeystoreSource.HW_USB, KeystoreSource.HW_AIRGAPPED, KeystoreSource.SW_SEED);
}
private Keystore getWatchOnlyKeystore() {
this.keystore = new Keystore();
keystore.setSource(KeystoreSource.SW_WATCH);
keystore.setWalletModel(WalletModel.SPARROW);
keystore.setKeyDerivation(new KeyDerivation("",""));
return keystore;
}
@Subscribe
public void keystoreImported(KeystoreImportEvent event) {
this.keystore = event.getKeystore();
@@ -6,6 +6,8 @@ import com.github.arteam.simplejsonrpc.client.builder.BatchRequestBuilder;
import com.github.arteam.simplejsonrpc.client.exception.JsonRpcBatchException;
import com.github.arteam.simplejsonrpc.client.exception.JsonRpcException;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.WalletHistoryStatusEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -61,6 +63,7 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
public Map<String, ScriptHashTx[]> getScriptHashHistory(Transport transport, Map<String, String> pathScriptHashes, boolean failOnError) {
JsonRpcClient client = new JsonRpcClient(transport);
BatchRequestBuilder<String, ScriptHashTx[]> batchRequest = client.createBatchRequest().keysType(String.class).returnType(ScriptHashTx[].class);
EventManager.get().post(new WalletHistoryStatusEvent(false, "Loading transactions"));
for(String path : pathScriptHashes.keySet()) {
batchRequest.add(path, "blockchain.scripthash.get_history", pathScriptHashes.get(path));
@@ -112,6 +115,7 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
public Map<String, String> subscribeScriptHashes(Transport transport, Map<String, String> pathScriptHashes) {
JsonRpcClient client = new JsonRpcClient(transport);
BatchRequestBuilder<String, String> batchRequest = client.createBatchRequest().keysType(String.class).returnType(String.class);
EventManager.get().post(new WalletHistoryStatusEvent(false, "Finding transactions"));
for(String path : pathScriptHashes.keySet()) {
batchRequest.add(path, "blockchain.scripthash.subscribe", pathScriptHashes.get(path));
@@ -130,6 +134,8 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
public Map<Integer, String> getBlockHeaders(Transport transport, Set<Integer> blockHeights) {
JsonRpcClient client = new JsonRpcClient(transport);
BatchRequestBuilder<Integer, String> batchRequest = client.createBatchRequest().keysType(Integer.class).returnType(String.class);
EventManager.get().post(new WalletHistoryStatusEvent(false, "Retrieving blocks"));
for(Integer height : blockHeights) {
batchRequest.add(height, "blockchain.block.header", height);
}
@@ -146,6 +152,8 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
public Map<String, String> getTransactions(Transport transport, Set<String> txids) {
JsonRpcClient client = new JsonRpcClient(transport);
BatchRequestBuilder<String, String> batchRequest = client.createBatchRequest().keysType(String.class).returnType(String.class);
EventManager.get().post(new WalletHistoryStatusEvent(false, "Retrieving transactions"));
for(String txid : txids) {
batchRequest.add(txid, "blockchain.transaction.get", txid);
}
@@ -1,6 +1,8 @@
package com.sparrowwallet.sparrow.net;
import com.github.arteam.simplejsonrpc.client.Transport;
import com.google.common.collect.Iterables;
import com.google.common.eventbus.Subscribe;
import com.google.common.net.HostAndPort;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.Utils;
@@ -8,8 +10,11 @@ import com.sparrowwallet.drongo.protocol.*;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.event.ConnectionEvent;
import com.sparrowwallet.sparrow.event.FeeRatesUpdatedEvent;
import com.sparrowwallet.sparrow.event.TorStatusEvent;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.wallet.SendController;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.concurrent.ScheduledService;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
@@ -29,7 +34,7 @@ public class ElectrumServer {
private static Transport transport;
private static final Map<String, String> subscribedScriptHashes = Collections.synchronizedMap(new HashMap<>());
private static final Map<String, Set<String>> subscribedScriptHashes = Collections.synchronizedMap(new HashMap<>());
private static ElectrumServerRpc electrumServerRpc = new SimpleElectrumServerRpc();
@@ -130,43 +135,53 @@ public class ElectrumServer {
return receiveTransactionMap;
}
public Map<WalletNode, Set<BlockTransactionHash>> getHistory(Wallet wallet, WalletNode walletNode) throws ServerException {
Map<WalletNode, Set<BlockTransactionHash>> nodeTransactionMap = new TreeMap<>();
Collection<WalletNode> nodes = Set.of(walletNode);
subscribeWalletNodes(wallet, nodes, nodeTransactionMap, 0);
getReferences(wallet, nodes, nodeTransactionMap);
return nodeTransactionMap;
}
public void getHistory(Wallet wallet, KeyPurpose keyPurpose, Map<WalletNode, Set<BlockTransactionHash>> nodeTransactionMap) throws ServerException {
WalletNode purposeNode = wallet.getNode(keyPurpose);
getHistory(wallet, purposeNode.getChildren(), nodeTransactionMap, 0);
subscribeWalletNodes(wallet, purposeNode.getChildren(), nodeTransactionMap, 0);
//Because node children are added sequentially in WalletNode.fillToIndex, we can simply look at the number of children to determine the highest filled index
int historySize = purposeNode.getChildren().size();
//The gap limit size takes the highest used index in the retrieved history and adds the gap limit (plus one to be comparable to the number of children since index is zero based)
int gapLimitSize = getGapLimitSize(nodeTransactionMap);
int gapLimitSize = getGapLimitSize(wallet, nodeTransactionMap);
while(historySize < gapLimitSize) {
purposeNode.fillToIndex(gapLimitSize - 1);
getHistory(wallet, purposeNode.getChildren(), nodeTransactionMap, historySize);
subscribeWalletNodes(wallet, purposeNode.getChildren(), nodeTransactionMap, historySize);
historySize = purposeNode.getChildren().size();
gapLimitSize = getGapLimitSize(nodeTransactionMap);
gapLimitSize = getGapLimitSize(wallet, nodeTransactionMap);
}
//All WalletNode keys in nodeTransactionMap with non-null values need to have their history fetched
Collection<WalletNode> usedNodes = nodeTransactionMap.entrySet().stream().filter(entry -> entry.getValue() != null).map(Map.Entry::getKey).collect(Collectors.toList());
log.debug("Retrieving history for " + usedNodes.stream().map(WalletNode::getDerivationPath).collect(Collectors.joining(", ")));
getReferences(wallet, usedNodes, nodeTransactionMap);
//Set the remaining WalletNode keys to empty sets to indicate no history
nodeTransactionMap.entrySet().stream().filter(entry -> entry.getValue() == null).forEach(entry -> entry.setValue(Collections.emptySet()));
}
private int getGapLimitSize(Map<WalletNode, Set<BlockTransactionHash>> nodeTransactionMap) {
private int getGapLimitSize(Wallet wallet, Map<WalletNode, Set<BlockTransactionHash>> nodeTransactionMap) {
int highestIndex = nodeTransactionMap.entrySet().stream().filter(entry -> !entry.getValue().isEmpty()).map(entry -> entry.getKey().getIndex()).max(Comparator.comparing(Integer::valueOf)).orElse(-1);
return highestIndex + Wallet.DEFAULT_LOOKAHEAD + 1;
return highestIndex + wallet.getGapLimit() + 1;
}
public void getHistory(Wallet wallet, Collection<WalletNode> nodes, Map<WalletNode, Set<BlockTransactionHash>> nodeTransactionMap, int startIndex) throws ServerException {
getReferences(wallet, "blockchain.scripthash.get_history", nodes, nodeTransactionMap, startIndex);
subscribeWalletNodes(wallet, nodes, startIndex);
}
public void getMempool(Wallet wallet, Collection<WalletNode> nodes, Map<WalletNode, Set<BlockTransactionHash>> nodeTransactionMap, int startIndex) throws ServerException {
getReferences(wallet, "blockchain.scripthash.get_mempool", nodes, nodeTransactionMap, startIndex);
}
public void getReferences(Wallet wallet, String method, Collection<WalletNode> nodes, Map<WalletNode, Set<BlockTransactionHash>> nodeTransactionMap, int startIndex) throws ServerException {
public void getReferences(Wallet wallet, Collection<WalletNode> nodes, Map<WalletNode, Set<BlockTransactionHash>> nodeTransactionMap) throws ServerException {
try {
Map<String, String> pathScriptHashes = new LinkedHashMap<>(nodes.size());
for(WalletNode node : nodes) {
if(node.getIndex() >= startIndex) {
pathScriptHashes.put(node.getDerivationPath(), getScriptHash(wallet, node));
}
pathScriptHashes.put(node.getDerivationPath(), getScriptHash(wallet, node));
}
if(pathScriptHashes.isEmpty()) {
return;
}
//Even if we have some successes, failure to retrieve all references will result in an incomplete wallet history. Don't proceed if that's the case.
@@ -207,14 +222,18 @@ public class ElectrumServer {
}
}
public void subscribeWalletNodes(Wallet wallet, Collection<WalletNode> nodes, int startIndex) throws ServerException {
public void subscribeWalletNodes(Wallet wallet, Collection<WalletNode> nodes, Map<WalletNode, Set<BlockTransactionHash>> nodeTransactionMap, int startIndex) throws ServerException {
try {
Set<String> scriptHashes = new HashSet<>();
Map<String, String> pathScriptHashes = new LinkedHashMap<>();
for(WalletNode node : nodes) {
if(node.getIndex() >= startIndex) {
String scriptHash = getScriptHash(wallet, node);
if(!subscribedScriptHashes.containsKey(scriptHash) && scriptHashes.add(scriptHash)) {
if(getSubscribedScriptHashStatus(scriptHash) != null) {
//Already subscribed, but still need to fetch history from a used node
nodeTransactionMap.put(node, new TreeSet<>());
} else if(!subscribedScriptHashes.containsKey(scriptHash) && scriptHashes.add(scriptHash)) {
//Unique script hash we are not yet subscribed to
pathScriptHashes.put(node.getDerivationPath(), scriptHash);
}
}
@@ -232,7 +251,15 @@ public class ElectrumServer {
Optional<WalletNode> optionalNode = nodes.stream().filter(n -> n.getDerivationPath().equals(path)).findFirst();
if(optionalNode.isPresent()) {
WalletNode node = optionalNode.get();
subscribedScriptHashes.put(getScriptHash(wallet, node), status);
String scriptHash = getScriptHash(wallet, node);
//Check if there is history for this script hash
if(status != null) {
//Set the value for this node to be an empty set to mark it as requiring a get_history RPC call for this wallet
nodeTransactionMap.put(node, new TreeSet<>());
}
updateSubscribedScriptHashStatus(scriptHash, status);
}
}
} catch (ElectrumServerRpcException e) {
@@ -539,10 +566,24 @@ public class ElectrumServer {
return Utils.bytesToHex(reversed);
}
public static Map<String, String> getSubscribedScriptHashes() {
public static Map<String, Set<String>> getSubscribedScriptHashes() {
return subscribedScriptHashes;
}
public static String getSubscribedScriptHashStatus(String scriptHash) {
Set<String> existingStatuses = subscribedScriptHashes.get(scriptHash);
if(existingStatuses != null) {
return Iterables.getLast(existingStatuses);
}
return null;
}
public static void updateSubscribedScriptHashStatus(String scriptHash, String status) {
Set<String> existingStatuses = subscribedScriptHashes.computeIfAbsent(scriptHash, k -> new LinkedHashSet<>());
existingStatuses.add(status);
}
public static boolean supportsBatching(List<String> serverVersion) {
return serverVersion.size() > 0 && serverVersion.get(0).toLowerCase().contains("electrumx");
}
@@ -578,6 +619,7 @@ public class ElectrumServer {
private boolean firstCall = true;
private Thread reader;
private long feeRatesRetrievedAt;
private StringProperty statusProperty = new SimpleStringProperty();
public ConnectionService() {
this(true);
@@ -600,6 +642,9 @@ public class ElectrumServer {
reader.setUncaughtExceptionHandler(ConnectionService.this);
reader.start();
//Start with simple RPC for maximum compatibility
electrumServerRpc = new SimpleElectrumServerRpc();
List<String> serverVersion = electrumServer.getServerVersion();
firstCall = false;
@@ -607,8 +652,6 @@ public class ElectrumServer {
if(supportsBatching(serverVersion)) {
log.debug("Upgrading to batched JSON-RPC");
electrumServerRpc = new BatchedElectrumServerRpc();
} else {
electrumServerRpc = new SimpleElectrumServerRpc();
}
BlockHeaderTip tip;
@@ -675,6 +718,15 @@ public class ElectrumServer {
public void uncaughtException(Thread t, Throwable e) {
log.error("Uncaught error in ConnectionService", e);
}
@Subscribe
public void torStatus(TorStatusEvent event) {
statusProperty.set(event.getStatus());
}
public StringProperty statusProperty() {
return statusProperty;
}
}
public static class ReadRunnable implements Runnable {
@@ -692,9 +744,16 @@ public class ElectrumServer {
public static class TransactionHistoryService extends Service<Boolean> {
private final Wallet wallet;
private final WalletNode node;
public TransactionHistoryService(Wallet wallet) {
this.wallet = wallet;
this.node = null;
}
public TransactionHistoryService(Wallet wallet, WalletNode node) {
this.wallet = wallet;
this.node = node;
}
@Override
@@ -702,7 +761,7 @@ public class ElectrumServer {
return new Task<>() {
protected Boolean call() throws ServerException {
ElectrumServer electrumServer = new ElectrumServer();
Map<WalletNode, Set<BlockTransactionHash>> nodeTransactionMap = electrumServer.getHistory(wallet);
Map<WalletNode, Set<BlockTransactionHash>> nodeTransactionMap = (node == null ? electrumServer.getHistory(wallet) : electrumServer.getHistory(wallet, node));
electrumServer.getReferencedTransactions(wallet, nodeTransactionMap);
electrumServer.calculateNodeHistory(wallet, nodeTransactionMap);
return true;
@@ -14,12 +14,16 @@ public enum Protocol {
TCP {
@Override
public Transport getTransport(HostAndPort server) {
if(isOnionAddress(server)) {
return new TorTcpTransport(server);
}
return new TcpTransport(server);
}
@Override
public Transport getTransport(HostAndPort server, File serverCert) {
return new TcpTransport(server);
return getTransport(server);
}
@Override
@@ -35,11 +39,19 @@ public enum Protocol {
SSL {
@Override
public Transport getTransport(HostAndPort server) throws KeyManagementException, NoSuchAlgorithmException {
if(isOnionAddress(server)) {
return new TorTcpOverTlsTransport(server);
}
return new TcpOverTlsTransport(server);
}
@Override
public Transport getTransport(HostAndPort server, File serverCert) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException {
if(isOnionAddress(server)) {
return new TorTcpOverTlsTransport(server, serverCert);
}
return new TcpOverTlsTransport(server, serverCert);
}
@@ -82,6 +94,10 @@ public enum Protocol {
return toUrlString() + hostAndPort.toString();
}
public boolean isOnionAddress(HostAndPort server) {
return server.getHost().toLowerCase().endsWith(".onion");
}
public static Protocol getProtocol(String url) {
if(url.startsWith("tcp://")) {
return TCP;
@@ -7,13 +7,13 @@ import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.sparrow.AppController;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.WalletHistoryStatusEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
import static com.sparrowwallet.drongo.protocol.Transaction.DUST_RELAY_TX_FEE;
public class SimpleElectrumServerRpc implements ElectrumServerRpc {
private static final Logger log = LoggerFactory.getLogger(SimpleElectrumServerRpc.class);
private static final int MAX_TARGET_BLOCKS = 25;
@@ -65,6 +65,7 @@ public class SimpleElectrumServerRpc implements ElectrumServerRpc {
Map<String, ScriptHashTx[]> result = new LinkedHashMap<>();
for(String path : pathScriptHashes.keySet()) {
EventManager.get().post(new WalletHistoryStatusEvent(false, "Loading transactions for " + path));
try {
ScriptHashTx[] scriptHashTxes = client.createRequest().returnAs(ScriptHashTx[].class).method("blockchain.scripthash.get_history").id(path).params(pathScriptHashes.get(path)).execute();
result.put(path, scriptHashTxes);
@@ -107,9 +108,10 @@ public class SimpleElectrumServerRpc implements ElectrumServerRpc {
Map<String, String> result = new LinkedHashMap<>();
for(String path : pathScriptHashes.keySet()) {
EventManager.get().post(new WalletHistoryStatusEvent(false, "Finding transactions for " + path));
try {
client.createRequest().method("blockchain.scripthash.subscribe").id(path).params(pathScriptHashes.get(path)).executeNullable();
result.put(path, "");
String scriptHash = client.createRequest().returnAs(String.class).method("blockchain.scripthash.subscribe").id(path).params(pathScriptHashes.get(path)).executeNullable();
result.put(path, scriptHash);
} catch(JsonRpcException | IllegalStateException | IllegalArgumentException e) {
//Even if we have some successes, failure to subscribe for all script hashes will result in outdated wallet view. Don't proceed.
throw new ElectrumServerRpcException("Failed to retrieve reference for path: " + path, e);
@@ -125,6 +127,7 @@ public class SimpleElectrumServerRpc implements ElectrumServerRpc {
Map<Integer, String> result = new LinkedHashMap<>();
for(Integer blockHeight : blockHeights) {
EventManager.get().post(new WalletHistoryStatusEvent(false, "Retrieving block at height " + blockHeight));
try {
String blockHeader = client.createRequest().returnAs(String.class).method("blockchain.block.header").id(blockHeight).params(blockHeight).execute();
result.put(blockHeight, blockHeader);
@@ -144,6 +147,7 @@ public class SimpleElectrumServerRpc implements ElectrumServerRpc {
Map<String, String> result = new LinkedHashMap<>();
for(String txid : txids) {
EventManager.get().post(new WalletHistoryStatusEvent(false, "Retrieving transaction [" + txid.substring(0, 6) + "]"));
try {
String rawTxHex = client.createRequest().returnAs(String.class).method("blockchain.transaction.get").id(txid).params(txid).execute();
result.put(txid, rawTxHex);
@@ -3,6 +3,7 @@ package com.sparrowwallet.sparrow.net;
import com.github.arteam.simplejsonrpc.core.annotation.JsonRpcMethod;
import com.github.arteam.simplejsonrpc.core.annotation.JsonRpcParam;
import com.github.arteam.simplejsonrpc.core.annotation.JsonRpcService;
import com.google.common.collect.Iterables;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.NewBlockEvent;
import com.sparrowwallet.sparrow.event.WalletNodeHistoryChangedEvent;
@@ -10,7 +11,7 @@ import javafx.application.Platform;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Objects;
import java.util.Set;
@JsonRpcService
public class SubscriptionService {
@@ -23,9 +24,17 @@ public class SubscriptionService {
@JsonRpcMethod("blockchain.scripthash.subscribe")
public void scriptHashStatusUpdated(@JsonRpcParam("scripthash") final String scriptHash, @JsonRpcParam("status") final String status) {
String oldStatus = ElectrumServer.getSubscribedScriptHashes().put(scriptHash, status);
if(Objects.equals(oldStatus, status)) {
Set<String> existingStatuses = ElectrumServer.getSubscribedScriptHashes().get(scriptHash);
if(existingStatuses == null) {
log.warn("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");
return;
} else {
String oldStatus = Iterables.getLast(existingStatuses);
log.debug("Status updated for script hash " + scriptHash + ", was " + oldStatus + " now " + status);
existingStatuses.add(status);
}
Platform.runLater(() -> EventManager.get().post(new WalletNodeHistoryChangedEvent(scriptHash)));
@@ -11,6 +11,8 @@ import org.slf4j.LoggerFactory;
import javax.net.SocketFactory;
import java.io.*;
import java.net.Socket;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class TcpTransport implements Transport, Closeable {
@@ -25,6 +27,9 @@ public class TcpTransport implements Transport, Closeable {
private String response;
private final ReentrantLock readLock = new ReentrantLock();
private final Condition readingCondition = readLock.newCondition();
private final ReentrantLock clientRequestLock = new ReentrantLock();
private boolean running = false;
private boolean reading = true;
@@ -57,64 +62,83 @@ public class TcpTransport implements Transport, Closeable {
out.flush();
}
private synchronized String readResponse() throws IOException {
if(firstRead) {
notifyAll();
firstRead = false;
}
while(reading) {
try {
wait();
} catch(InterruptedException e) {
//Restore interrupt status and continue
Thread.currentThread().interrupt();
private String readResponse() throws IOException {
try {
if(!readLock.tryLock(60, TimeUnit.SECONDS)) {
throw new IOException("No response from server");
}
} catch(InterruptedException e) {
throw new IOException("Read thread interrupted");
}
if(lastException != null) {
throw new IOException("Error reading response: " + lastException.getMessage(), lastException);
try {
if(firstRead) {
readingCondition.signal();
firstRead = false;
}
while(reading) {
try {
readingCondition.await();
} catch(InterruptedException e) {
//Restore interrupt status and continue
Thread.currentThread().interrupt();
}
}
if(lastException != null) {
throw new IOException("Error reading response: " + lastException.getMessage(), lastException);
}
reading = true;
readingCondition.signal();
return response;
} finally {
readLock.unlock();
}
reading = true;
notifyAll();
return response;
}
public synchronized void readInputLoop() throws ServerException {
try {
//Don't start reading until first RPC request is sent
wait();
} catch(InterruptedException e) {
Thread.currentThread().interrupt();
}
public void readInputLoop() throws ServerException {
readLock.lock();
while(running) {
try {
try {
String received = readInputStream();
if(received.contains("method")) {
//Handle subscription notification
jsonRpcServer.handle(received, subscriptionService);
} else {
//Handle client's response
response = received;
reading = false;
notifyAll();
wait();
}
//Don't start reading until first RPC request is sent
readingCondition.await();
} catch(InterruptedException e) {
//Restore interrupt status and continue
Thread.currentThread().interrupt();
} catch(Exception e) {
if(running) {
lastException = e;
reading = false;
notifyAll();
//Allow this thread to terminate as we will need to reconnect with a new transport anyway
running = false;
}
while(running) {
try {
String received = readInputStream();
if(received.contains("method")) {
//Handle subscription notification
jsonRpcServer.handle(received, subscriptionService);
} else {
//Handle client's response
response = received;
reading = false;
readingCondition.signal();
readingCondition.await();
}
} catch(InterruptedException e) {
//Restore interrupt status and continue
Thread.currentThread().interrupt();
} catch(Exception e) {
log.debug("Connection error while reading", e);
if(running) {
lastException = e;
reading = false;
readingCondition.signal();
//Allow this thread to terminate as we will need to reconnect with a new transport anyway
running = false;
}
}
}
} finally {
readLock.unlock();
}
}
@@ -0,0 +1,49 @@
package com.sparrowwallet.sparrow.net;
import com.google.common.net.HostAndPort;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.net.ssl.SSLSocket;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.net.Socket;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
public class TorTcpOverTlsTransport extends TcpOverTlsTransport {
private static final Logger log = LoggerFactory.getLogger(TorTcpOverTlsTransport.class);
public TorTcpOverTlsTransport(HostAndPort server) throws NoSuchAlgorithmException, KeyManagementException {
super(server);
}
public TorTcpOverTlsTransport(HostAndPort server, File crtFile) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException {
super(server, crtFile);
}
@Override
protected Socket createSocket() throws IOException {
TorTcpTransport torTcpTransport = new TorTcpTransport(server);
Socket socket = torTcpTransport.createSocket();
try {
Field socketField = socket.getClass().getDeclaredField("socket");
socketField.setAccessible(true);
Socket innerSocket = (Socket)socketField.get(socket);
Field connectedField = innerSocket.getClass().getSuperclass().getDeclaredField("connected");
connectedField.setAccessible(true);
connectedField.set(innerSocket, true);
} catch(Exception e) {
log.error("Could not set socket connected status", e);
}
SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(socket, server.getHost(), server.getPortOrDefault(DEFAULT_PORT), true);
sslSocket.startHandshake();
return sslSocket;
}
}
@@ -0,0 +1,55 @@
package com.sparrowwallet.sparrow.net;
import com.google.common.net.HostAndPort;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.StatusEvent;
import com.sparrowwallet.sparrow.event.TorStatusEvent;
import javafx.application.Platform;
import org.berndpruenster.netlayer.tor.*;
import java.io.*;
import java.net.Socket;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.LinkedHashMap;
public class TorTcpTransport extends TcpTransport {
public static final String TOR_DIR_PREFIX = "tor";
public TorTcpTransport(HostAndPort server) {
super(server);
}
@Override
protected Socket createSocket() throws IOException {
if(Tor.getDefault() == null) {
Platform.runLater(() -> {
String status = "Starting Tor...";
EventManager.get().post(new TorStatusEvent(status));
});
Path path = Files.createTempDirectory(TOR_DIR_PREFIX);
File torInstallDir = path.toFile();
torInstallDir.deleteOnExit();
try {
LinkedHashMap<String, String> torrcOptionsMap = new LinkedHashMap<>();
torrcOptionsMap.put("DisableNetwork", "0");
Torrc override = new Torrc(torrcOptionsMap);
NativeTor nativeTor = new NativeTor(torInstallDir, Collections.emptyList(), override);
Tor.setDefault(nativeTor);
} catch(TorCtlException e) {
e.printStackTrace();
throw new IOException(e);
}
}
Platform.runLater(() -> {
String status = "Tor running, connecting to " + server.toString() + "...";
EventManager.get().post(new TorStatusEvent(status));
});
return new TorSocket(server.getHost(), server.getPort(), "sparrow");
}
}
@@ -0,0 +1,151 @@
package com.sparrowwallet.sparrow.net;
import com.google.gson.Gson;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.address.InvalidAddressException;
import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.sparrow.MainApp;
import com.sparrowwallet.sparrow.event.VersionUpdatedEvent;
import javafx.concurrent.ScheduledService;
import javafx.concurrent.Task;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.net.ssl.HttpsURLConnection;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.security.SignatureException;
import java.util.Map;
public class VersionCheckService extends ScheduledService<VersionUpdatedEvent> {
private static final Logger log = LoggerFactory.getLogger(VersionCheckService.class);
private static final String VERSION_CHECK_URL = "https://www.sparrowwallet.com/version";
@Override
protected Task<VersionUpdatedEvent> createTask() {
return new Task<>() {
protected VersionUpdatedEvent call() {
try {
VersionCheck versionCheck = getVersionCheck();
if(isNewer(versionCheck) && verifySignature(versionCheck)) {
return new VersionUpdatedEvent(versionCheck.version);
}
} catch(IOException e) {
log.error("Error retrieving version check file", e);
}
return null;
}
};
}
private VersionCheck getVersionCheck() throws IOException {
URL url = new URL(VERSION_CHECK_URL);
HttpsURLConnection conn = (HttpsURLConnection)url.openConnection();
try(InputStreamReader reader = new InputStreamReader(conn.getInputStream())) {
Gson gson = new Gson();
return gson.fromJson(reader, VersionCheck.class);
}
}
private boolean verifySignature(VersionCheck versionCheck) {
try {
for(String addressString : versionCheck.signatures.keySet()) {
if(!addressString.equals("1LiJx1HQ49L2LzhBwbgwXdHiGodvPg5YaV")) {
log.warn("Invalid address for version check " + addressString);
continue;
}
String signature = versionCheck.signatures.get(addressString);
ECKey signedMessageKey = ECKey.signedMessageToKey(versionCheck.version, signature, false);
Address providedAddress = Address.fromString(addressString);
Address signedMessageAddress = ScriptType.P2PKH.getAddress(signedMessageKey);
if(providedAddress.equals(signedMessageAddress)) {
return true;
} else {
log.warn("Invalid signature for version check " + signature + " from address " + addressString);
}
}
} catch(SignatureException e) {
log.error("Error in version check signature", e);
} catch(InvalidAddressException e) {
log.error("Error in version check address", e);
}
return false;
}
private boolean isNewer(VersionCheck versionCheck) {
try {
Version versionCheckVersion = new Version(versionCheck.version);
Version currentVersion = new Version(MainApp.APP_VERSION);
return versionCheckVersion.compareTo(currentVersion) > 0;
} catch(IllegalArgumentException e) {
log.error("Invalid versions to compare: " + versionCheck.version + " to " + MainApp.APP_VERSION, e);
}
return false;
}
private static class VersionCheck {
public String version;
public Map<String, String> signatures;
}
public static class Version implements Comparable<Version> {
private final String version;
public final String get() {
return this.version;
}
public Version(String version) {
if(version == null) {
throw new IllegalArgumentException("Version can not be null");
}
if(!version.matches("[0-9]+(\\.[0-9]+)*")) {
throw new IllegalArgumentException("Invalid version format");
}
this.version = version;
}
@Override
public int compareTo(Version that) {
if(that == null) {
return 1;
}
String[] thisParts = this.get().split("\\.");
String[] thatParts = that.get().split("\\.");
int length = Math.max(thisParts.length, thatParts.length);
for(int i = 0; i < length; i++) {
int thisPart = i < thisParts.length ? Integer.parseInt(thisParts[i]) : 0;
int thatPart = i < thatParts.length ? Integer.parseInt(thatParts[i]) : 0;
if(thisPart < thatPart) {
return -1;
}
if(thisPart > thatPart) {
return 1;
}
}
return 0;
}
@Override
public boolean equals(Object that) {
if(this == that) {
return true;
}
if(that == null) {
return false;
}
if(this.getClass() != that.getClass()) {
return false;
}
return this.compareTo((Version)that) == 0;
}
}
}
@@ -5,6 +5,7 @@ import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.UnlabeledToggleSwitch;
import com.sparrowwallet.sparrow.event.BitcoinUnitChangedEvent;
import com.sparrowwallet.sparrow.event.FiatCurrencySelectedEvent;
import com.sparrowwallet.sparrow.event.VersionCheckStatusEvent;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.io.ExchangeSource;
import javafx.beans.value.ChangeListener;
@@ -38,6 +39,9 @@ public class GeneralPreferencesController extends PreferencesDetailController {
@FXML
private UnlabeledToggleSwitch notifyNewTransactions;
@FXML
private UnlabeledToggleSwitch checkNewVersions;
private final ChangeListener<Currency> fiatCurrencyListener = new ChangeListener<Currency>() {
@Override
public void changed(ObservableValue<? extends Currency> observable, Currency oldValue, Currency newValue) {
@@ -86,6 +90,12 @@ public class GeneralPreferencesController extends PreferencesDetailController {
notifyNewTransactions.selectedProperty().addListener((observableValue, oldValue, newValue) -> {
config.setNotifyNewTransactions(newValue);
});
checkNewVersions.setSelected(config.isCheckNewVersions());
checkNewVersions.selectedProperty().addListener((observableValue, oldValue, newValue) -> {
config.setCheckNewVersions(newValue);
EventManager.get().post(new VersionCheckStatusEvent(newValue));
});
}
private void updateCurrencies(ExchangeSource exchangeSource) {
@@ -31,7 +31,7 @@ public class PreferencesDialog extends Dialog<Boolean> {
try {
FXMLLoader preferencesLoader = new FXMLLoader(AppController.class.getResource("preferences/preferences.fxml"));
dialogPane.setContent(Borders.wrap(preferencesLoader.load()).lineBorder().outerPadding(0).innerPadding(0).buildAll());
dialogPane.setContent(Borders.wrap(preferencesLoader.load()).emptyBorder().buildAll());
PreferencesController preferencesController = preferencesLoader.getController();
preferencesController.initializeView(Config.get());
if(initialGroup != null) {
@@ -28,6 +28,8 @@ import org.controlsfx.validation.ValidationSupport;
import org.controlsfx.validation.Validator;
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.net.ssl.SSLHandshakeException;
import java.io.File;
@@ -36,6 +38,8 @@ import java.security.cert.CertificateFactory;
import java.util.List;
public class ServerPreferencesController extends PreferencesDetailController {
private static final Logger log = LoggerFactory.getLogger(ServerPreferencesController.class);
@FXML
private TextField host;
@@ -140,13 +144,20 @@ public class ServerPreferencesController extends PreferencesDetailController {
testConnection.setGraphic(getGlyph(FontAwesome5.Glyph.ELLIPSIS_H, null));
ElectrumServer.ConnectionService connectionService = new ElectrumServer.ConnectionService(false);
connectionService.setPeriod(Duration.minutes(1));
connectionService.setPeriod(Duration.ZERO);
EventManager.get().register(connectionService);
connectionService.statusProperty().addListener((observable, oldValue, newValue) -> {
testResults.setText(testResults.getText() + "\n" + newValue);
});
connectionService.setOnSucceeded(successEvent -> {
EventManager.get().unregister(connectionService);
ConnectionEvent connectionEvent = (ConnectionEvent)connectionService.getValue();
showConnectionSuccess(connectionEvent.getServerVersion(), connectionEvent.getServerBanner());
connectionService.cancel();
});
connectionService.setOnFailed(workerStateEvent -> {
EventManager.get().unregister(connectionService);
showConnectionFailure(workerStateEvent);
connectionService.cancel();
});
@@ -216,7 +227,7 @@ public class ServerPreferencesController extends PreferencesDetailController {
}
private void showConnectionSuccess(List<String> serverVersion, String serverBanner) {
testConnection.setGraphic(getGlyph(FontAwesome5.Glyph.CHECK_CIRCLE, Color.rgb(80, 161, 79)));
testConnection.setGraphic(getGlyph(FontAwesome5.Glyph.CHECK_CIRCLE, "success"));
if(serverVersion != null) {
testResults.setText("Connected to " + serverVersion.get(0) + " on protocol version " + serverVersion.get(1));
if(ElectrumServer.supportsBatching(serverVersion)) {
@@ -230,13 +241,14 @@ public class ServerPreferencesController extends PreferencesDetailController {
private void showConnectionFailure(WorkerStateEvent failEvent) {
Throwable e = failEvent.getSource().getException();
log.error("Connection error", e);
String reason = e.getCause() != null ? e.getCause().getMessage() : e.getMessage();
if(e.getCause() != null && e.getCause() instanceof SSLHandshakeException) {
reason = "SSL Handshake Error\n" + reason;
}
testResults.setText("Could not connect:\n\n" + reason);
testConnection.setGraphic(getGlyph(FontAwesome5.Glyph.EXCLAMATION_CIRCLE, Color.rgb(202, 18, 67)));
testConnection.setGraphic(getGlyph(FontAwesome5.Glyph.EXCLAMATION_CIRCLE, "failure"));
}
private void setupValidation() {
@@ -328,11 +340,11 @@ public class ServerPreferencesController extends PreferencesDetailController {
}
}
private Glyph getGlyph(FontAwesome5.Glyph glyphName, Color color) {
private Glyph getGlyph(FontAwesome5.Glyph glyphName, String styleClass) {
Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, glyphName);
glyph.setFontSize(13);
if(color != null) {
glyph.setColor(color);
glyph.setFontSize(12);
if(styleClass != null) {
glyph.getStyleClass().add(styleClass);
}
return glyph;
@@ -49,7 +49,7 @@ import java.time.*;
import java.util.*;
import java.util.stream.Collectors;
public class HeadersController extends TransactionFormController implements Initializable {
public class HeadersController extends TransactionFormController implements Initializable, DynamicUpdate {
private static final Logger log = LoggerFactory.getLogger(HeadersController.class);
public static final String LOCKTIME_DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";
public static final String BLOCK_TIMESTAMP_DATE_FORMAT = "yyyy-MM-dd HH:mm:ss ZZZ";
@@ -121,7 +121,7 @@ public class HeadersController extends TransactionFormController implements Init
private CopyableLabel feeRate;
@FXML
private Form blockchainForm;
private DynamicForm blockchainForm;
@FXML
private Label blockStatus;
@@ -410,6 +410,8 @@ public class HeadersController extends TransactionFormController implements Init
EventManager.get().post(new RequestOpenWalletsEvent());
}
blockchainForm.setDynamicUpdate(this);
}
private void updateType() {
@@ -493,6 +495,8 @@ public class HeadersController extends TransactionFormController implements Init
indicator = (ConfirmationProgressIndicator)blockStatus.getGraphic();
indicator.setConfirmations(confirmations);
}
} else {
blockStatus.setGraphic(null);
}
}
@@ -779,6 +783,19 @@ public class HeadersController extends TransactionFormController implements Init
}
}
@Override
public void update() {
BlockTransaction blockTransaction = headersForm.getBlockTransaction();
Sha256Hash txId = headersForm.getTransaction().getTxId();
if(headersForm.getSigningWallet() != null && headersForm.getSigningWallet().getTransactions().containsKey(txId)) {
blockTransaction = headersForm.getSigningWallet().getTransactions().get(txId);
}
if(blockTransaction != null && AppController.getCurrentBlockHeight() != null) {
updateBlockchainForm(blockTransaction, AppController.getCurrentBlockHeight());
}
}
@Subscribe
public void transactionChanged(TransactionChangedEvent event) {
if(headersForm.getTransaction().equals(event.getTransaction())) {
@@ -81,6 +81,10 @@ public class UR {
}
}
public String toString() {
return UREncoder.encode(this);
}
@Override
public boolean equals(Object o) {
if(this == o) {
@@ -5,10 +5,7 @@ import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.wallet.WalletNode;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.AddressTreeTable;
import com.sparrowwallet.sparrow.event.BitcoinUnitChangedEvent;
import com.sparrowwallet.sparrow.event.WalletEntryLabelChangedEvent;
import com.sparrowwallet.sparrow.event.WalletHistoryChangedEvent;
import com.sparrowwallet.sparrow.event.WalletNodesChangedEvent;
import com.sparrowwallet.sparrow.event.*;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
@@ -0,0 +1,30 @@
package com.sparrowwallet.sparrow.wallet;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.SettingsChangedEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Spinner;
import javafx.scene.control.SpinnerValueFactory;
import java.net.URL;
import java.util.ResourceBundle;
public class AdvancedController implements Initializable {
@FXML
private Spinner<Integer> gapLimit;
@Override
public void initialize(URL location, ResourceBundle resources) {
}
public void initializeView(Wallet wallet) {
gapLimit.setValueFactory(new SpinnerValueFactory.IntegerSpinnerValueFactory(Wallet.DEFAULT_LOOKAHEAD, 10000, wallet.getGapLimit()));
gapLimit.valueProperty().addListener((observable, oldValue, newValue) -> {
wallet.setGapLimit(newValue);
EventManager.get().post(new SettingsChangedEvent(wallet, SettingsChangedEvent.Type.GAP_LIMIT));
});
}
}
@@ -0,0 +1,35 @@
package com.sparrowwallet.sparrow.wallet;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.AppController;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.ButtonBar;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Dialog;
import javafx.scene.control.DialogPane;
import org.controlsfx.tools.Borders;
import java.io.IOException;
public class AdvancedDialog extends Dialog<Void> {
public AdvancedDialog(Wallet wallet) {
final DialogPane dialogPane = getDialogPane();
AppController.setStageIcon(dialogPane.getScene().getWindow());
try {
FXMLLoader advancedLoader = new FXMLLoader(AppController.class.getResource("wallet/advanced.fxml"));
dialogPane.setContent(Borders.wrap(advancedLoader.load()).emptyBorder().buildAll());
AdvancedController settingsAdvancedController = advancedLoader.getController();
settingsAdvancedController.initializeView(wallet);
final ButtonType closeButtonType = new javafx.scene.control.ButtonType("Close", ButtonBar.ButtonData.CANCEL_CLOSE);
dialogPane.getButtonTypes().addAll(closeButtonType);
dialogPane.setPrefWidth(400);
dialogPane.setPrefHeight(300);
}
catch(IOException e) {
throw new RuntimeException(e);
}
}
}
@@ -183,7 +183,7 @@ public class KeystoreController extends WalletFormController implements Initiali
validationSupport.registerValidator(fingerprint, Validator.combine(
Validator.createEmptyValidator("Master fingerprint is required"),
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Master fingerprint is invalid", (newValue.length() != 8 || !Utils.isHex(newValue)))
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Master fingerprint is invalid", (newValue == null || newValue.length() != 8 || !Utils.isHex(newValue)))
));
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
@@ -243,7 +243,7 @@ public class KeystoreController extends WalletFormController implements Initiali
private void launchImportDialog(KeystoreSource initialSource) {
KeystoreImportDialog dlg = new KeystoreImportDialog(getWalletForm().getWallet(), initialSource);
Optional<Keystore> result = dlg.showAndWait();
if (result.isPresent()) {
if(result.isPresent()) {
selectSourcePane.setVisible(false);
Keystore importedKeystore = result.get();
@@ -259,7 +259,12 @@ public class KeystoreController extends WalletFormController implements Initiali
label.setText(keystore.getLabel());
fingerprint.setText(keystore.getKeyDerivation().getMasterFingerprint());
derivation.setText(keystore.getKeyDerivation().getDerivationPath());
xpub.setText(keystore.getExtendedPublicKey().toString());
if(keystore.getExtendedPublicKey() != null) {
xpub.setText(keystore.getExtendedPublicKey().toString());
} else {
xpub.setText("");
}
}
}
@@ -114,7 +114,7 @@ public class ReceiveController extends WalletFormController implements Initializ
scriptPubKeyArea.appendScript(nodeEntry.getOutputScript(), null, null);
outputDescriptor.clear();
outputDescriptor.appendText(nodeEntry.getOutputDescriptor());
outputDescriptor.append(nodeEntry.getOutputDescriptor(), "descriptor-text");
updateDisplayAddress(AppController.getDevices());
}
@@ -13,6 +13,7 @@ import com.sparrowwallet.drongo.wallet.WalletModel;
import com.sparrowwallet.sparrow.AppController;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.CopyableLabel;
import com.sparrowwallet.sparrow.control.DescriptorArea;
import com.sparrowwallet.sparrow.control.WalletPasswordDialog;
import com.sparrowwallet.sparrow.event.SettingsChangedEvent;
import com.sparrowwallet.sparrow.event.StorageEvent;
@@ -20,6 +21,7 @@ import com.sparrowwallet.sparrow.event.TimedEvent;
import com.sparrowwallet.sparrow.io.Storage;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.collections.FXCollections;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
@@ -46,7 +48,7 @@ public class SettingsController extends WalletFormController implements Initiali
private ComboBox<PolicyType> policyType;
@FXML
private TextField spendingMiniscript;
private DescriptorArea descriptor;
@FXML
private ComboBox<ScriptType> scriptType;
@@ -86,7 +88,7 @@ public class SettingsController extends WalletFormController implements Initiali
@Override
public void initializeView() {
keystoreTabs = new TabPane();
keystoreTabsPane.getChildren().add(Borders.wrap(keystoreTabs).etchedBorder().outerPadding(10, 5, 0 ,0).innerPadding(0).raised().buildAll());
keystoreTabsPane.getChildren().add(keystoreTabs);
policyType.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, policyType) -> {
walletForm.getWallet().setPolicyType(policyType);
@@ -160,6 +162,8 @@ public class SettingsController extends WalletFormController implements Initiali
}
});
initializeDescriptorField(descriptor);
revert.setOnAction(event -> {
keystoreTabs.getTabs().removeAll(keystoreTabs.getTabs());
totalKeystores.unbind();
@@ -172,7 +176,7 @@ public class SettingsController extends WalletFormController implements Initiali
apply.setOnAction(event -> {
revert.setDisable(true);
apply.setDisable(true);
saveWallet();
saveWallet(false);
});
setFieldsFromWallet(walletForm.getWallet());
@@ -224,6 +228,7 @@ public class SettingsController extends WalletFormController implements Initiali
KeystoreController controller = keystoreLoader.getController();
controller.setKeystore(getWalletForm(), keystore);
tab.textProperty().bind(controller.getLabel().textProperty());
tab.setUserData(keystore);
controller.getValidationSupport().validationResultProperty().addListener((o, oldValue, result) -> {
if(result.getErrors().isEmpty()) {
@@ -243,6 +248,24 @@ public class SettingsController extends WalletFormController implements Initiali
}
}
public void showAdvanced(ActionEvent event) {
AdvancedDialog advancedDialog = new AdvancedDialog(walletForm.getWallet());
advancedDialog.showAndWait();
}
@Override
protected String describeKeystore(Keystore keystore) {
if(!keystore.isValid()) {
for(Tab tab : keystoreTabs.getTabs()) {
if(tab.getUserData() == keystore && tab.getTooltip() != null) {
return tab.getTooltip().getText();
}
}
}
return super.describeKeystore(keystore);
}
@Subscribe
public void update(SettingsChangedEvent event) {
Wallet wallet = event.getWallet();
@@ -253,18 +276,22 @@ public class SettingsController extends WalletFormController implements Initiali
wallet.setDefaultPolicy(Policy.getPolicy(wallet.getPolicyType(), wallet.getScriptType(), wallet.getKeystores(), (int)multisigControl.getLowValue()));
}
spendingMiniscript.setText(wallet.getDefaultPolicy().getMiniscript().getScript());
descriptor.setWallet(wallet);
revert.setDisable(false);
apply.setDisable(!wallet.isValid());
}
}
private void saveWallet() {
private void saveWallet(boolean changePassword) {
ECKey existingPubKey = walletForm.getStorage().getEncryptionPubKey();
WalletPasswordDialog.PasswordRequirement requirement;
if(existingPubKey == null) {
requirement = WalletPasswordDialog.PasswordRequirement.UPDATE_NEW;
if(changePassword) {
requirement = WalletPasswordDialog.PasswordRequirement.UPDATE_CHANGE;
} else {
requirement = WalletPasswordDialog.PasswordRequirement.UPDATE_NEW;
}
} else if(Storage.NO_PASSWORD_KEY.equals(existingPubKey)) {
requirement = WalletPasswordDialog.PasswordRequirement.UPDATE_EMPTY;
} else {
@@ -313,6 +340,12 @@ public class SettingsController extends WalletFormController implements Initiali
return;
}
if(dlg.isChangePassword()) {
walletForm.getStorage().setEncryptionPubKey(null);
saveWallet(true);
return;
}
key = new Key(encryptionFullKey.getPrivKeyBytes(), walletForm.getStorage().getKeyDeriver().getSalt(), EncryptionType.Deriver.ARGON2);
walletForm.getWallet().encrypt(key);
@@ -77,6 +77,10 @@ public class SettingsWalletForm extends WalletForm {
}
}
if(original.getGapLimit() != changed.getGapLimit()) {
return true;
}
return false;
}
}
@@ -122,4 +122,9 @@ public class TransactionsController extends WalletFormController implements Init
public void exchangeRatesUpdated(ExchangeRatesUpdatedEvent event) {
setFiatBalance(event.getCurrencyRate(), getWalletForm().getWalletTransactionsEntry().getBalance());
}
@Subscribe
public void walletHistoryStatus(WalletHistoryStatusEvent event) {
transactionsTable.updateHistoryStatus(event);
}
}
@@ -125,4 +125,9 @@ public class UtxosController extends WalletFormController implements Initializab
utxosChart.setBitcoinUnit(getWalletForm().getWallet(), event.getBitcoinUnit());
updateSendSelected(event.getBitcoinUnit());
}
@Subscribe
public void walletHistoryStatus(WalletHistoryStatusEvent event) {
utxosTable.updateHistoryStatus(event);
}
}
@@ -65,15 +65,23 @@ public class WalletForm {
}
public void refreshHistory(Integer blockHeight) {
refreshHistory(blockHeight, null);
}
public void refreshHistory(Integer blockHeight, WalletNode node) {
Wallet previousWallet = wallet.copy();
if(wallet.isValid() && AppController.isOnline()) {
ElectrumServer.TransactionHistoryService historyService = new ElectrumServer.TransactionHistoryService(wallet);
log.debug(node == null ? "Refreshing full wallet history" : "Requesting node wallet history for " + node.getDerivationPath());
ElectrumServer.TransactionHistoryService historyService = new ElectrumServer.TransactionHistoryService(wallet, node);
historyService.setOnSucceeded(workerStateEvent -> {
EventManager.get().post(new WalletHistoryStatusEvent(true));
updateWallet(previousWallet, blockHeight);
});
historyService.setOnFailed(workerStateEvent -> {
log.error("Error retrieving wallet history", workerStateEvent.getSource().getException());
EventManager.get().post(new WalletHistoryStatusEvent(workerStateEvent.getSource().getException().getMessage()));
});
EventManager.get().post(new WalletHistoryStatusEvent(false));
historyService.start();
}
}
@@ -213,8 +221,9 @@ public class WalletForm {
@Subscribe
public void walletNodeHistoryChanged(WalletNodeHistoryChangedEvent event) {
if(event.getWalletNode(wallet) != null) {
refreshHistory(AppController.getCurrentBlockHeight());
WalletNode walletNode = event.getWalletNode(wallet);
if(walletNode != null) {
refreshHistory(AppController.getCurrentBlockHeight(), walletNode);
}
}
@@ -89,21 +89,26 @@ public class WalletTransactionsEntry extends Entry {
for(WalletNode addressNode : purposeNode.getChildren()) {
for(BlockTransactionHashIndex hashIndex : addressNode.getTransactionOutputs()) {
BlockTransaction inputTx = wallet.getTransactions().get(hashIndex.getHash());
WalletTransaction inputWalletTx = walletTransactionMap.get(inputTx);
if(inputWalletTx == null) {
inputWalletTx = new WalletTransaction(wallet, inputTx);
walletTransactionMap.put(inputTx, inputWalletTx);
}
inputWalletTx.incoming.put(hashIndex, keyPurpose);
if(hashIndex.getSpentBy() != null) {
BlockTransaction outputTx = wallet.getTransactions().get(hashIndex.getSpentBy().getHash());
WalletTransaction outputWalletTx = walletTransactionMap.get(outputTx);
if(outputWalletTx == null) {
outputWalletTx = new WalletTransaction(wallet, outputTx);
walletTransactionMap.put(outputTx, outputWalletTx);
//A null inputTx here means the wallet is still updating - ignore as the WalletHistoryChangedEvent will run this again
if(inputTx != null) {
WalletTransaction inputWalletTx = walletTransactionMap.get(inputTx);
if(inputWalletTx == null) {
inputWalletTx = new WalletTransaction(wallet, inputTx);
walletTransactionMap.put(inputTx, inputWalletTx);
}
inputWalletTx.incoming.put(hashIndex, keyPurpose);
if(hashIndex.getSpentBy() != null) {
BlockTransaction outputTx = wallet.getTransactions().get(hashIndex.getSpentBy().getHash());
if(outputTx != null) {
WalletTransaction outputWalletTx = walletTransactionMap.get(outputTx);
if(outputWalletTx == null) {
outputWalletTx = new WalletTransaction(wallet, outputTx);
walletTransactionMap.put(outputTx, outputWalletTx);
}
outputWalletTx.outgoing.put(hashIndex.getSpentBy(), keyPurpose);
}
}
outputWalletTx.outgoing.put(hashIndex.getSpentBy(), keyPurpose);
}
}
}
+1
View File
@@ -20,6 +20,7 @@ open module com.sparrowwallet.sparrow {
requires com.fasterxml.jackson.databind;
requires cbor;
requires webcam.capture;
requires netlayer.jpms;
requires centerdevice.nsmenufx;
requires slf4j.api;
}
@@ -7,6 +7,7 @@
<?import javafx.scene.shape.Rectangle?>
<?import com.sparrowwallet.sparrow.control.UnlabeledToggleSwitch?>
<?import com.sparrowwallet.drongo.BitcoinUnit?>
<?import com.sparrowwallet.sparrow.Theme?>
<VBox maxHeight="-Infinity" maxWidth="-Infinity" minHeight="200" minWidth="350" prefHeight="770.0" prefWidth="1000.0" fx:controller="com.sparrowwallet.sparrow.AppController" xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1">
<children>
@@ -29,16 +30,19 @@
<SeparatorMenuItem />
<MenuItem mnemonicParsing="false" text="Import Wallet..." onAction="#importWallet"/>
<MenuItem fx:id="exportWallet" mnemonicParsing="false" text="Export Wallet..." onAction="#exportWallet"/>
<SeparatorMenuItem styleClass="macHide" />
<MenuItem styleClass="macHide" mnemonicParsing="false" text="Preferences..." onAction="#openPreferences"/>
<SeparatorMenuItem styleClass="osxHide" />
<MenuItem styleClass="osxHide" mnemonicParsing="false" text="Preferences..." onAction="#openPreferences"/>
<SeparatorMenuItem />
<MenuItem mnemonicParsing="false" text="Close Tab" onAction="#closeTab"/>
<MenuItem styleClass="macHide" mnemonicParsing="false" text="Quit" onAction="#quit"/>
<MenuItem styleClass="osxHide" mnemonicParsing="false" text="Quit" onAction="#quit"/>
</items>
</Menu>
<fx:define>
<ToggleGroup fx:id="bitcoinUnit"/>
</fx:define>
<fx:define>
<ToggleGroup fx:id="theme"/>
</fx:define>
<Menu mnemonicParsing="false" text="View">
<items>
<Menu mnemonicParsing="false" text="Bitcoin Unit">
@@ -60,11 +64,28 @@
</RadioMenuItem>
</items>
</Menu>
<Menu mnemonicParsing="false" text="Theme">
<items>
<RadioMenuItem mnemonicParsing="false" text="Light" toggleGroup="$theme" onAction="#setTheme">
<userData>
<Theme fx:constant="LIGHT" />
</userData>
</RadioMenuItem>
<RadioMenuItem mnemonicParsing="false" text="Dark" toggleGroup="$theme" onAction="#setTheme">
<userData>
<Theme fx:constant="DARK" />
</userData>
</RadioMenuItem>
</items>
</Menu>
<CheckMenuItem fx:id="showTxHex" mnemonicParsing="false" text="Show Transaction Hex" onAction="#showTxHex"/>
</items>
</Menu>
<Menu fx:id="toolsMenu" mnemonicParsing="false" text="Tools">
<MenuItem mnemonicParsing="false" text="Sign/Verify Message" onAction="#signVerifyMessage"/>
</Menu>
<Menu fx:id="helpMenu" mnemonicParsing="false" text="Help">
<MenuItem styleClass="macHide" mnemonicParsing="false" text="About Sparrow" onAction="#showAbout"/>
<MenuItem styleClass="osxHide" mnemonicParsing="false" text="About Sparrow" onAction="#showAbout"/>
</Menu>
</menus>
</MenuBar>
@@ -0,0 +1,147 @@
.root {
-fx-accent: #1e88cf;
-fx-focus-color: -fx-accent;
-fx-base: #373e43;
-fx-control-inner-background: derive(-fx-base, 35%);
-fx-control-inner-background-alt: -fx-control-inner-background ;
}
.label{
-fx-text-fill: lightgray;
}
.text-field {
-fx-prompt-text-fill: gray;
}
.titulo{
-fx-font-weight: bold;
-fx-font-size: 18px;
}
.button{
-fx-focus-traversable: false;
}
.button:hover{
-fx-text-fill: white;
}
.separator *.line {
-fx-background-color: #3C3C3C;
-fx-border-style: solid;
-fx-border-width: 1px;
}
.scroll-bar{
-fx-background-color: derive(-fx-base,45%)
}
.button:default {
-fx-base: -fx-accent ;
}
.table-view{
/*-fx-background-color: derive(-fx-base, 10%);*/
-fx-selection-bar-non-focused: derive(-fx-base, 50%);
}
.table-view .column-header .label{
-fx-alignment: CENTER_LEFT;
-fx-font-weight: none;
}
.list-cell:even,
.list-cell:odd,
.table-row-cell:even,
.table-row-cell:odd{
-fx-control-inner-background: derive(-fx-base, 15%);
}
.list-cell:empty,
.table-row-cell:empty {
-fx-background-color: transparent;
}
.list-cell,
.table-row-cell{
-fx-border-color: transparent;
-fx-table-cell-border-color:transparent;
}
.status-bar {
-fx-background-color: black, -fx-body-color;
}
.chart .default-color0.chart-series-line {
-fx-stroke: rgba(135, 138, 149, 0.6);
}
.chart .chart-bar {
-fx-bar-fill: rgba(135, 138, 149, 0.5);
}
#inputsPie .default-color0.chart-pie {
-fx-pie-color: #e06c75;
}
#outputsPie .default-color3.chart-pie {
-fx-pie-color: #e06c75
}
.root .etched-raised-border {
-fx-border-color: #ffffff, #000000;
-fx-border-style: solid, solid;
-fx-border-width: 1px, 1px;
}
.root .line-border {
-fx-border-color: #000000;
-fx-border-style: solid;
-fx-border-width: 1px;
}
.root .duplicate-warning {
-fx-text-fill: #e06c75;
}
.root .unused-check {
-fx-text-fill: #98c379;
}
.root .script-nest { -fx-fill: #565c64 }
.root .script-opcode { -fx-fill: #56b6c2 }
.root .script-hash { -fx-fill: #d19a66 }
.root .script-signature { -fx-fill: #98c379 }
.root .script-pubkey { -fx-fill: #c678dd }
.root .script-redeem { -fx-fill: derive(#e06c75, 20%) }
.root .script-other { -fx-fill: #c8ccd4 }
.root #txhex {
-fx-background-color: derive(-fx-control-inner-background, -50%);
color-0: #e06c75;
color-1: #e5c07b;
color-2: #d19a66;
color-3: #98c379;
color-4: #61afef;
color-5: #56b6c2;
color-6: #c678dd;
color-7: #be5046;
color-8: #c8ccd4;
color-grey: #3e4451;
}
.root .success {
-fx-text-fill: #98c379;
}
.root .failure {
-fx-text-fill: #e06c75;
}
.root .titled-description-pane > .title {
-fx-background-color: derive(-fx-base, 10%);
-fx-padding: 0;
-fx-border-color: derive(-fx-base, -2%);
/*-fx-border-width: 1;*/
}
@@ -0,0 +1,3 @@
.descriptor-text { -fx-fill: -fx-text-inner-color }
.descriptor-error { -fx-fill: #ca1243 }
@@ -126,4 +126,28 @@
.default-button {
-fx-base: -fx-default-button;
}
}
.etched-raised-border {
-fx-border-color: #ffffff, #a9a9a9;
-fx-border-style: solid, solid;
-fx-border-width: 1px, 1px;
}
.line-border {
-fx-border-color: #a9a9a9;
-fx-border-style: solid;
-fx-border-width: 1px;
}
.success {
-fx-text-fill: rgb(80, 161, 79);
}
.failure {
-fx-text-fill: rgb(202, 18, 67);
}
.root .header-panel {
-fx-background-color: -fx-box-border, derive(-fx-background, 10%);
}
@@ -9,7 +9,7 @@
<?import com.sparrowwallet.drongo.wallet.KeystoreSource?>
<?import javafx.geometry.Insets?>
<BorderPane stylesheets="@../general.css, @keystoreimport.css" xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml" fx:controller="com.sparrowwallet.sparrow.keystoreimport.KeystoreImportController">
<BorderPane stylesheets="@../general.css, @keystoreimport.css" styleClass="line-border" xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml" fx:controller="com.sparrowwallet.sparrow.keystoreimport.KeystoreImportController">
<padding>
<Insets top="0" left="0" right="0" bottom="0" />
</padding>
@@ -9,14 +9,14 @@
.notification-bar > .pane .label.title {
-fx-font-size: 15px;
-fx-text-fill: #292929;
-fx-text-fill: -fx-text-inner-color;
-fx-padding: 5 0 0 68;
-fx-translate-y: 10;
}
.notification-bar > .pane .label {
-fx-font-size: 1em;
-fx-text-fill: #292929;
-fx-text-fill: -fx-text-inner-color;
-fx-alignment: top-left;
-fx-padding: 0 0 0 10;
-fx-graphic-text-gap: 8;
@@ -73,6 +73,10 @@
<UnlabeledToggleSwitch fx:id="notifyNewTransactions" />
<HelpLabel helpText="Show system notifications on new wallet transactions"/>
</Field>
<Field text="Software updates:">
<UnlabeledToggleSwitch fx:id="checkNewVersions" />
<HelpLabel helpText="Check for updates to Sparrow"/>
</Field>
</Fieldset>
</Form>
</GridPane>
@@ -9,7 +9,7 @@
<?import javafx.geometry.Insets?>
<?import com.sparrowwallet.sparrow.preferences.PreferenceGroup?>
<?import org.controlsfx.glyphfont.Glyph?>
<BorderPane stylesheets="@../general.css, @preferences.css" xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml" fx:controller="com.sparrowwallet.sparrow.preferences.PreferencesController">
<BorderPane stylesheets="@../general.css, @preferences.css" styleClass="line-border" xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml" fx:controller="com.sparrowwallet.sparrow.preferences.PreferencesController">
<left>
<VBox styleClass="list-menu">
<ToggleButton VBox.vgrow="ALWAYS" text="General" wrapText="true" textAlignment="CENTER" contentDisplay="TOP" styleClass="list-item" maxHeight="Infinity" toggleGroup="$preferencesMenu">
@@ -31,6 +31,7 @@
-fx-font-size: 13px;
-fx-font-family: 'Roboto Mono';
-fx-padding: 4;
-fx-fill: -fx-text-inner-color;
}
.virtualized-scroll-pane {
@@ -1,24 +1,24 @@
.version { -fx-fill: #986801 }
.segwit-marker { -fx-fill: #000000 }
.segwit-flag { -fx-fill: #4078f2 }
.version { -fx-fill: color-7 }
.segwit-marker { -fx-fill: color-8 }
.segwit-flag { -fx-fill: color-5 }
.num-inputs { -fx-fill: #ca1243 }
.input-hash { -fx-fill: #0184bc }
.input-index { -fx-fill: #0184bc }
.input-sigscript-length { -fx-fill: #0184bc }
.input-sigscript { -fx-fill: #0184bc }
.input-sequence { -fx-fill: #0184bc }
.num-inputs { -fx-fill: color-0 }
.input-hash { -fx-fill: color-4 }
.input-index { -fx-fill: color-4 }
.input-sigscript-length { -fx-fill: color-4 }
.input-sigscript { -fx-fill: color-4 }
.input-sequence { -fx-fill: color-4 }
.num-outputs { -fx-fill: #ca1243 }
.output-value { -fx-fill: #50a14f }
.output-pubkeyscript-length { -fx-fill: #50a14f }
.output-pubkeyscript { -fx-fill: #50a14f }
.num-outputs { -fx-fill: color-0 }
.output-value { -fx-fill: color-3 }
.output-pubkeyscript-length { -fx-fill: color-3 }
.output-pubkeyscript { -fx-fill: color-3 }
.witness-count { -fx-fill: #ca1243 }
.witness-length { -fx-fill: #a626a4 }
.witness-data { -fx-fill: #a626a4 }
.witness-count { -fx-fill: color-0 }
.witness-length { -fx-fill: color-6 }
.witness-data { -fx-fill: color-6 }
.locktime { -fx-fill: #986801 }
.locktime { -fx-fill: color-7 }
#locktimeCurrentHeight {
-fx-padding: 0 0 0 12;
@@ -24,6 +24,7 @@
<?import com.sparrowwallet.sparrow.control.SignaturesProgressBar?>
<?import javafx.scene.control.ProgressBar?>
<?import javafx.scene.control.Tooltip?>
<?import com.sparrowwallet.sparrow.control.DynamicForm?>
<GridPane hgap="10.0" vgap="10.0" styleClass="tx-pane" xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.sparrowwallet.sparrow.transaction.HeadersController" stylesheets="@headers.css, @transaction.css, @../general.css">
<padding>
@@ -131,7 +132,7 @@
<Separator GridPane.columnIndex="0" GridPane.rowIndex="5" GridPane.columnSpan="2" styleClass="form-separator"/>
<Form fx:id="blockchainForm" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.rowIndex="6">
<DynamicForm fx:id="blockchainForm" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.rowIndex="6">
<Fieldset text="Blockchain" inputGrow="SOMETIMES">
<Field text="Status:">
<Label fx:id="blockStatus" contentDisplay="RIGHT" graphicTextGap="5" />
@@ -146,7 +147,7 @@
<IdLabel fx:id="blockHash" />
</Field>
</Fieldset>
</Form>
</DynamicForm>
<Form fx:id="signingWalletForm" GridPane.columnIndex="0" GridPane.rowIndex="6">
<Fieldset text="Signatures" inputGrow="SOMETIMES">
@@ -1,27 +1,27 @@
.version { -fx-fill: #e5e5e6 }
.segwit-marker { -fx-fill: #e5e5e6 }
.segwit-flag { -fx-fill: #e5e5e6 }
.version { -fx-fill: color-grey }
.segwit-marker { -fx-fill: color-grey }
.segwit-flag { -fx-fill: color-grey }
.num-inputs { -fx-fill: #e5e5e6 }
.input-other { -fx-fill: #e5e5e6 }
.num-inputs { -fx-fill: color-grey }
.input-other { -fx-fill: color-grey }
.input-hash { -fx-fill: #0184bc }
.input-index { -fx-fill: #000000 }
.input-sigscript-length { -fx-fill: #a626a4 }
.input-sigscript { -fx-fill: #50a14f }
.input-sequence { -fx-fill: #986801 }
.input-hash { -fx-fill: color-4 }
.input-index { -fx-fill: color-8 }
.input-sigscript-length { -fx-fill: color-6 }
.input-sigscript { -fx-fill: color-3 }
.input-sequence { -fx-fill: color-7 }
.num-outputs { -fx-fill: #e5e5e6 }
.output-value { -fx-fill: #e5e5e6 }
.output-pubkeyscript-length { -fx-fill: #e5e5e6 }
.output-pubkeyscript { -fx-fill: #e5e5e6 }
.num-outputs { -fx-fill: color-grey }
.output-value { -fx-fill: color-grey }
.output-pubkeyscript-length { -fx-fill: color-grey }
.output-pubkeyscript { -fx-fill: color-grey }
.witness-other { -fx-fill: #e5e5e6 }
.witness-other { -fx-fill: color-grey }
.witness-count { -fx-fill: #ca1243 }
.witness-length { -fx-fill: #986801 }
.witness-data { -fx-fill: #a626a4 }
.witness-count { -fx-fill: color-0 }
.witness-length { -fx-fill: color-7 }
.witness-data { -fx-fill: color-6 }
.locktime { -fx-fill: #e5e5e6 }
.locktime { -fx-fill: color-grey }
@@ -1,24 +1,24 @@
.version { -fx-fill: #e5e5e6 }
.segwit-marker { -fx-fill: #e5e5e6 }
.segwit-flag { -fx-fill: #e5e5e6 }
.version { -fx-fill: color-grey }
.segwit-marker { -fx-fill: color-grey }
.segwit-flag { -fx-fill: color-grey }
.num-inputs { -fx-fill: #ca1243 }
.input-hash { -fx-fill: #0184bc }
.input-index { -fx-fill: #000000 }
.input-sigscript-length { -fx-fill: #a626a4 }
.input-sigscript { -fx-fill: #50a14f }
.input-sequence { -fx-fill: #986801 }
.num-inputs { -fx-fill: color-0 }
.input-hash { -fx-fill: color-4 }
.input-index { -fx-fill: color-8 }
.input-sigscript-length { -fx-fill: color-6 }
.input-sigscript { -fx-fill: color-3 }
.input-sequence { -fx-fill: color-7 }
.num-outputs { -fx-fill: #e5e5e6 }
.output-value { -fx-fill: #e5e5e6 }
.output-pubkeyscript-length { -fx-fill: #e5e5e6 }
.output-pubkeyscript { -fx-fill: #e5e5e6 }
.num-outputs { -fx-fill: color-grey }
.output-value { -fx-fill: color-grey }
.output-pubkeyscript-length { -fx-fill: color-grey }
.output-pubkeyscript { -fx-fill: color-grey }
.witness-count { -fx-fill: #ca1243 }
.witness-length { -fx-fill: #986801 }
.witness-data { -fx-fill: #a626a4 }
.witness-count { -fx-fill: color-0 }
.witness-length { -fx-fill: color-7 }
.witness-data { -fx-fill: color-6 }
.locktime { -fx-fill: #e5e5e6 }
.locktime { -fx-fill: color-grey }
.chart-legend-item {
-fx-font-size: 13px;
@@ -1,26 +1,26 @@
.version { -fx-fill: #e5e5e6 }
.segwit-marker { -fx-fill: #e5e5e6 }
.segwit-flag { -fx-fill: #e5e5e6 }
.version { -fx-fill: color-grey }
.segwit-marker { -fx-fill: color-grey }
.segwit-flag { -fx-fill: color-grey }
.num-inputs { -fx-fill: #e5e5e6 }
.input-hash { -fx-fill: #e5e5e6 }
.input-index { -fx-fill: #e5e5e6 }
.input-sigscript-length { -fx-fill: #e5e5e6 }
.input-sigscript { -fx-fill: #e5e5e6 }
.input-sequence { -fx-fill: #e5e5e6 }
.num-inputs { -fx-fill: color-grey }
.input-hash { -fx-fill: color-grey }
.input-index { -fx-fill: color-grey }
.input-sigscript-length { -fx-fill: color-grey }
.input-sigscript { -fx-fill: color-grey }
.input-sequence { -fx-fill: color-grey }
.num-outputs { -fx-fill: #e5e5e6 }
.output-other { -fx-fill: #e5e5e6 }
.num-outputs { -fx-fill: color-grey }
.output-other { -fx-fill: color-grey }
.output-value { -fx-fill: #000000 }
.output-pubkeyscript-length { -fx-fill: #a626a4 }
.output-pubkeyscript { -fx-fill: #50a14f }
.output-value { -fx-fill: color-8 }
.output-pubkeyscript-length { -fx-fill: color-6 }
.output-pubkeyscript { -fx-fill: color-3 }
.witness-count { -fx-fill: #e5e5e6 }
.witness-length { -fx-fill: #e5e5e6 }
.witness-data { -fx-fill: #e5e5e6 }
.witness-count { -fx-fill: color-grey }
.witness-length { -fx-fill: color-grey }
.witness-data { -fx-fill: color-grey }
.locktime { -fx-fill: #e5e5e6 }
.locktime { -fx-fill: color-grey }
#spentField .input-container {
-fx-alignment: center-left;
@@ -1,24 +1,24 @@
.version { -fx-fill: #e5e5e6 }
.segwit-marker { -fx-fill: #e5e5e6 }
.segwit-flag { -fx-fill: #e5e5e6 }
.version { -fx-fill: color-grey }
.segwit-marker { -fx-fill: color-grey }
.segwit-flag { -fx-fill: color-grey }
.num-inputs { -fx-fill: #e5e5e6 }
.input-hash { -fx-fill: #e5e5e6 }
.input-index { -fx-fill: #e5e5e6 }
.input-sigscript-length { -fx-fill: #e5e5e6 }
.input-sigscript { -fx-fill: #e5e5e6 }
.input-sequence { -fx-fill: #e5e5e6 }
.num-inputs { -fx-fill: color-grey }
.input-hash { -fx-fill: color-grey }
.input-index { -fx-fill: color-grey }
.input-sigscript-length { -fx-fill: color-grey }
.input-sigscript { -fx-fill: color-grey }
.input-sequence { -fx-fill: color-grey }
.num-outputs { -fx-fill: #ca1243 }
.output-value { -fx-fill: #000000 }
.output-pubkeyscript-length { -fx-fill: #a626a4 }
.output-pubkeyscript { -fx-fill: #50a14f }
.num-outputs { -fx-fill: color-0 }
.output-value { -fx-fill: color-8 }
.output-pubkeyscript-length { -fx-fill: color-6 }
.output-pubkeyscript { -fx-fill: color-3 }
.witness-count { -fx-fill: #e5e5e6 }
.witness-length { -fx-fill: #e5e5e6 }
.witness-data { -fx-fill: #e5e5e6 }
.witness-count { -fx-fill: color-grey }
.witness-length { -fx-fill: color-grey }
.witness-data { -fx-fill: color-grey }
.locktime { -fx-fill: #e5e5e6 }
.locktime { -fx-fill: color-grey }
.chart-legend-item {
-fx-font-size: 13;
@@ -1,8 +1,18 @@
#txhex {
-fx-background-color: #ffffff;
-fx-background-color: -fx-control-inner-background;
-fx-font-size: 13px;
-fx-font-family: 'Roboto Mono';
-fx-padding: 2;
color-0: #ca1243;
color-1: #d75f00;
color-2: #c18401;
color-3: #50a14f;
color-4: #0184bc;
color-5: #4078f2;
color-6: #a626a4;
color-7: #986801;
color-8: #000000;
color-grey: #e5e5e6;
}
.tx-pane {
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import tornadofx.control.Form?>
<?import tornadofx.control.Fieldset?>
<?import tornadofx.control.Field?>
<?import com.sparrowwallet.sparrow.control.HelpLabel?>
<?import javafx.geometry.Insets?>
<BorderPane stylesheets="@../general.css" styleClass="line-border" xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml" fx:controller="com.sparrowwallet.sparrow.wallet.AdvancedController">
<center>
<GridPane hgap="10.0" vgap="10.0">
<padding>
<Insets left="25.0" right="25.0" top="25.0" />
</padding>
<columnConstraints>
<ColumnConstraints percentWidth="100" />
</columnConstraints>
<rowConstraints>
<RowConstraints />
</rowConstraints>
<Form GridPane.columnIndex="0" GridPane.rowIndex="0">
<Fieldset inputGrow="SOMETIMES" text="Advanced Settings" styleClass="wideLabelFieldSet">
<Field text="Gap limit:">
<Spinner fx:id="gapLimit" editable="true" prefWidth="90" />
<HelpLabel helpText="Change how far ahead to look for additional transactions beyond the highest derivation with previous transaction outputs"/>
</Field>
</Fieldset>
</Form>
</GridPane>
</center>
</BorderPane>
@@ -13,6 +13,7 @@
<?import org.controlsfx.control.SegmentedButton?>
<?import com.sparrowwallet.drongo.wallet.KeystoreSource?>
<?import org.controlsfx.glyphfont.Glyph?>
<?import com.sparrowwallet.sparrow.control.HelpLabel?>
<StackPane stylesheets="@keystore.css, @settings.css, @../general.css" xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.sparrowwallet.sparrow.wallet.KeystoreController">
<padding>
<Insets left="25.0" right="25.0" />
@@ -40,7 +41,7 @@
<TextField fx:id="label" maxWidth="160"/>
</Field>
<Field text="Master fingerprint:">
<TextField fx:id="fingerprint" maxWidth="80" promptText="ffffffff"/>
<TextField fx:id="fingerprint" maxWidth="80" promptText="ffffffff"/> <HelpLabel helpText="A master fingerprint is the first 4 bytes of the master public key hash. It is safe to use any valid value (ffffffff) for Watch Only Wallets." />
</Field>
<Field text="Derivation:">
<TextField fx:id="derivation" maxWidth="200"/>
@@ -80,7 +81,7 @@
<KeystoreSource fx:constant="SW_SEED"/>
</userData>
</ToggleButton>
<ToggleButton text="XPUB / Watch Only Wallet" contentDisplay="TOP" wrapText="true" textAlignment="CENTER" toggleGroup="$keystoreSourceToggleGroup" onAction="#selectSource">
<ToggleButton text="xPub / Watch Only Wallet" contentDisplay="TOP" wrapText="true" textAlignment="CENTER" toggleGroup="$keystoreSourceToggleGroup" onAction="#selectSource">
<graphic>
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="20" icon="EYE" />
</graphic>
@@ -18,7 +18,7 @@
<?import com.sparrowwallet.sparrow.control.CopyableTextField?>
<?import com.sparrowwallet.sparrow.control.ScriptArea?>
<BorderPane stylesheets="@receive.css, @wallet.css, @../script.css, @../general.css" styleClass="wallet-pane" xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.sparrowwallet.sparrow.wallet.ReceiveController">
<BorderPane stylesheets="@receive.css, @wallet.css, @../script.css, @../descriptor.css, @../general.css" styleClass="wallet-pane" xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.sparrowwallet.sparrow.wallet.ReceiveController">
<center>
<GridPane styleClass="receive-form" hgap="10.0" vgap="10.0">
@@ -10,8 +10,9 @@
-fx-alignment: center-left;
}
#spendingMiniscript {
-fx-font-size: 13px;
-fx-font-family: 'Roboto Mono';
.keystore-padding-border {
-fx-border-color: #00000000;
-fx-border-style: none;
-fx-border-width: 10 5 0 0;
}
@@ -9,8 +9,10 @@
<?import com.sparrowwallet.sparrow.control.CopyableLabel?>
<?import com.sparrowwallet.drongo.policy.PolicyType?>
<?import com.sparrowwallet.drongo.protocol.ScriptType?>
<?import com.sparrowwallet.sparrow.control.DescriptorArea?>
<?import org.fxmisc.flowless.VirtualizedScrollPane?>
<BorderPane stylesheets="@settings.css, @wallet.css, @../general.css" styleClass="wallet-pane" xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.sparrowwallet.sparrow.wallet.SettingsController">
<BorderPane stylesheets="@settings.css, @wallet.css, @../script.css, @../descriptor.css, @../general.css" styleClass="wallet-pane" xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.sparrowwallet.sparrow.wallet.SettingsController">
<center>
<GridPane hgap="10.0" vgap="10.0">
<padding>
@@ -59,7 +61,7 @@
<Form GridPane.columnIndex="1" GridPane.rowIndex="0">
<Fieldset inputGrow="SOMETIMES" text="" fx:id="multisigFieldset">
<Field text="Cosigners:">
<RangeSlider fx:id="multisigControl" showTickMarks="true" showTickLabels="true" blockIncrement="1" min="2" max="9" lowValue="2" highValue="3" snapToTicks="true" majorTickUnit="1" minorTickCount="0" />
<RangeSlider fx:id="multisigControl" showTickMarks="true" showTickLabels="true" blockIncrement="1" min="1" max="9" highValue="3" lowValue="2" snapToTicks="true" majorTickUnit="1" minorTickCount="0" />
</Field>
<Field text="M of N:">
<CopyableLabel fx:id="multisigLowLabel" />
@@ -70,16 +72,24 @@
</Form>
<Form GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.rowIndex="1">
<Fieldset inputGrow="SOMETIMES" text="Spending Policy">
<Field text="Miniscript:">
<TextField fx:id="spendingMiniscript" editable="false" />
<Fieldset inputGrow="SOMETIMES" text="Script Policy">
<Field text="Descriptor:">
<VirtualizedScrollPane hbarPolicy="NEVER" vbarPolicy="NEVER">
<content>
<DescriptorArea fx:id="descriptor" editable="false" styleClass="uneditable-codearea" prefHeight="27" maxHeight="27" />
</content>
</VirtualizedScrollPane>
</Field>
</Fieldset>
</Form>
<Form GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.rowIndex="2">
<Fieldset inputGrow="SOMETIMES" text="Keystores">
<StackPane fx:id="keystoreTabsPane" />
<StackPane styleClass="keystore-padding-border">
<StackPane styleClass="etched-raised-border">
<StackPane fx:id="keystoreTabsPane"/>
</StackPane>
</StackPane>
</Fieldset>
</Form>
</GridPane>
@@ -89,8 +99,11 @@
<padding>
<Insets left="25.0" right="25.0" bottom="25.0" />
</padding>
<Button fx:id="apply" text="Apply" defaultButton="true" AnchorPane.rightAnchor="10" />
<Button fx:id="revert" text="Revert" cancelButton="true" AnchorPane.rightAnchor="80" />
<HBox AnchorPane.rightAnchor="10" spacing="20">
<Button text="Advanced..." onAction="#showAdvanced" />
<Button fx:id="revert" text="Revert" cancelButton="true" />
<Button fx:id="apply" text="Apply" defaultButton="true" />
</HBox>
</AnchorPane>
</bottom>
</BorderPane>
@@ -35,33 +35,29 @@
}
.hashindex-row {
-fx-text-fill: #696c77;
-fx-opacity: 0.7;
}
.hashindex-row.spent {
-fx-text-fill: #a0a1a7;
-fx-opacity: 0.4;
}
.transaction-row.confirming {
-fx-text-fill: #696c77;
-fx-opacity: 0.7;
}
.utxo-row.unspendable {
-fx-text-fill: #a0a1a7;
-fx-opacity: 0.4;
}
.tree-table-row-cell:selected .utxo-row.unspendable {
-fx-text-fill: #696c77;
-fx-opacity: 0.7;
}
.tree-table-view:focused:row-selection .tree-table-row-cell:selected .utxo-row.unspendable {
-fx-text-fill: derive(white, -15%);
}
.tree-table-row-cell:selected .hashindex-row, .tree-table-row-cell:selected .transaction-row {
-fx-text-fill: white;
}
.label-cell .text-field {
-fx-padding: 0;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

@@ -59,7 +59,7 @@ public class ColdcardMultisigTest extends IoTest {
Assert.assertEquals(PolicyType.MULTI, wallet.getPolicyType());
Assert.assertEquals(ScriptType.P2WSH, wallet.getScriptType());
Assert.assertEquals(2, wallet.getDefaultPolicy().getNumSignaturesRequired());
Assert.assertEquals("multi(2,coldcard1,coldcard2,coldcard3,coldcard4)", wallet.getDefaultPolicy().getMiniscript().getScript());
Assert.assertEquals("wsh(sortedmulti(2,coldcard1,coldcard2,coldcard3,coldcard4))", wallet.getDefaultPolicy().getMiniscript().getScript());
Assert.assertTrue(wallet.isValid());
}
@@ -71,7 +71,7 @@ public class ColdcardMultisigTest extends IoTest {
Assert.assertEquals(PolicyType.MULTI, wallet.getPolicyType());
Assert.assertEquals(ScriptType.P2SH_P2WSH, wallet.getScriptType());
Assert.assertEquals(2, wallet.getDefaultPolicy().getNumSignaturesRequired());
Assert.assertEquals("multi(2,coldcard1,coldcard2,coldcard3,coldcard4)", wallet.getDefaultPolicy().getMiniscript().getScript());
Assert.assertEquals("sh(wsh(sortedmulti(2,coldcard1,coldcard2,coldcard3,coldcard4)))", wallet.getDefaultPolicy().getMiniscript().getScript());
Assert.assertTrue(wallet.isValid());
}
@@ -83,7 +83,7 @@ public class ColdcardMultisigTest extends IoTest {
Assert.assertEquals(PolicyType.MULTI, wallet.getPolicyType());
Assert.assertEquals(ScriptType.P2WSH, wallet.getScriptType());
Assert.assertEquals(3, wallet.getDefaultPolicy().getNumSignaturesRequired());
Assert.assertEquals("multi(3,coldcard1,coldcard2,coldcard3)", wallet.getDefaultPolicy().getMiniscript().getScript());
Assert.assertEquals("wsh(sortedmulti(3,coldcard1,coldcard2,coldcard3))", wallet.getDefaultPolicy().getMiniscript().getScript());
Assert.assertEquals("06b57041", wallet.getKeystores().get(0).getKeyDerivation().getMasterFingerprint());
Assert.assertEquals("m/48'/0'/0'/2'", wallet.getKeystores().get(0).getKeyDerivation().getDerivationPath());
Assert.assertEquals("xpub6EfEGa5isJbQFSswM5Uptw5BSq2Td1ZDJr3QUNUcMySpC7itZ3ccypVHtLPnvMzKQ2qxrAgH49vhVxRcaQLFbixAVRR8RACrYTp88Uv9h8Z", wallet.getKeystores().get(0).getExtendedPublicKey().toString());
@@ -22,7 +22,7 @@ public class ElectrumTest extends IoTest {
Assert.assertEquals(PolicyType.SINGLE, wallet.getPolicyType());
Assert.assertEquals(ScriptType.P2SH_P2WPKH, wallet.getScriptType());
Assert.assertEquals(1, wallet.getDefaultPolicy().getNumSignaturesRequired());
Assert.assertEquals("pkh(trezortest)", wallet.getDefaultPolicy().getMiniscript().getScript());
Assert.assertEquals("sh(wpkh(trezortest))", wallet.getDefaultPolicy().getMiniscript().getScript());
Assert.assertEquals("ab543c67", wallet.getKeystores().get(0).getKeyDerivation().getMasterFingerprint());
Assert.assertEquals("m/49'/0'/0'", wallet.getKeystores().get(0).getKeyDerivation().getDerivationPath());
Assert.assertEquals("xpub6FFEQVG6QR28chQzgSJ7Gjx5j5BGLkCMgZ9bc41YJCXfwYiCKUQdcwm4Fe1stvzRjosz5udMedYZFRL56AeZXCsiVmnVUysio4jkAKTukmN", wallet.getKeystores().get(0).getExtendedPublicKey().toString());
@@ -42,7 +42,7 @@ public class ElectrumTest extends IoTest {
Assert.assertEquals(PolicyType.SINGLE, wallet.getPolicyType());
Assert.assertEquals(ScriptType.P2SH_P2WPKH, wallet.getScriptType());
Assert.assertEquals(1, wallet.getDefaultPolicy().getNumSignaturesRequired());
Assert.assertEquals("pkh(trezortest)", wallet.getDefaultPolicy().getMiniscript().getScript());
Assert.assertEquals("sh(wpkh(trezortest))", wallet.getDefaultPolicy().getMiniscript().getScript());
Assert.assertEquals("ab543c67", wallet.getKeystores().get(0).getKeyDerivation().getMasterFingerprint());
Assert.assertEquals("m/49'/0'/0'", wallet.getKeystores().get(0).getKeyDerivation().getDerivationPath());
Assert.assertEquals("xpub6FFEQVG6QR28chQzgSJ7Gjx5j5BGLkCMgZ9bc41YJCXfwYiCKUQdcwm4Fe1stvzRjosz5udMedYZFRL56AeZXCsiVmnVUysio4jkAKTukmN", wallet.getKeystores().get(0).getExtendedPublicKey().toString());
@@ -56,7 +56,7 @@ public class ElectrumTest extends IoTest {
Assert.assertEquals(PolicyType.MULTI, wallet.getPolicyType());
Assert.assertEquals(ScriptType.P2SH_P2WSH, wallet.getScriptType());
Assert.assertEquals(2, wallet.getDefaultPolicy().getNumSignaturesRequired());
Assert.assertEquals("multi(2,coldcard6ba6cfd0,coldcard747b698e,coldcard7bb026be,coldcard0f056943)", wallet.getDefaultPolicy().getMiniscript().getScript());
Assert.assertEquals("sh(wsh(sortedmulti(2,coldcard6ba6cfd0,coldcard747b698e,coldcard7bb026be,coldcard0f056943)))", wallet.getDefaultPolicy().getMiniscript().getScript());
Assert.assertEquals("6ba6cfd0", wallet.getKeystores().get(0).getKeyDerivation().getMasterFingerprint());
Assert.assertEquals("m/48'/1'/0'/1'", wallet.getKeystores().get(0).getKeyDerivation().getDerivationPath());
Assert.assertEquals("xpub6FFEQVG6QR28chQzgSJ7Gjx5j5BGLkCMgZ9bc41YJCXfwYiCKUQdcwm4Fe1stvzRjosz5udMedYZFRL56AeZXCsiVmnVUysio4jkAKTukmN", wallet.getKeystores().get(0).getExtendedPublicKey().toString());
@@ -78,7 +78,7 @@ public class ElectrumTest extends IoTest {
Assert.assertEquals(PolicyType.MULTI, wallet.getPolicyType());
Assert.assertEquals(ScriptType.P2SH_P2WSH, wallet.getScriptType());
Assert.assertEquals(2, wallet.getDefaultPolicy().getNumSignaturesRequired());
Assert.assertEquals("multi(2,coldcard6ba6cfd0,coldcard747b698e,coldcard7bb026be,coldcard0f056943)", wallet.getDefaultPolicy().getMiniscript().getScript());
Assert.assertEquals("sh(wsh(sortedmulti(2,coldcard6ba6cfd0,coldcard747b698e,coldcard7bb026be,coldcard0f056943)))", wallet.getDefaultPolicy().getMiniscript().getScript());
Assert.assertEquals("6ba6cfd0", wallet.getKeystores().get(0).getKeyDerivation().getMasterFingerprint());
Assert.assertEquals("m/48'/1'/0'/1'", wallet.getKeystores().get(0).getKeyDerivation().getDerivationPath());
Assert.assertEquals("xpub6FFEQVG6QR28chQzgSJ7Gjx5j5BGLkCMgZ9bc41YJCXfwYiCKUQdcwm4Fe1stvzRjosz5udMedYZFRL56AeZXCsiVmnVUysio4jkAKTukmN", wallet.getKeystores().get(0).getExtendedPublicKey().toString());
@@ -96,7 +96,7 @@ public class ElectrumTest extends IoTest {
Assert.assertEquals(PolicyType.SINGLE, wallet.getPolicyType());
Assert.assertEquals(ScriptType.P2WPKH, wallet.getScriptType());
Assert.assertEquals(1, wallet.getDefaultPolicy().getNumSignaturesRequired());
Assert.assertEquals("pkh(electrum)", wallet.getDefaultPolicy().getMiniscript().getScript());
Assert.assertEquals("wpkh(electrum)", wallet.getDefaultPolicy().getMiniscript().getScript());
Assert.assertEquals("f881eac5", wallet.getKeystores().get(0).getKeyDerivation().getMasterFingerprint());
Assert.assertEquals("m/0'", wallet.getKeystores().get(0).getKeyDerivation().getDerivationPath());
Assert.assertEquals("xpub69iSRreMB6fu24sU8Tdxv7yYGqzPkDwPkwqUfKJTxW3p8afW7XvTewVCapuX3dQjdD197iF65WcjYaNpFbwWT3RyuZ1KJ3ToJNVWKWyAJ6f", wallet.getKeystores().get(0).getExtendedPublicKey().toString());
@@ -116,7 +116,7 @@ public class ElectrumTest extends IoTest {
Assert.assertEquals(PolicyType.SINGLE, wallet.getPolicyType());
Assert.assertEquals(ScriptType.P2WPKH, wallet.getScriptType());
Assert.assertEquals(1, wallet.getDefaultPolicy().getNumSignaturesRequired());
Assert.assertEquals("pkh(electrum)", wallet.getDefaultPolicy().getMiniscript().getScript());
Assert.assertEquals("wpkh(electrum)", wallet.getDefaultPolicy().getMiniscript().getScript());
Assert.assertEquals("59c5474f", wallet.getKeystores().get(0).getKeyDerivation().getMasterFingerprint());
Assert.assertEquals("m/0'", wallet.getKeystores().get(0).getKeyDerivation().getDerivationPath());
Assert.assertEquals("xpub68YmVxWbxqjpxbUqqaPrgkBQPBSJuq6gEaL22uuytSEojtS2x5eLPN2uspUuyigtnMkoHrFSF1KwoXPwjzuaUjErUwztxfHquAwuaQhSd9J", wallet.getKeystores().get(0).getExtendedPublicKey().toString());