Compare commits

..
24 Commits
Author SHA1 Message Date
Craig Raw fa082e892f about dialog, older mac hwi build 2020-08-21 20:58:11 +02:00
Craig Raw c814b8e350 various fixes, better non-electrumx support 2020-08-21 17:16:14 +02:00
Craig Raw 2b48b4cd6e better handling of server connection editing 2020-08-21 10:48:41 +02:00
Craig Raw d49f85fe5b more import fixes 2020-08-21 10:08:43 +02:00
Craig Raw af525797ff improvements to importing and exporting 2020-08-21 09:59:11 +02:00
Craig Raw e9e10de266 export file naming with correct extensions 2020-08-20 18:20:42 +02:00
Craig Raw ab5fdef919 display full derivation path where possible 2020-08-20 17:59:58 +02:00
Craig Raw f723d20c9a disable absolute locktime if sequence disabled 2020-08-20 17:40:56 +02:00
Craig Raw 03c488cf4d add warnings to block time, set current height, close once 2020-08-20 17:30:39 +02:00
Craig Raw ca64a1d307 scan qr to bitcoin uri 2020-08-20 15:26:59 +02:00
Craig Raw 4c8f7bb711 clear fiat amounts 2020-08-20 12:30:21 +02:00
Craig Raw 77dd8ca5d7 unregister closed tab and dialog subscribers, fix connection error reset behaviour 2020-08-20 11:25:34 +02:00
Craig Raw d5aba35184 make controller and tab data share data sources 2020-08-19 15:44:40 +02:00
Craig Raw 30e2ab5e2a fill tx labels on send 2020-08-19 08:40:06 +02:00
Craig Raw e93fcf7fbd show fiat fee amount 2020-08-18 12:54:43 +02:00
Craig Raw 198672a06e further keystore improvements 2020-08-18 11:50:27 +02:00
Craig Raw 133d771c09 keystore buttons label tooltips 2020-08-18 11:02:45 +02:00
Craig Raw 9a80065605 connection toggle tooltip 2020-08-18 10:55:07 +02:00
Craig Raw ccf4e98968 ellipsis and incomplete wallet background save fixes 2020-08-18 10:47:32 +02:00
Craig Raw df15129f64 bip39 word list improvements #2 2020-08-18 10:20:56 +02:00
Craig Raw e878648587 bip39 word list usability improvements 2020-08-17 19:42:30 +02:00
Craig Raw 75bf63d936 fix wallet settings update bugs 2020-08-17 17:25:33 +02:00
Craig Raw 304bdd5c48 remove sys err exception logging 2020-08-17 17:00:48 +02:00
Craig Raw e1934c99bc upgrade controlsfx, fix notifications bug 2020-08-17 16:48:31 +02:00
73 changed files with 956 additions and 233 deletions
+2 -2
View File
@@ -45,7 +45,7 @@ dependencies {
exclude group: 'com.nativelibs4java', module: 'bridj'
}
implementation('de.codecentric.centerdevice:centerdevice-nsmenufx:2.1.7')
implementation('org.controlsfx:controlsfx:11.0.1' ) {
implementation('org.controlsfx:controlsfx:11.0.2' ) {
exclude group: 'org.openjfx', module: 'javafx-base'
exclude group: 'org.openjfx', module: 'javafx-graphics'
exclude group: 'org.openjfx', module: 'javafx-controls'
@@ -118,7 +118,7 @@ jlink {
jpackage {
imageName = "Sparrow"
installerName = "Sparrow"
appVersion = "0.6"
appVersion = "0.9.1"
skipInstaller = true
imageOptions = []
installerOptions = [
+1 -1
Submodule drongo updated: 6a2af38b8a...446eac3483
BIN
View File
Binary file not shown.
@@ -0,0 +1,16 @@
package com.sparrowwallet.sparrow;
import javafx.event.ActionEvent;
import javafx.stage.Stage;
public class AboutController {
private Stage stage;
public void setStage(Stage stage) {
this.stage = stage;
}
public void close(ActionEvent event) {
stage.close();
}
}
@@ -21,6 +21,7 @@ import com.sparrowwallet.sparrow.io.*;
import com.sparrowwallet.sparrow.net.ElectrumServer;
import com.sparrowwallet.sparrow.preferences.PreferencesDialog;
import com.sparrowwallet.sparrow.transaction.TransactionController;
import com.sparrowwallet.sparrow.transaction.TransactionData;
import com.sparrowwallet.sparrow.transaction.TransactionView;
import com.sparrowwallet.sparrow.wallet.WalletController;
import com.sparrowwallet.sparrow.wallet.WalletForm;
@@ -37,6 +38,8 @@ import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
@@ -45,6 +48,7 @@ import javafx.scene.input.TransferMode;
import javafx.scene.layout.StackPane;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import javafx.stage.Window;
import javafx.util.Duration;
import org.controlsfx.control.Notifications;
@@ -187,6 +191,18 @@ public class AppController implements Initializable {
EventManager.get().post(new OpenWalletsEvent(getOpenWallets()));
}
List<WalletTabData> closedWalletTabs = c.getRemoved().stream().map(tab -> (TabData)tab.getUserData())
.filter(tabData -> tabData.getType() == TabData.TabType.WALLET).map(tabData -> (WalletTabData)tabData).collect(Collectors.toList());
if(!closedWalletTabs.isEmpty()) {
EventManager.get().post(new WalletTabsClosedEvent(closedWalletTabs));
}
List<TransactionTabData> closedTransactionTabs = c.getRemoved().stream().map(tab -> (TabData)tab.getUserData())
.filter(tabData -> tabData.getType() == TabData.TabType.TRANSACTION).map(tabData -> (TransactionTabData)tabData).collect(Collectors.toList());
if(!closedTransactionTabs.isEmpty()) {
EventManager.get().post(new TransactionTabsClosedEvent(closedTransactionTabs));
}
if(tabs.getTabs().isEmpty()) {
Stage tabStage = (Stage)tabs.getScene().getWindow();
tabStage.setTitle("Sparrow");
@@ -234,6 +250,7 @@ public class AppController implements Initializable {
});
onlineProperty.bindBidirectional(serverToggle.selectedProperty());
onlineProperty().addListener((observable, oldValue, newValue) -> serverToggle.setTooltip(new Tooltip(newValue ? "Connected to " + Config.get().getElectrumServer() : "Disconnected")));
Config config = Config.get();
connectionService = createConnectionService();
@@ -266,10 +283,14 @@ public class AppController implements Initializable {
}
});
connectionService.setOnFailed(failEvent -> {
//Close connection here to create a new transport next time we try
connectionService.resetConnection();
changeMode = false;
onlineProperty.setValue(false);
changeMode = true;
log.debug("Connection failed", failEvent.getSource().getException());
EventManager.get().post(new ConnectionFailedEvent(failEvent.getSource().getException()));
});
@@ -316,7 +337,7 @@ public class AppController implements Initializable {
MenuToolkit tk = MenuToolkit.toolkit();
MenuItem preferences = new MenuItem("Preferences...");
preferences.setOnAction(this::openPreferences);
Menu defaultApplicationMenu = new Menu("Apple", null, tk.createAboutMenuItem(MainApp.APP_NAME, null), new SeparatorMenuItem(),
Menu defaultApplicationMenu = new Menu("Apple", null, tk.createAboutMenuItem(MainApp.APP_NAME, getAboutStage()), new SeparatorMenuItem(),
preferences, new SeparatorMenuItem(),
tk.createHideMenuItem(MainApp.APP_NAME), tk.createHideOthersMenuItem(), tk.createUnhideAllMenuItem(), new SeparatorMenuItem(),
tk.createQuitMenuItem(MainApp.APP_NAME));
@@ -326,6 +347,27 @@ public class AppController implements Initializable {
}
}
private Stage getAboutStage() {
try {
FXMLLoader loader = new FXMLLoader(AppController.class.getResource("about.fxml"));
Parent root = loader.load();
AboutController controller = loader.getController();
Stage stage = new Stage();
stage.setTitle("About Sparrow");
stage.initStyle(StageStyle.UNDECORATED);
stage.setResizable(false);
stage.setScene(new Scene(root));
controller.setStage(stage);
return stage;
} catch(IOException e) {
log.error("Error loading about stage", e);
}
return null;
}
public void openTransactionFromFile(ActionEvent event) {
Stage window = new Stage();
@@ -467,13 +509,9 @@ public class AppController implements Initializable {
TransactionTabData transactionTabData = (TransactionTabData)tabData;
Transaction transaction = transactionTabData.getTransaction();
//Note the transactionTabData's transaction does not change even once the final tx is extracted, so extract it here if possible
if(transactionTabData.getPsbt() != null && transactionTabData.getPsbt().isFinalized()) {
transaction = transactionTabData.getPsbt().extractTransaction();
}
//Save a transaction if the PSBT is null or finalized (a finalized PSBT is less useful than a broadcastable tx)
boolean saveTx = (transactionTabData.getPsbt() == null || transactionTabData.getPsbt().isFinalized());
//Save a transaction if the PSBT is null or transaction has already been extracted, otherwise save PSBT
//The PSBT's transaction is not altered with transaction extraction, but the extracted transaction is stored in TransactionData
boolean saveTx = (transactionTabData.getPsbt() == null || transactionTabData.getPsbt().getTransaction() != transaction);
Stage window = new Stage();
FileChooser fileChooser = new FileChooser();
@@ -481,8 +519,10 @@ public class AppController implements Initializable {
String fileName = selectedTab.getText();
if(fileName != null && !fileName.isEmpty()) {
if(transactionTabData.getPsbt() != null && !fileName.endsWith(".psbt")) {
fileName += ".psbt";
if(transactionTabData.getPsbt() != null) {
if(!fileName.endsWith(".psbt")) {
fileName += ".psbt";
}
} else if(!fileName.endsWith(".txn")) {
fileName += ".txn";
}
@@ -516,6 +556,14 @@ public class AppController implements Initializable {
return onlineProperty.get();
}
public void connect() {
serverToggle.selectedProperty().set(true);
}
public void disconnect() {
serverToggle.selectedProperty().set(false);
}
public static BooleanProperty onlineProperty() { return onlineProperty; }
public static Integer getCurrentBlockHeight() {
@@ -753,8 +801,6 @@ public class AppController implements Initializable {
name = name.substring(0, name.lastIndexOf('.'));
}
Tab tab = new Tab(name);
TabData tabData = new WalletTabData(TabData.TabType.WALLET, wallet, storage);
tab.setUserData(tabData);
tab.setContextMenu(getTabContextMenu(tab));
tab.setClosable(true);
FXMLLoader walletLoader = new FXMLLoader(getClass().getResource("wallet/wallet.fxml"));
@@ -766,6 +812,9 @@ public class AppController implements Initializable {
EventManager.get().register(walletForm);
controller.setWalletForm(walletForm);
TabData tabData = new WalletTabData(TabData.TabType.WALLET, walletForm);
tab.setUserData(tabData);
tabs.getTabs().add(tab);
return tab;
} catch(IOException e) {
@@ -862,29 +911,30 @@ public class AppController implements Initializable {
}
Tab tab = new Tab(tabName);
TabData tabData = new TransactionTabData(TabData.TabType.TRANSACTION, transaction, psbt);
tab.setUserData(tabData);
tab.setContextMenu(getTabContextMenu(tab));
tab.setClosable(true);
FXMLLoader transactionLoader = new FXMLLoader(getClass().getResource("transaction/transaction.fxml"));
tab.setContent(transactionLoader.load());
TransactionController controller = transactionLoader.getController();
TransactionData transactionData;
if(psbt != null) {
controller.setPSBT(psbt);
transactionData = new TransactionData(name, psbt);
} else if(blockTransaction != null) {
controller.setBlockTransaction(blockTransaction);
transactionData = new TransactionData(name, blockTransaction);
} else {
controller.setTransaction(transaction);
transactionData = new TransactionData(name, transaction);
}
controller.setName(name);
controller.setTransactionData(transactionData);
if(initialView != null) {
controller.setInitialView(initialView, initialIndex);
}
controller.initializeView();
TabData tabData = new TransactionTabData(TabData.TabType.TRANSACTION, transactionData);
tab.setUserData(tabData);
tabs.getTabs().add(tab);
return tab;
@@ -929,7 +979,7 @@ public class AppController implements Initializable {
TransactionTabSelectedEvent txTabEvent = (TransactionTabSelectedEvent)event;
TransactionTabData transactionTabData = txTabEvent.getTransactionTabData();
saveTransaction.setDisable(false);
saveTransaction.setText("Save " + (transactionTabData.getPsbt() == null || transactionTabData.getPsbt().isFinalized() ? "Transaction..." : "PSBT..."));
saveTransaction.setText("Save " + (transactionTabData.getPsbt() == null || transactionTabData.getPsbt().getTransaction() != transactionTabData.getTransaction() ? "Transaction..." : "PSBT..."));
exportWallet.setDisable(true);
showTxHex.setDisable(false);
} else if(event instanceof WalletTabSelectedEvent) {
@@ -943,12 +993,12 @@ public class AppController implements Initializable {
}
@Subscribe
public void psbtFinalizedEvent(PSBTFinalizedEvent event) {
public void transactionExtractedEvent(TransactionExtractedEvent event) {
for(Tab tab : tabs.getTabs()) {
TabData tabData = (TabData) tab.getUserData();
if(tabData instanceof TransactionTabData) {
TransactionTabData transactionTabData = (TransactionTabData)tabData;
if(Arrays.equals(transactionTabData.getTransaction().bitcoinSerialize(), event.getPsbt().getTransaction().bitcoinSerialize())) {
if(transactionTabData.getTransaction() == event.getFinalTransaction()) {
saveTransaction.setText("Save Transaction...");
}
}
@@ -966,7 +1016,7 @@ public class AppController implements Initializable {
String text;
if(event.getBlockTransactions().size() == 1) {
BlockTransaction blockTransaction = event.getBlockTransactions().get(0);
if(blockTransaction.getHeight() == 0) {
if(blockTransaction.getHeight() <= 0) {
text = "New mempool transaction: ";
} else {
int confirmations = blockTransaction.getConfirmations(getCurrentBlockHeight());
@@ -1002,11 +1052,16 @@ public class AppController implements Initializable {
.title("Sparrow - " + event.getWallet().getName())
.text(text)
.graphic(new ImageView(image))
.hideAfter(Duration.seconds(5))
.hideAfter(Duration.seconds(15))
.position(Pos.TOP_RIGHT)
.threshold(5, Notifications.create().title("Sparrow").text("Multiple new wallet transactions").graphic(new ImageView(image)))
.onAction(e -> selectTab(event.getWallet()));
//If controlsfx can't find our window, we must set the window ourselves (unfortunately notification is then shown within this window)
if(org.controlsfx.tools.Utils.getWindow(null) == null) {
notificationBuilder.owner(tabs.getScene().getWindow());
}
notificationBuilder.show();
}
}
@@ -1191,4 +1246,14 @@ public class AppController implements Initializable {
public void requestQRScan(RequestQRScanEvent event) {
openTransactionFromQR(null);
}
@Subscribe
public void requestConnect(RequestConnectEvent event) {
connect();
}
@Subscribe
public void requestDisconnect(RequestDisconnectEvent event) {
disconnect();
}
}
@@ -2,26 +2,25 @@ package com.sparrowwallet.sparrow;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.sparrow.transaction.TransactionData;
public class TransactionTabData extends TabData {
private final Transaction transaction;
private final PSBT psbt;
private final TransactionData transactionData;
public TransactionTabData(TabType type, Transaction transaction) {
this(type, transaction, null);
public TransactionTabData(TabType type, TransactionData transactionData) {
super(type);
this.transactionData = transactionData;
}
public TransactionTabData(TabType type, Transaction transaction, PSBT psbt) {
super(type);
this.transaction = transaction;
this.psbt = psbt;
public TransactionData getTransactionData() {
return transactionData;
}
public Transaction getTransaction() {
return transaction;
return transactionData.getTransaction();
}
public PSBT getPsbt() {
return psbt;
return transactionData.getPsbt();
}
}
@@ -1,34 +1,26 @@
package com.sparrowwallet.sparrow;
import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.event.WalletSettingsChangedEvent;
import com.sparrowwallet.sparrow.io.Storage;
import com.sparrowwallet.sparrow.wallet.WalletForm;
public class WalletTabData extends TabData {
private Wallet wallet;
private final Storage storage;
private final WalletForm walletForm;
public WalletTabData(TabType type, Wallet wallet, Storage storage) {
public WalletTabData(TabType type, WalletForm walletForm) {
super(type);
this.wallet = wallet;
this.storage = storage;
this.walletForm = walletForm;
}
EventManager.get().register(this);
public WalletForm getWalletForm() {
return walletForm;
}
public Wallet getWallet() {
return wallet;
return walletForm.getWallet();
}
public Storage getStorage() {
return storage;
}
@Subscribe
public void walletSettingsChanged(WalletSettingsChangedEvent event) {
if(event.getWalletFile().equals(storage.getWalletFile())) {
wallet = event.getWallet();
}
return walletForm.getStorage();
}
}
@@ -54,7 +54,7 @@ public class ConfirmationProgressIndicator extends StackPane {
arcLengthTimeline.getKeyFrames().add(arcLengthFrame);
sequence.getChildren().add(arcLengthTimeline);
if(newValue.intValue() == BlockTransactionHash.BLOCKS_TO_CONFIRM) {
if(newValue.intValue() >= BlockTransactionHash.BLOCKS_TO_CONFIRM) {
Timeline arcRadiusTimeline = new Timeline();
KeyValue arcRadiusXValue = new KeyValue(arc.radiusXProperty(), 0.0);
KeyValue arcRadiusYValue = new KeyValue(arc.radiusYProperty(), 0.0);
@@ -19,6 +19,9 @@ public class DeviceAddressDialog extends DeviceDialog<String> {
this.keyDerivation = keyDerivation;
EventManager.get().register(this);
setOnCloseRequest(event -> {
EventManager.get().unregister(this);
});
}
@Override
@@ -28,8 +31,6 @@ public class DeviceAddressDialog extends DeviceDialog<String> {
@Subscribe
public void addressDisplayed(AddressDisplayedEvent event) {
EventManager.get().unregister(this);
setResult(event.getAddress());
this.close();
}
}
@@ -15,6 +15,9 @@ public class DeviceSignDialog extends DeviceDialog<PSBT> {
super(devices);
this.psbt = psbt;
EventManager.get().register(this);
setOnCloseRequest(event -> {
EventManager.get().unregister(this);
});
setResultConverter(dialogButton -> dialogButton.getButtonData().isCancelButton() ? null : psbt);
}
@@ -26,9 +29,7 @@ public class DeviceSignDialog extends DeviceDialog<PSBT> {
@Subscribe
public void psbtSigned(PSBTSignedEvent event) {
if(psbt == event.getPsbt()) {
EventManager.get().unregister(this);
setResult(event.getSignedPsbt());
this.close();
}
}
}
@@ -44,6 +44,7 @@ public class FileWalletExportPane extends TitledDescriptionPane {
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Export " + exporter.getWalletModel().toDisplayString() + " File");
fileChooser.setInitialFileName(wallet.getName() + "-" + exporter.getWalletModel().toDisplayString().toLowerCase() + "." + exporter.getExportFileExtension());
File file = fileChooser.showSaveDialog(window);
if(file != null) {
@@ -5,6 +5,7 @@ import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.KeystoreImportEvent;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.*;
import javafx.beans.property.SimpleListProperty;
import javafx.beans.property.SimpleStringProperty;
@@ -21,6 +22,7 @@ import javafx.util.Callback;
import org.controlsfx.control.textfield.AutoCompletionBinding;
import org.controlsfx.control.textfield.CustomTextField;
import org.controlsfx.control.textfield.TextFields;
import org.controlsfx.glyphfont.Glyph;
import org.controlsfx.validation.ValidationResult;
import org.controlsfx.validation.ValidationSupport;
import org.controlsfx.validation.Validator;
@@ -29,6 +31,7 @@ import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
@@ -39,7 +42,11 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
private SplitMenuButton importButton;
private TilePane wordsPane;
private Button verifyButton;
private Button generateButton;
private Label validLabel;
private Label invalidLabel;
private Button calculateButton;
private Button backButton;
private Button confirmButton;
private List<String> generatedMnemonicCode;
@@ -56,7 +63,7 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
}
public MnemonicKeystoreImportPane(Keystore keystore) {
super(keystore.getSeed().getType().getName(), keystore.getSeed().needsPassphrase() ? "Passphrase enabled" : "Passphrase disabled", "", "image/" + WalletModel.SEED + ".png");
super(keystore.getSeed().getType().getName(), keystore.getSeed().needsPassphrase() ? "Passphrase enabled" : "Passphrase disabled", "", "image/" + WalletModel.SEED.getType() + ".png");
this.wallet = null;
this.importer = null;
showHideLink.setVisible(false);
@@ -74,7 +81,7 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
private void createEnterMnemonicButton() {
enterMnemonicButton = new SplitMenuButton();
enterMnemonicButton.setAlignment(Pos.CENTER_RIGHT);
enterMnemonicButton.setText("Enter Mnemonic");
enterMnemonicButton.setText("Set Words Length");
enterMnemonicButton.setOnAction(event -> {
enterMnemonic(24);
});
@@ -148,49 +155,102 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
if(!displayWordsOnly) {
PassphraseEntry passphraseEntry = new PassphraseEntry();
passphraseEntry.setPadding(new Insets(0, 32, 0, 10));
passphraseEntry.setPadding(new Insets(0, 26, 10, 10));
vBox.getChildren().add(passphraseEntry);
AnchorPane buttonPane = new AnchorPane();
buttonPane.setPadding(new Insets(0, 32, 0, 10));
buttonPane.setPadding(new Insets(0, 26, 0, 10));
Button generateButton = new Button("Generate New");
generateButton = new Button("Generate New");
generateButton.setOnAction(event -> {
generateNew();
});
generateButton.managedProperty().bind(generateButton.visibleProperty());
generateButton.setTooltip(new Tooltip("Generate a unique set of words that provide the seed for your wallet"));
buttonPane.getChildren().add(generateButton);
AnchorPane.setLeftAnchor(generateButton, 0.0);
confirmButton = new Button("Confirm Backup");
validLabel = new Label("Valid checksum", getValidGlyph());
validLabel.setContentDisplay(ContentDisplay.LEFT);
validLabel.setGraphicTextGap(5.0);
validLabel.managedProperty().bind(validLabel.visibleProperty());
validLabel.setVisible(false);
buttonPane.getChildren().add(validLabel);
AnchorPane.setTopAnchor(validLabel, 5.0);
AnchorPane.setLeftAnchor(validLabel, 0.0);
invalidLabel = new Label("Invalid checksum", getInvalidGlyph());
invalidLabel.setContentDisplay(ContentDisplay.LEFT);
invalidLabel.setGraphicTextGap(5.0);
invalidLabel.managedProperty().bind(invalidLabel.visibleProperty());
invalidLabel.setVisible(false);
buttonPane.getChildren().add(invalidLabel);
AnchorPane.setTopAnchor(invalidLabel, 5.0);
AnchorPane.setLeftAnchor(invalidLabel, 0.0);
confirmButton = new Button("Confirm Written Backup");
confirmButton.setOnAction(event -> {
confirmBackup();
});
confirmButton.managedProperty().bind(confirmButton.visibleProperty());
confirmButton.setVisible(false);
confirmButton.setDefaultButton(true);
confirmButton.setTooltip(new Tooltip("Write down the words above as a backup - you will need to re-enter them to confirm your backup is correct"));
buttonPane.getChildren().add(confirmButton);
AnchorPane.setRightAnchor(confirmButton, 0.0);
verifyButton = new Button("Verify");
verifyButton.setDisable(true);
verifyButton.setDefaultButton(true);
verifyButton.setOnAction(event -> {
calculateButton = new Button("Calculate Seed");
calculateButton.setDisable(true);
calculateButton.setDefaultButton(true);
calculateButton.setOnAction(event -> {
prepareImport();
});
verifyButton.managedProperty().bind(verifyButton.visibleProperty());
calculateButton.managedProperty().bind(calculateButton.visibleProperty());
calculateButton.setTooltip(new Tooltip("Calculate the seed from the provided word list"));
backButton = new Button("Back");
backButton.setOnAction(event -> {
displayMnemonicCode();
});
backButton.managedProperty().bind(backButton.visibleProperty());
backButton.setTooltip(new Tooltip("Go back to the generated word list"));
backButton.setVisible(false);
wordEntriesProperty.addListener((ListChangeListener<String>) c -> {
boolean empty = true;
boolean validWords = true;
boolean validChecksum = false;
for(String word : wordEntryList) {
if(!word.isEmpty()) {
empty = false;
}
if(!WordEntry.isValid(word)) {
verifyButton.setDisable(true);
return;
validWords = false;
}
}
verifyButton.setDisable(false);
if(!empty && validWords) {
try {
importer.getKeystore(wallet.getScriptType().getDefaultDerivation(), wordEntriesProperty.get(), passphraseProperty.get());
validChecksum = true;
} catch(ImportException e) {
//ignore
}
}
generateButton.setVisible(empty && generatedMnemonicCode == null);
calculateButton.setDisable(!validChecksum);
validLabel.setVisible(validChecksum);
invalidLabel.setVisible(!validChecksum && !empty);
});
buttonPane.getChildren().add(verifyButton);
AnchorPane.setRightAnchor(verifyButton, 0.0);
HBox rightBox = new HBox();
rightBox.setSpacing(10);
rightBox.getChildren().addAll(backButton, calculateButton);
buttonPane.getChildren().add(rightBox);
AnchorPane.setRightAnchor(rightBox, 0.0);
vBox.getChildren().add(buttonPane);
}
@@ -199,14 +259,23 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
}
private void generateNew() {
setDescription("Write down word list to confirm backup");
showHideLink.setVisible(false);
int mnemonicSeedLength = wordEntriesProperty.get().size() * 11;
int entropyLength = mnemonicSeedLength - (mnemonicSeedLength/33);
DeterministicSeed deterministicSeed = new DeterministicSeed(new SecureRandom(), entropyLength, "");
generatedMnemonicCode = deterministicSeed.getMnemonicCode();
displayMnemonicCode();
}
private void displayMnemonicCode() {
setDescription("Write down word list to confirm backup");
showHideLink.setVisible(false);
calculateButton.setVisible(false);
confirmButton.setVisible(true);
backButton.setVisible(false);
if(generatedMnemonicCode.size() != wordsPane.getChildren().size()) {
throw new IllegalStateException("Generated mnemonic words list not same size as displayed words list");
}
@@ -216,9 +285,6 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
wordEntry.getEditor().setText(generatedMnemonicCode.get(i));
wordEntry.getEditor().setEditable(false);
}
verifyButton.setVisible(false);
confirmButton.setVisible(true);
}
private void confirmBackup() {
@@ -226,6 +292,8 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
showHideLink.setVisible(false);
setContent(getMnemonicWordsEntry(wordEntriesProperty.get().size(), false));
setExpanded(true);
backButton.setVisible(true);
generateButton.setVisible(false);
}
private void prepareImport() {
@@ -235,7 +303,7 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
}
if(importKeystore(wallet.getScriptType().getDefaultDerivation(), true)) {
setExpanded(false);
setExpanded(true);
enterMnemonicButton.setVisible(false);
importButton.setVisible(true);
importButton.setDisable(false);
@@ -277,13 +345,14 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
setSpacing(10);
Label label = new Label((wordNumber+1) + ".");
label.setPrefWidth(20);
label.setPrefWidth(22);
label.setAlignment(Pos.CENTER_RIGHT);
wordField = new TextField();
wordField.setMaxWidth(100);
wordList = Bip39MnemonicCode.INSTANCE.getWordList();
TextFields.bindAutoCompletion(wordField, new WordlistSuggestionProvider(wordList));
AutoCompletionBinding<String> autoCompletionBinding = TextFields.bindAutoCompletion(wordField, new WordlistSuggestionProvider(wordList));
autoCompletionBinding.setDelay(50);
ValidationSupport validationSupport = new ValidationSupport();
validationSupport.registerValidator(wordField, Validator.combine(
@@ -320,6 +389,10 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
List<String> suggestions = new ArrayList<>();
if(!request.getUserText().isEmpty()) {
for(String word : wordList) {
if(word.equals(request.getUserText())) {
return Collections.emptyList();
}
if(word.startsWith(request.getUserText())) {
suggestions.add(word);
}
@@ -341,7 +414,11 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
passphraseProperty.bind(passphraseField.textProperty());
passphraseField.setPromptText("Leave blank for none");
getChildren().addAll(passphraseLabel, passphraseField);
HelpLabel helpLabel = new HelpLabel();
helpLabel.setStyle("-fx-padding: 0 0 0 0");
helpLabel.setHelpText("A passphrase provides optional added security - it is not stored so it must be remembered!");
getChildren().addAll(passphraseLabel, passphraseField, helpLabel);
}
}
@@ -392,4 +469,18 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
wordEntry.getEditor().setEditable(false);
}
}
public static Glyph getValidGlyph() {
Glyph validGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CHECK_CIRCLE);
validGlyph.getStyleClass().add("valid-checksum");
validGlyph.setFontSize(12);
return validGlyph;
}
public static Glyph getInvalidGlyph() {
Glyph invalidGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_CIRCLE);
invalidGlyph.getStyleClass().add("invalid-checksum");
invalidGlyph.setFontSize(12);
return invalidGlyph;
}
}
@@ -2,9 +2,11 @@ package com.sparrowwallet.sparrow.control;
import com.github.sarxos.webcam.WebcamResolution;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.protocol.Base43;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.uri.BitcoinURI;
import com.sparrowwallet.sparrow.ur.ResultType;
import com.sparrowwallet.sparrow.ur.UR;
import com.sparrowwallet.sparrow.ur.URDecoder;
@@ -99,6 +101,24 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
} else {
PSBT psbt;
Transaction transaction;
BitcoinURI bitcoinURI;
Address address;
try {
bitcoinURI = new BitcoinURI(qrtext);
result = new Result(bitcoinURI);
return;
} catch(Exception e) {
//Ignore, not an BIP 21 URI
}
try {
address = Address.fromString(qrtext);
result = new Result(address);
return;
} catch(Exception e) {
//Ignore, not an address
}
try {
psbt = PSBT.fromString(qrtext);
result = new Result(psbt);
@@ -132,9 +152,8 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
}
//Try Base43 used by Electrum
byte[] base43 = Base43.decode(qrResult.getText());
try {
psbt = new PSBT(base43);
psbt = new PSBT(Base43.decode(qrResult.getText()));
result = new Result(psbt);
return;
} catch(Exception e) {
@@ -142,14 +161,14 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
}
try {
transaction = new Transaction(base43);
transaction = new Transaction(Base43.decode(qrResult.getText()));
result = new Result(transaction);
return;
} catch(Exception e) {
//Ignore, not parseable as base43 decoded bytes
}
result = new Result("Cannot parse QR code into a PSBT or transaction");
result = new Result("Cannot parse QR code into a PSBT, transaction or address");
}
}
}
@@ -157,12 +176,14 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
public static class Result {
public final Transaction transaction;
public final PSBT psbt;
public final BitcoinURI uri;
public final String error;
public final Throwable exception;
public Result(Transaction transaction) {
this.transaction = transaction;
this.psbt = null;
this.uri = null;
this.error = null;
this.exception = null;
}
@@ -170,6 +191,23 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
public Result(PSBT psbt) {
this.transaction = null;
this.psbt = psbt;
this.uri = null;
this.error = null;
this.exception = null;
}
public Result(BitcoinURI uri) {
this.transaction = null;
this.psbt = null;
this.uri = uri;
this.error = null;
this.exception = null;
}
public Result(Address address) {
this.transaction = null;
this.psbt = null;
this.uri = BitcoinURI.fromAddress(address);
this.error = null;
this.exception = null;
}
@@ -177,6 +215,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
public Result(String error) {
this.transaction = null;
this.psbt = null;
this.uri = null;
this.error = error;
this.exception = null;
}
@@ -184,6 +223,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
public Result(Throwable exception) {
this.transaction = null;
this.psbt = null;
this.uri = null;
this.error = null;
this.exception = exception;
}
@@ -125,7 +125,7 @@ public class TitledDescriptionPane extends TitledPane {
contentBox.setPadding(new Insets(10, 30, 10, 30));
double width = TextUtils.computeTextWidth(details.getFont(), message, 0.0D);
double numLines = Math.max(1, width / 400);
double numLines = Math.max(1, Math.ceil(width / 400d));
//Handle long words like txids
OptionalDouble maxWordLength = Arrays.stream(message.split(" ")).mapToDouble(word -> TextUtils.computeTextWidth(details.getFont(), message, 0.0D)).max();
@@ -133,7 +133,7 @@ public class TitledDescriptionPane extends TitledPane {
numLines += 1.0;
}
double height = Math.max(60, numLines * 40);
double height = Math.max(60, numLines * 20);
contentBox.setPrefHeight(height);
return contentBox;
@@ -62,7 +62,7 @@ public class UtxosChart extends BarChart<String, Number> {
}
if(utxoSeries.getData().size() > utxoDataList.size()) {
utxoSeries.getData().remove(utxoDataList.size() - 1, utxoSeries.getData().size());
utxoSeries.getData().remove(Math.max(0, utxoDataList.size() - 1), utxoSeries.getData().size());
}
if(selectedEntries != null) {
@@ -21,6 +21,9 @@ public class WalletExportDialog extends Dialog<Wallet> {
public WalletExportDialog(Wallet wallet) {
EventManager.get().register(this);
setOnCloseRequest(event -> {
EventManager.get().unregister(this);
});
final DialogPane dialogPane = getDialogPane();
@@ -64,9 +67,7 @@ public class WalletExportDialog extends Dialog<Wallet> {
@Subscribe
public void walletExported(WalletExportEvent event) {
EventManager.get().unregister(this);
wallet = event.getWallet();
setResult(wallet);
this.close();
}
}
@@ -16,6 +16,9 @@ public class WalletImportDialog extends Dialog<Wallet> {
public WalletImportDialog() {
EventManager.get().register(this);
setOnCloseRequest(event -> {
EventManager.get().unregister(this);
});
final DialogPane dialogPane = getDialogPane();
@@ -51,9 +54,7 @@ public class WalletImportDialog extends Dialog<Wallet> {
@Subscribe
public void walletImported(WalletImportEvent event) {
EventManager.get().unregister(this);
wallet = event.getWallet();
setResult(wallet);
this.close();
}
}
@@ -4,8 +4,12 @@ import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.image.ImageView;
import javafx.scene.layout.Region;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class WebcamView {
private static final Logger log = LoggerFactory.getLogger(WebcamView.class);
private final ImageView imageView;
private final WebcamService service;
private final Region view;
@@ -47,7 +51,7 @@ public class WebcamView {
imageView.imageProperty().unbind();
statusPlaceholder.setText("Error");
getChildren().setAll(statusPlaceholder);
service.getException().printStackTrace();
log.error("Failed to start web cam", service.getException());
break;
case SUCCEEDED:
// unreachable...
@@ -29,7 +29,7 @@ public class WelcomeDialog extends Dialog<Mode> {
dialogPane.setHeaderText("Welcome to Sparrow!");
dialogPane.getStylesheets().add(AppController.class.getResource("general.css").toExternalForm());
dialogPane.setPrefWidth(600);
dialogPane.setPrefHeight(450);
dialogPane.setPrefHeight(480);
Image image = new Image("image/sparrow-small.png", 50, 50, false, false);
if (!image.isError()) {
@@ -0,0 +1,5 @@
package com.sparrowwallet.sparrow.event;
public class RequestConnectEvent {
//Empty event class used to request programmatic connection to the server
}
@@ -0,0 +1,5 @@
package com.sparrowwallet.sparrow.event;
public class RequestDisconnectEvent {
//Empty event class used to request programmatic disconnection from the server
}
@@ -0,0 +1,17 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.sparrow.TransactionTabData;
import java.util.List;
public class TransactionTabsClosedEvent {
private final List<TransactionTabData> closedTransactionTabData;
public TransactionTabsClosedEvent(List<TransactionTabData> closedTransactionTabData) {
this.closedTransactionTabData = closedTransactionTabData;
}
public List<TransactionTabData> getClosedTransactionTabData() {
return closedTransactionTabData;
}
}
@@ -40,4 +40,8 @@ public class WalletNodeHistoryChangedEvent {
return null;
}
public String getScriptHash() {
return scriptHash;
}
}
@@ -0,0 +1,17 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.sparrow.WalletTabData;
import java.util.List;
public class WalletTabsClosedEvent {
private final List<WalletTabData> closedWalletTabData;
public WalletTabsClosedEvent(List<WalletTabData> closedWalletTabData) {
this.closedWalletTabData = closedWalletTabData;
}
public List<WalletTabData> getClosedWalletTabData() {
return closedWalletTabData;
}
}
@@ -15,6 +15,7 @@ public class FontAwesome5 extends GlyphFont {
* The individual glyphs offered by the FontAwesome5 font.
*/
public static enum Glyph implements INamedCharacter {
ANGLE_DOUBLE_RIGHT('\uf101'),
ARROW_DOWN('\uf063'),
ARROW_UP('\uf062'),
BTC('\uf15a'),
@@ -23,6 +24,8 @@ public class FontAwesome5 extends GlyphFont {
CIRCLE('\uf111'),
COINS('\uf51e'),
EXCLAMATION_CIRCLE('\uf06a'),
EXCLAMATION_TRIANGLE('\uf071'),
EXTERNAL_LINK_ALT('\uf35d'),
ELLIPSIS_H('\uf141'),
EYE('\uf06e'),
HAND_HOLDING('\uf4bd'),
@@ -37,6 +40,7 @@ public class FontAwesome5 extends GlyphFont {
SD_CARD('\uf7c2'),
SEARCH('\uf002'),
TOOLS('\uf7d9'),
UNDO('\uf0e2'),
WALLET('\uf555');
private final char ch;
@@ -21,7 +21,7 @@ public class Bip39 implements KeystoreMnemonicImport {
@Override
public String getKeystoreImportDescription() {
return "Import your 12 to 24 word mnemonic and optional passphrase";
return "Import or generate your 12 to 24 word mnemonic and optional passphrase.";
}
@Override
@@ -68,7 +68,12 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
@Override
public String getKeystoreImportDescription() {
return "Import file created by using the Settings > Multisig Wallets > Export XPUB feature on your Coldcard";
return "Import file created by using the Settings > Multisig Wallets > Export XPUB feature on your Coldcard.";
}
@Override
public String getExportFileExtension() {
return "txt";
}
@Override
@@ -138,7 +143,7 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
@Override
public String getWalletImportDescription() {
return "Import file created by using the Settings > Multisig Wallets > [Wallet Detail] > Coldcard Export feature on your Coldcard";
return "Import file created by using the Settings > Multisig Wallets > [Wallet Detail] > Coldcard Export feature on your Coldcard.";
}
@Override
@@ -191,7 +196,7 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
@Override
public String getWalletExportDescription() {
return "Export file that can be read by your Coldcard using the Settings > Multisig Wallets > Import from SD feature";
return "Export file that can be read by your Coldcard using the Settings > Multisig Wallets > Import from SD feature.";
}
@Override
@@ -24,7 +24,7 @@ public class ColdcardSinglesig implements KeystoreFileImport {
@Override
public String getKeystoreImportDescription() {
return "Import file created by using the Advanced > MicroSD > Export Wallet > Generic JSON feature on your Coldcard";
return "Import file created by using the Advanced > MicroSD > Export Wallet > Generic JSON feature on your Coldcard. Note this requires firmware version 3.1.3 or later.";
}
@Override
@@ -14,10 +14,7 @@ import com.sparrowwallet.drongo.wallet.*;
import java.io.*;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Base64;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.*;
import java.util.zip.InflaterInputStream;
public class Electrum implements KeystoreFileImport, WalletImport, WalletExport {
@@ -33,7 +30,7 @@ public class Electrum implements KeystoreFileImport, WalletImport, WalletExport
@Override
public String getKeystoreImportDescription() {
return "Import a single keystore from an Electrum wallet (use File > Import > Electrum to import a multisig wallet)";
return "Import a single keystore from an Electrum wallet (use File > Import > Electrum to import a multisig wallet).";
}
@Override
@@ -46,7 +43,7 @@ public class Electrum implements KeystoreFileImport, WalletImport, WalletExport
if(!wallet.getScriptType().equals(scriptType)) {
//TODO: Derive appropriate ScriptType keystore from xprv if present
throw new ImportException("Wallet has an incompatible script type of " + wallet.getScriptType() + ", and the correct script type cannot be derived without the master private key");
throw new ImportException("Wallet has an incompatible script type of " + wallet.getScriptType() + ", and the correct script type cannot be derived without the master private key.");
}
return wallet.getKeystores().get(0);
@@ -105,11 +102,12 @@ public class Electrum implements KeystoreFileImport, WalletImport, WalletExport
keystore.setSource(KeystoreSource.HW_USB);
keystore.setWalletModel(WalletModel.fromType(ek.hw_type));
if(keystore.getWalletModel() == null) {
throw new ImportException("Wallet has keystore of unknown hardware wallet type \"" + ek.hw_type + "\"");
throw new ImportException("Wallet has keystore of unknown hardware wallet type \"" + ek.hw_type + "\".");
}
} else if("bip32".equals(ek.type)) {
if(ek.xprv != null && ek.seed == null) {
throw new ImportException("Electrum does not support exporting BIP39 derived seeds.");
throw new ImportException("Electrum does not support exporting BIP39 derived seeds, as it does not store the mnemonic words. Only seeds created with its native Electrum Seed Version System are exportable. " +
"If you have the mnemonic words, create a new wallet with a BIP39 keystore.");
} else if(ek.seed != null) {
keystore.setSource(KeystoreSource.SW_SEED);
String mnemonic = ek.seed;
@@ -181,7 +179,12 @@ public class Electrum implements KeystoreFileImport, WalletImport, WalletExport
@Override
public String getWalletImportDescription() {
return "Import an Electrum wallet";
return "Import an Electrum wallet.";
}
@Override
public String getExportFileExtension() {
return "json";
}
@Override
@@ -263,12 +266,12 @@ public class Electrum implements KeystoreFileImport, WalletImport, WalletExport
@Override
public boolean isEncrypted(File file) {
return FileType.BINARY.equals(IOUtils.getFileType(file));
return (FileType.BINARY.equals(IOUtils.getFileType(file)));
}
@Override
public String getWalletExportDescription() {
return "Export this wallet as an Electrum wallet file";
return "Export this wallet as an Electrum wallet file.";
}
private static class ElectrumJsonWallet {
@@ -7,7 +7,7 @@ public class IOUtils {
public static FileType getFileType(File file) {
try {
String type = Files.probeContentType(file.toPath());
if (type == null) {
if(type == null) {
if(file.getName().toLowerCase().endsWith("txn") || file.getName().toLowerCase().endsWith("psbt")) {
return FileType.TEXT;
}
@@ -17,6 +17,8 @@ public class IOUtils {
String line = br.readLine();
if(line.startsWith("01000000") || line.startsWith("cHNid")) {
return FileType.TEXT;
} else if(line.startsWith("{")) {
return FileType.JSON;
}
}
}
@@ -7,4 +7,5 @@ import java.io.OutputStream;
public interface WalletExport extends Export {
void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException;
String getWalletExportDescription();
String getExportFileExtension();
}
@@ -27,6 +27,10 @@ public class KeystoreImportDialog extends Dialog<Keystore> {
public KeystoreImportDialog(Wallet wallet, KeystoreSource initialSource) {
EventManager.get().register(this);
setOnCloseRequest(event -> {
EventManager.get().unregister(this);
});
final DialogPane dialogPane = getDialogPane();
try {
@@ -39,7 +43,7 @@ public class KeystoreImportDialog extends Dialog<Keystore> {
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
dialogPane.getButtonTypes().addAll(cancelButtonType);
dialogPane.setPrefWidth(650);
dialogPane.setPrefHeight(600);
dialogPane.setPrefHeight(620);
setResultConverter(dialogButton -> dialogButton != cancelButtonType ? keystore : null);
} catch(IOException e) {
@@ -55,6 +59,5 @@ public class KeystoreImportDialog extends Dialog<Keystore> {
public void keystoreImported(KeystoreImportEvent event) {
this.keystore = event.getKeystore();
setResult(keystore);
this.close();
}
}
@@ -167,7 +167,7 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
@Override
@SuppressWarnings("unchecked")
public Map<String, VerboseTransaction> getVerboseTransactions(Transport transport, Set<String> txids) {
public Map<String, VerboseTransaction> getVerboseTransactions(Transport transport, Set<String> txids, String scriptHash) {
JsonRpcClient client = new JsonRpcClient(transport);
BatchRequestBuilder<String, VerboseTransaction> batchRequest = client.createBatchRequest().keysType(String.class).returnType(VerboseTransaction.class);
for(String txid : txids) {
@@ -479,13 +479,13 @@ public class ElectrumServer {
}
}
public Map<Sha256Hash, BlockTransaction> getReferencedTransactions(Set<Sha256Hash> references) throws ServerException {
public Map<Sha256Hash, BlockTransaction> getReferencedTransactions(Set<Sha256Hash> references, String scriptHash) throws ServerException {
Set<String> txids = new LinkedHashSet<>(references.size());
for(Sha256Hash reference : references) {
txids.add(reference.toString());
}
Map<String, VerboseTransaction> result = electrumServerRpc.getVerboseTransactions(getTransport(), txids);
Map<String, VerboseTransaction> result = electrumServerRpc.getVerboseTransactions(getTransport(), txids, scriptHash);
Map<Sha256Hash, BlockTransaction> transactionMap = new HashMap<>();
for(String txid : result.keySet()) {
@@ -535,13 +535,13 @@ public class ElectrumServer {
return Utils.bytesToHex(reversed);
}
private String getScriptHash(TransactionOutput output) {
public static String getScriptHash(TransactionOutput output) {
byte[] hash = Sha256Hash.hash(output.getScript().getProgram());
byte[] reversed = Utils.reverseBytes(hash);
return Utils.bytesToHex(reversed);
}
static Map<String, String> getSubscribedScriptHashes() {
public static Map<String, String> getSubscribedScriptHashes() {
return subscribedScriptHashes;
}
@@ -579,7 +579,6 @@ public class ElectrumServer {
private final boolean subscribe;
private boolean firstCall = true;
private Thread reader;
private Throwable lastReaderException;
private long feeRatesRetrievedAt;
public ConnectionService() {
@@ -598,7 +597,7 @@ public class ElectrumServer {
if(firstCall) {
electrumServer.connect();
reader = new Thread(new ReadRunnable());
reader = new Thread(new ReadRunnable(), "ElectrumServerReadThread");
reader.setDaemon(true);
reader.setUncaughtExceptionHandler(ConnectionService.this);
reader.start();
@@ -639,8 +638,7 @@ public class ElectrumServer {
return new FeeRatesUpdatedEvent(blockTargetFeeRates);
}
} else {
firstCall = true;
throw new ServerException("Connection to server failed", lastReaderException);
resetConnection();
}
}
@@ -649,12 +647,21 @@ public class ElectrumServer {
};
}
public void resetConnection() {
try {
closeActiveConnection();
firstCall = true;
} catch (ServerException e) {
log.error("Error closing connection during connection reset", e);
}
}
@Override
public boolean cancel() {
try {
closeActiveConnection();
} catch (ServerException e) {
log.error("Eror closing connection", e);
log.error("Error closing connection", e);
}
return super.cancel();
@@ -664,12 +671,11 @@ public class ElectrumServer {
public void reset() {
super.reset();
firstCall = true;
lastReaderException = null;
}
@Override
public void uncaughtException(Thread t, Throwable e) {
this.lastReaderException = e;
log.error("Uncaught error in ConnectionService", e);
}
}
@@ -679,8 +685,9 @@ public class ElectrumServer {
try {
TcpTransport tcpTransport = (TcpTransport)getTransport();
tcpTransport.readInputLoop();
} catch (ServerException e) {
throw new RuntimeException(e.getCause() != null ? e.getCause() : e);
} catch(ServerException e) {
//Only debug logging here as the exception has been passed on to the ConnectionService thread via TcpTransport
log.debug("Read thread terminated", e);
}
}
}
@@ -708,6 +715,7 @@ public class ElectrumServer {
public static class TransactionReferenceService extends Service<Map<Sha256Hash, BlockTransaction>> {
private final Set<Sha256Hash> references;
private String scriptHash;
public TransactionReferenceService(Transaction transaction) {
references = new HashSet<>();
@@ -717,6 +725,11 @@ public class ElectrumServer {
}
}
public TransactionReferenceService(Set<Sha256Hash> references, String scriptHash) {
this(references);
this.scriptHash = scriptHash;
}
public TransactionReferenceService(Set<Sha256Hash> references) {
this.references = references;
}
@@ -726,7 +739,7 @@ public class ElectrumServer {
return new Task<>() {
protected Map<Sha256Hash, BlockTransaction> call() throws ServerException {
ElectrumServer electrumServer = new ElectrumServer();
return electrumServer.getReferencedTransactions(references);
return electrumServer.getReferencedTransactions(references, scriptHash);
}
};
}
@@ -25,7 +25,7 @@ public interface ElectrumServerRpc {
Map<String, String> getTransactions(Transport transport, Set<String> txids);
Map<String, VerboseTransaction> getVerboseTransactions(Transport transport, Set<String> txids);
Map<String, VerboseTransaction> getVerboseTransactions(Transport transport, Set<String> txids, String scriptHash);
Map<Integer, Double> getFeeEstimates(Transport transport, List<Integer> targetBlocks);
@@ -1,10 +1,12 @@
package com.sparrowwallet.sparrow.net;
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
import com.github.arteam.simplejsonrpc.client.JsonRpcClient;
import com.github.arteam.simplejsonrpc.client.Transport;
import com.github.arteam.simplejsonrpc.client.exception.JsonRpcException;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.sparrow.AppController;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -17,6 +19,7 @@ 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;
@Override
public void ping(Transport transport) {
@@ -156,7 +159,7 @@ public class SimpleElectrumServerRpc implements ElectrumServerRpc {
}
@Override
public Map<String, VerboseTransaction> getVerboseTransactions(Transport transport, Set<String> txids) {
public Map<String, VerboseTransaction> getVerboseTransactions(Transport transport, Set<String> txids, String scriptHash) {
JsonRpcClient client = new JsonRpcClient(transport);
Map<String, VerboseTransaction> result = new LinkedHashMap<>();
@@ -166,6 +169,27 @@ public class SimpleElectrumServerRpc implements ElectrumServerRpc {
result.put(txid, verboseTransaction);
} catch(IllegalStateException | IllegalArgumentException e) {
log.warn("Error retrieving transaction: " + txid + " (" + (e.getCause() != null ? e.getCause().getMessage() : e.getMessage()) + ")");
String rawTxHex = client.createRequest().returnAs(String.class).method("blockchain.transaction.get").id(txid).params(txid).execute();
Transaction tx = new Transaction(Utils.hexToBytes(rawTxHex));
String id = tx.getTxId().toString();
int height = 0;
if(scriptHash != null) {
ScriptHashTx[] scriptHashTxes = client.createRequest().returnAs(ScriptHashTx[].class).method("blockchain.scripthash.get_history").id(id).params(scriptHash).execute();
for(ScriptHashTx scriptHashTx : scriptHashTxes) {
if(scriptHashTx.tx_hash.equals(id)) {
height = scriptHashTx.height;
break;
}
}
}
VerboseTransaction verboseTransaction = new VerboseTransaction();
verboseTransaction.txid = id;
verboseTransaction.hex = rawTxHex;
verboseTransaction.confirmations = (height <= 0 ? 0 : AppController.getCurrentBlockHeight() - height + 1);
verboseTransaction.blockhash = Sha256Hash.ZERO_HASH.toString();
result.put(txid, verboseTransaction);
} catch(JsonRpcException e) {
log.warn("Error retrieving transaction: " + txid + " (" + e.getErrorMessage() + ")");
}
@@ -180,14 +204,18 @@ public class SimpleElectrumServerRpc implements ElectrumServerRpc {
Map<Integer, Double> result = new LinkedHashMap<>();
for(Integer targetBlock : targetBlocks) {
try {
Double targetBlocksFeeRateBtcKb = client.createRequest().returnAs(Double.class).method("blockchain.estimatefee").id(targetBlock).params(targetBlock).execute();
result.put(targetBlock, targetBlocksFeeRateBtcKb);
} catch(IllegalStateException | IllegalArgumentException e) {
log.warn("Failed to retrieve fee rate for target blocks: " + targetBlock + " (" + e.getMessage() + ")");
result.put(targetBlock, DUST_RELAY_TX_FEE);
} catch(JsonRpcException e) {
throw new ElectrumServerRpcException("Failed to retrieve fee rate for target blocks: " + targetBlock, e);
if(targetBlock <= MAX_TARGET_BLOCKS) {
try {
Double targetBlocksFeeRateBtcKb = client.createRequest().returnAs(Double.class).method("blockchain.estimatefee").id(targetBlock).params(targetBlock).execute();
result.put(targetBlock, targetBlocksFeeRateBtcKb);
} catch(IllegalStateException | IllegalArgumentException e) {
log.warn("Failed to retrieve fee rate for target blocks: " + targetBlock + " (" + e.getMessage() + ")");
result.put(targetBlock, 1d);
} catch(JsonRpcException e) {
throw new ElectrumServerRpcException("Failed to retrieve fee rate for target blocks: " + targetBlock, e);
}
} else {
result.put(targetBlock, 1d);
}
}
@@ -5,6 +5,8 @@ import com.github.arteam.simplejsonrpc.server.JsonRpcServer;
import com.google.common.net.HostAndPort;
import com.sparrowwallet.sparrow.io.Config;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.net.SocketFactory;
import java.io.*;
@@ -12,6 +14,8 @@ import java.net.Socket;
import java.util.concurrent.locks.ReentrantLock;
public class TcpTransport implements Transport, Closeable {
private static final Logger log = LoggerFactory.getLogger(TcpTransport.class);
public static final int DEFAULT_PORT = 50001;
protected final HostAndPort server;
@@ -24,10 +28,13 @@ public class TcpTransport implements Transport, Closeable {
private final ReentrantLock clientRequestLock = new ReentrantLock();
private boolean running = false;
private boolean reading = true;
private boolean firstRead = true;
private final JsonRpcServer jsonRpcServer = new JsonRpcServer();
private final SubscriptionService subscriptionService = new SubscriptionService();
private Exception lastException;
public TcpTransport(HostAndPort server) {
this.server = server;
this.socketFactory = SocketFactory.getDefault();
@@ -50,7 +57,12 @@ public class TcpTransport implements Transport, Closeable {
out.flush();
}
private synchronized String readResponse() {
private synchronized String readResponse() throws IOException {
if(firstRead) {
notifyAll();
firstRead = false;
}
while(reading) {
try {
wait();
@@ -60,6 +72,10 @@ public class TcpTransport implements Transport, Closeable {
}
}
if(lastException != null) {
throw new IOException("Error reading response: " + lastException.getMessage(), lastException);
}
reading = true;
notifyAll();
@@ -67,6 +83,13 @@ public class TcpTransport implements Transport, Closeable {
}
public synchronized void readInputLoop() throws ServerException {
try {
//Don't start reading until first RPC request is sent
wait();
} catch(InterruptedException e) {
Thread.currentThread().interrupt();
}
while(running) {
try {
String received = readInputStream();
@@ -83,9 +106,13 @@ public class TcpTransport implements Transport, Closeable {
} catch(InterruptedException e) {
//Restore interrupt status and continue
Thread.currentThread().interrupt();
} catch(IOException e) {
} catch(Exception e) {
if(running) {
throw new ServerException(e);
lastException = e;
reading = false;
notifyAll();
//Allow this thread to terminate as we will need to reconnect with a new transport anyway
running = false;
}
}
}
@@ -22,6 +22,10 @@ class VerboseTransaction {
public int version;
public int getHeight() {
if(confirmations <= 0) {
return 0;
}
Integer currentHeight = AppController.getCurrentBlockHeight();
if(currentHeight != null) {
return currentHeight - confirmations + 1;
@@ -31,6 +35,11 @@ class VerboseTransaction {
}
public Date getDate() {
if(blocktime == 0) {
//Ok to return as null here as date inspection for verbose txes is only done by HeadersController, which checks for null values
return null;
}
return new Date(blocktime * 1000);
}
@@ -1,7 +1,10 @@
package com.sparrowwallet.sparrow.preferences;
import com.sparrowwallet.sparrow.AppController;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.RequestConnectEvent;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.net.ElectrumServer;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.ButtonBar;
import javafx.scene.control.ButtonType;
@@ -12,6 +15,8 @@ import org.controlsfx.tools.Borders;
import java.io.IOException;
public class PreferencesDialog extends Dialog<Boolean> {
private final boolean existingConnection;
public PreferencesDialog() {
this(null);
}
@@ -45,6 +50,13 @@ public class PreferencesDialog extends Dialog<Boolean> {
dialogPane.setPrefWidth(650);
dialogPane.setPrefHeight(500);
existingConnection = ElectrumServer.isConnected();
setOnCloseRequest(event -> {
if(existingConnection && !ElectrumServer.isConnected()) {
EventManager.get().post(new RequestConnectEvent());
}
});
setResultConverter(dialogButton -> dialogButton == newWalletButtonType ? Boolean.TRUE : null);
} catch(IOException e) {
throw new RuntimeException(e);
@@ -1,9 +1,11 @@
package com.sparrowwallet.sparrow.preferences;
import com.google.common.net.HostAndPort;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.TextFieldValidator;
import com.sparrowwallet.sparrow.control.UnlabeledToggleSwitch;
import com.sparrowwallet.sparrow.event.ConnectionEvent;
import com.sparrowwallet.sparrow.event.RequestDisconnectEvent;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.net.ElectrumServer;
@@ -61,6 +63,9 @@ public class ServerPreferencesController extends PreferencesDetailController {
@FXML
private Button testConnection;
@FXML
private Button editConnection;
@FXML
private TextArea testResults;
@@ -125,33 +130,36 @@ public class ServerPreferencesController extends PreferencesDetailController {
}
});
testConnection.setDisable(ElectrumServer.isConnected());
boolean isConnected = ElectrumServer.isConnected();
setFieldsEditable(!isConnected);
testConnection.managedProperty().bind(testConnection.visibleProperty());
testConnection.setVisible(!isConnected);
testConnection.setOnAction(event -> {
testResults.setText("Connecting to " + config.getElectrumServer() + "...");
testConnection.setGraphic(getGlyph(FontAwesome5.Glyph.ELLIPSIS_H, null));
boolean existingConnection = ElectrumServer.isConnected();
if(existingConnection) {
ElectrumServer.ServerBannerService serverBannerService = new ElectrumServer.ServerBannerService();
serverBannerService.setOnSucceeded(successEvent -> {
showConnectionSuccess(null, serverBannerService.getValue());
});
serverBannerService.setOnFailed(this::showConnectionFailure);
serverBannerService.start();
} else {
ElectrumServer.ConnectionService connectionService = new ElectrumServer.ConnectionService(false);
connectionService.setPeriod(Duration.minutes(1));
connectionService.setOnSucceeded(successEvent -> {
ConnectionEvent connectionEvent = (ConnectionEvent)connectionService.getValue();
showConnectionSuccess(connectionEvent.getServerVersion(), connectionEvent.getServerBanner());
connectionService.cancel();
});
connectionService.setOnFailed(workerStateEvent -> {
showConnectionFailure(workerStateEvent);
connectionService.cancel();
});
connectionService.start();
}
ElectrumServer.ConnectionService connectionService = new ElectrumServer.ConnectionService(false);
connectionService.setPeriod(Duration.minutes(1));
connectionService.setOnSucceeded(successEvent -> {
ConnectionEvent connectionEvent = (ConnectionEvent)connectionService.getValue();
showConnectionSuccess(connectionEvent.getServerVersion(), connectionEvent.getServerBanner());
connectionService.cancel();
});
connectionService.setOnFailed(workerStateEvent -> {
showConnectionFailure(workerStateEvent);
connectionService.cancel();
});
connectionService.start();
});
editConnection.managedProperty().bind(editConnection.visibleProperty());
editConnection.setVisible(isConnected);
editConnection.setOnAction(event -> {
EventManager.get().post(new RequestDisconnectEvent());
setFieldsEditable(true);
editConnection.setVisible(false);
testConnection.setVisible(true);
});
String electrumServer = config.getElectrumServer();
@@ -196,6 +204,17 @@ public class ServerPreferencesController extends PreferencesDetailController {
}
}
private void setFieldsEditable(boolean editable) {
host.setEditable(editable);
port.setEditable(editable);
useSsl.setDisable(!editable);
certificate.setEditable(editable);
certificateSelect.setDisable(!editable);
useProxy.setDisable(!editable);
proxyHost.setEditable(editable);
proxyPort.setEditable(editable);
}
private void showConnectionSuccess(List<String> serverVersion, String serverBanner) {
testConnection.setGraphic(getGlyph(FontAwesome5.Glyph.CHECK_CIRCLE, Color.rgb(80, 161, 79)));
if(serverVersion != null) {
@@ -1,5 +1,6 @@
package com.sparrowwallet.sparrow.transaction;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.SecureString;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.protocol.*;
@@ -14,6 +15,9 @@ import com.sparrowwallet.sparrow.glyphfont.FontAwesome5Brands;
import com.sparrowwallet.sparrow.io.Device;
import com.sparrowwallet.sparrow.net.ElectrumServer;
import com.sparrowwallet.sparrow.io.Storage;
import com.sparrowwallet.sparrow.wallet.HashIndexEntry;
import com.sparrowwallet.sparrow.wallet.TransactionEntry;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
@@ -92,9 +96,18 @@ public class HeadersController extends TransactionFormController implements Init
@FXML
private Spinner<Integer> locktimeBlock;
@FXML
private Hyperlink locktimeCurrentHeight;
@FXML
private Label futureBlockWarning;
@FXML
private DateTimePicker locktimeDate;
@FXML
private Label futureDateWarning;
@FXML
private CopyableLabel size;
@@ -192,6 +205,11 @@ public class HeadersController extends TransactionFormController implements Init
initializeView();
}
@Override
protected TransactionForm getTransactionForm() {
return headersForm;
}
private void initializeView() {
Transaction tx = headersForm.getTransaction();
@@ -227,6 +245,8 @@ public class HeadersController extends TransactionFormController implements Init
locktimeFieldset.getChildren().add(locktimeBlockField);
Integer block = locktimeBlock.getValue();
if(block != null) {
locktimeCurrentHeight.setVisible(headersForm.isEditable() && AppController.getCurrentBlockHeight() != null && block < AppController.getCurrentBlockHeight());
futureBlockWarning.setVisible(AppController.getCurrentBlockHeight() != null && block > AppController.getCurrentBlockHeight());
tx.setLocktime(block);
if(old_toggle != null) {
EventManager.get().post(new TransactionChangedEvent(tx));
@@ -240,6 +260,7 @@ public class HeadersController extends TransactionFormController implements Init
LocalDateTime date = locktimeDate.getDateTimeValue();
if(date != null) {
locktimeDate.setDateTimeValue(date);
futureDateWarning.setVisible(date.isAfter(LocalDateTime.now()));
tx.setLocktime(date.toEpochSecond(OffsetDateTime.now(ZoneId.systemDefault()).getOffset()));
if(old_toggle != null) {
EventManager.get().post(new TransactionChangedEvent(tx));
@@ -249,6 +270,13 @@ public class HeadersController extends TransactionFormController implements Init
}
});
locktimeCurrentHeight.managedProperty().bind(locktimeCurrentHeight.visibleProperty());
locktimeCurrentHeight.setVisible(false);
futureBlockWarning.managedProperty().bind(futureBlockWarning.visibleProperty());
futureBlockWarning.setVisible(false);
futureDateWarning.managedProperty().bind(futureDateWarning.visibleProperty());
futureDateWarning.setVisible(false);
locktimeNone.setValueFactory(new SpinnerValueFactory.IntegerSpinnerValueFactory(0, (int)Transaction.MAX_BLOCK_LOCKTIME-1, 0));
if(tx.getLocktime() < Transaction.MAX_BLOCK_LOCKTIME) {
locktimeBlock.setValueFactory(new SpinnerValueFactory.IntegerSpinnerValueFactory(0, (int)Transaction.MAX_BLOCK_LOCKTIME-1, (int)tx.getLocktime()));
@@ -268,6 +296,8 @@ public class HeadersController extends TransactionFormController implements Init
locktimeBlock.valueProperty().addListener((obs, oldValue, newValue) -> {
tx.setLocktime(newValue);
locktimeCurrentHeight.setVisible(headersForm.isEditable() && AppController.getCurrentBlockHeight() != null && newValue < AppController.getCurrentBlockHeight());
futureBlockWarning.setVisible(AppController.getCurrentBlockHeight() != null && newValue > AppController.getCurrentBlockHeight());
if(oldValue != null) {
EventManager.get().post(new TransactionChangedEvent(tx));
EventManager.get().post(new TransactionLocktimeChangedEvent(tx));
@@ -277,16 +307,19 @@ public class HeadersController extends TransactionFormController implements Init
locktimeDate.setFormat(LOCKTIME_DATE_FORMAT);
locktimeDate.dateTimeValueProperty().addListener((obs, oldValue, newValue) -> {
tx.setLocktime(newValue.toEpochSecond(OffsetDateTime.now(ZoneId.systemDefault()).getOffset()));
futureDateWarning.setVisible(newValue.isAfter(LocalDateTime.now()));
if(oldValue != null) {
EventManager.get().post(new TransactionChangedEvent(tx));
}
});
locktimeNoneType.setDisable(!headersForm.isEditable());
locktimeBlockType.setDisable(!headersForm.isEditable());
locktimeDateType.setDisable(!headersForm.isEditable());
locktimeBlock.setDisable(!headersForm.isEditable());
locktimeDate.setDisable(!headersForm.isEditable());
boolean locktimeEnabled = headersForm.getTransaction().isLocktimeSequenceEnabled();
locktimeNoneType.setDisable(!headersForm.isEditable() || !locktimeEnabled);
locktimeBlockType.setDisable(!headersForm.isEditable() || !locktimeEnabled);
locktimeDateType.setDisable(!headersForm.isEditable() || !locktimeEnabled);
locktimeBlock.setDisable(!headersForm.isEditable() || !locktimeEnabled);
locktimeDate.setDisable(!headersForm.isEditable() || !locktimeEnabled);
locktimeCurrentHeight.setDisable(!headersForm.isEditable() || !locktimeEnabled);
updateSize();
@@ -436,7 +469,10 @@ public class HeadersController extends TransactionFormController implements Init
private void updateBlockchainForm(BlockTransaction blockTransaction, Integer currentHeight) {
blockchainForm.setVisible(true);
if(currentHeight == null) {
if(Sha256Hash.ZERO_HASH.equals(blockTransaction.getBlockHash()) && blockTransaction.getHeight() == 0 && headersForm.getSigningWallet() == null) {
//A zero block hash indicates that this blocktransaction is incomplete and the height is likely incorrect if we are not sending a tx
blockStatus.setText("Unknown");
} else if(currentHeight == null) {
blockStatus.setText(blockTransaction.getHeight() > 0 ? "Confirmed" : "Unconfirmed");
} else {
int confirmations = blockTransaction.getHeight() > 0 ? currentHeight - blockTransaction.getHeight() + 1 : 0;
@@ -479,7 +515,7 @@ public class HeadersController extends TransactionFormController implements Init
blockTimestampField.setVisible(false);
}
if(blockTransaction.getBlockHash() != null) {
if(blockTransaction.getBlockHash() != null && !blockTransaction.getBlockHash().equals(Sha256Hash.ZERO_HASH)) {
blockHashField.setVisible(true);
blockHash.setText(blockTransaction.getBlockHash().toString());
blockHash.setContextMenu(new BlockHeightContextMenu(blockTransaction.getBlockHash()));
@@ -530,6 +566,13 @@ public class HeadersController extends TransactionFormController implements Init
Clipboard.getSystemClipboard().setContent(content);
}
public void setLocktimeToCurrentHeight(ActionEvent event) {
if(AppController.getCurrentBlockHeight() != null && locktimeBlock.isEditable()) {
locktimeBlock.getValueFactory().setValue(AppController.getCurrentBlockHeight());
Platform.runLater(() -> locktimeBlockType.requestFocus());
}
}
public void openWallet(ActionEvent event) {
EventManager.get().post(new RequestWalletOpenEvent());
}
@@ -693,6 +736,9 @@ public class HeadersController extends TransactionFormController implements Init
if(headersForm.getSigningWallet() instanceof FinalizingPSBTWallet) {
//Ensure the script hashes of the UTXOs in FinalizingPSBTWallet are subscribed to
ElectrumServer.TransactionHistoryService historyService = new ElectrumServer.TransactionHistoryService(headersForm.getSigningWallet());
historyService.setOnFailed(workerStateEvent -> {
log.error("Error subscribing FinalizingPSBTWallet script hashes", workerStateEvent.getSource().getException());
});
historyService.start();
}
@@ -702,6 +748,7 @@ public class HeadersController extends TransactionFormController implements Init
});
broadcastTransactionService.setOnFailed(workerStateEvent -> {
broadcastProgressBar.setProgress(0);
log.error("Error broadcasting transaction", workerStateEvent.getSource().getException());
AppController.showErrorDialog("Error broadcasting transaction", workerStateEvent.getSource().getException().getMessage());
broadcastButton.setDisable(false);
});
@@ -745,13 +792,14 @@ public class HeadersController extends TransactionFormController implements Init
locktimeBlock.setDisable(!locktimeEnabled);
locktimeDateType.setDisable(!locktimeEnabled);
locktimeDate.setDisable(!locktimeEnabled);
locktimeCurrentHeight.setDisable(!locktimeEnabled);
}
}
@Subscribe
public void blockTransactionFetched(BlockTransactionFetchedEvent event) {
if(event.getTxId().equals(headersForm.getTransaction().getTxId())) {
if(event.getBlockTransaction() != null) {
if(event.getBlockTransaction() != null && (!Sha256Hash.ZERO_HASH.equals(event.getBlockTransaction().getBlockHash()) || headersForm.getBlockTransaction() == null)) {
updateBlockchainForm(event.getBlockTransaction(), AppController.getCurrentBlockHeight());
}
@@ -831,6 +879,7 @@ public class HeadersController extends TransactionFormController implements Init
locktimeDateType.setDisable(true);
locktimeBlock.setDisable(true);
locktimeDate.setDisable(true);
locktimeCurrentHeight.setVisible(false);
updateTxId();
headersForm.setSigningWallet(event.getSigningWallet());
@@ -891,7 +940,7 @@ public class HeadersController extends TransactionFormController implements Init
public void walletNodeHistoryChanged(WalletNodeHistoryChangedEvent event) {
if(headersForm.getSigningWallet() != null && event.getWalletNode(headersForm.getSigningWallet()) != null) {
Sha256Hash txid = headersForm.getTransaction().getTxId();
ElectrumServer.TransactionReferenceService transactionReferenceService = new ElectrumServer.TransactionReferenceService(Set.of(txid));
ElectrumServer.TransactionReferenceService transactionReferenceService = new ElectrumServer.TransactionReferenceService(Set.of(txid), event.getScriptHash());
transactionReferenceService.setOnSucceeded(successEvent -> {
Map<Sha256Hash, BlockTransaction> transactionMap = transactionReferenceService.getValue();
BlockTransaction blockTransaction = transactionMap.get(txid);
@@ -901,14 +950,47 @@ public class HeadersController extends TransactionFormController implements Init
updateBlockchainForm(blockTransaction, AppController.getCurrentBlockHeight());
}
});
transactionReferenceService.setOnFailed(failEvent -> {
log.error("Could not update block transaction", failEvent.getSource().getException());
});
transactionReferenceService.start();
}
}
@Subscribe
public void walletHistoryChanged(WalletHistoryChangedEvent event) {
//Update tx and input/output reference labels on history changed wallet if this txid matches and label is null
if(headersForm.getSigningWallet() != null && !(headersForm.getSigningWallet() instanceof FinalizingPSBTWallet)) {
Sha256Hash txid = headersForm.getTransaction().getTxId();
BlockTransaction blockTransaction = event.getWallet().getTransactions().get(txid);
if(blockTransaction != null && blockTransaction.getLabel() == null) {
blockTransaction.setLabel(headersForm.getName());
Platform.runLater(() -> EventManager.get().post(new WalletEntryLabelChangedEvent(event.getWallet(), new TransactionEntry(event.getWallet(), blockTransaction, Collections.emptyMap(), Collections.emptyMap()))));
}
for(WalletNode walletNode : event.getHistoryChangedNodes()) {
for(BlockTransactionHashIndex output : walletNode.getTransactionOutputs()) {
if(output.getHash().equals(txid) && output.getLabel() == null) { //If we send to ourselves, usually change
output.setLabel(headersForm.getName() + (walletNode.getKeyPurpose() == KeyPurpose.CHANGE ? " (change)" : " (received)"));
Platform.runLater(() -> EventManager.get().post(new WalletEntryLabelChangedEvent(event.getWallet(), new HashIndexEntry(event.getWallet(), output, HashIndexEntry.Type.OUTPUT, walletNode.getKeyPurpose()))));
}
if(output.getSpentBy() != null && output.getSpentBy().getHash().equals(txid) && output.getSpentBy().getLabel() == null) { //The norm - sending out
output.getSpentBy().setLabel(headersForm.getName() + " (input)");
Platform.runLater(() -> EventManager.get().post(new WalletEntryLabelChangedEvent(event.getWallet(), new HashIndexEntry(event.getWallet(), output.getSpentBy(), HashIndexEntry.Type.INPUT, walletNode.getKeyPurpose()))));
}
}
}
}
}
@Subscribe
public void newBlock(NewBlockEvent event) {
if(headersForm.getBlockTransaction() != null) {
updateBlockchainForm(headersForm.getBlockTransaction(), event.getHeight());
}
if(futureBlockWarning.isVisible()) {
futureBlockWarning.setVisible(AppController.getCurrentBlockHeight() != null && locktimeBlock.getValue() > event.getHeight());
}
}
}
@@ -461,6 +461,11 @@ public class InputController extends TransactionFormController implements Initia
initializeView();
}
@Override
protected TransactionForm getTransactionForm() {
return inputForm;
}
@Override
protected String describeScriptChunk(ScriptChunk chunk) {
String chunkString = super.describeScriptChunk(chunk);
@@ -49,6 +49,11 @@ public class InputsController extends TransactionFormController implements Initi
initialiseView();
}
@Override
protected TransactionForm getTransactionForm() {
return inputsForm;
}
private void initialiseView() {
Transaction tx = inputsForm.getTransaction();
count.setText(Integer.toString(tx.getInputs().size()));
@@ -147,6 +147,11 @@ public class OutputController extends TransactionFormController implements Initi
initializeView();
}
@Override
protected TransactionForm getTransactionForm() {
return outputForm;
}
@Subscribe
public void blockTransactionOutputsFetched(BlockTransactionOutputsFetchedEvent event) {
if(event.getTxId().equals(outputForm.getTransaction().getTxId()) && outputForm.getPsbt() == null && outputForm.getIndex() >= event.getPageStart() && outputForm.getIndex() < event.getPageEnd()) {
@@ -51,6 +51,11 @@ public class OutputsController extends TransactionFormController implements Init
}
}
@Override
protected TransactionForm getTransactionForm() {
return outputsForm;
}
@Subscribe
public void bitcoinUnitChanged(BitcoinUnitChangedEvent event) {
total.refresh(event.getBitcoinUnit());
@@ -13,7 +13,7 @@ public class PageForm extends IndexedTransactionForm {
private final int pageEnd;
public PageForm(TransactionView view, int pageStart, int pageEnd) {
super(new TransactionData(ElectrumServer.UNFETCHABLE_BLOCK_TRANSACTION), pageStart);
super(new TransactionData(pageStart + "-" + pageEnd, ElectrumServer.UNFETCHABLE_BLOCK_TRANSACTION), pageStart);
this.view = view;
this.pageStart = pageStart;
this.pageEnd = pageEnd;
@@ -8,6 +8,7 @@ import com.sparrowwallet.drongo.psbt.PSBTOutput;
import com.sparrowwallet.drongo.wallet.BlockTransaction;
import com.sparrowwallet.sparrow.AppController;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.TransactionTabData;
import com.sparrowwallet.sparrow.control.TransactionHexArea;
import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.net.ElectrumServer;
@@ -318,9 +319,9 @@ public class TransactionController implements Initializable {
Map<Sha256Hash, BlockTransaction> transactionMap = transactionReferenceService.getValue();
BlockTransaction thisBlockTx = null;
Map<Sha256Hash, BlockTransaction> inputTransactions = new HashMap<>();
for (Sha256Hash txid : transactionMap.keySet()) {
for(Sha256Hash txid : transactionMap.keySet()) {
BlockTransaction blockTx = transactionMap.get(txid);
if (txid.equals(getTransaction().getTxId())) {
if(txid.equals(getTransaction().getTxId())) {
thisBlockTx = blockTx;
} else {
inputTransactions.put(txid, blockTx);
@@ -340,7 +341,7 @@ public class TransactionController implements Initializable {
});
});
transactionReferenceService.setOnFailed(failedEvent -> {
failedEvent.getSource().getException().printStackTrace();
log.error("Error fetching transaction or input references", failedEvent.getSource().getException());
});
transactionReferenceService.start();
}
@@ -357,40 +358,28 @@ public class TransactionController implements Initializable {
});
});
transactionOutputsReferenceService.setOnFailed(failedEvent -> {
failedEvent.getSource().getException().printStackTrace();
log.error("Error fetching transaction output references", failedEvent.getSource().getException());
});
transactionOutputsReferenceService.start();
}
}
public void setTransactionData(TransactionData transactionData) {
this.txdata = transactionData;
}
public Transaction getTransaction() {
return txdata.getTransaction();
}
public void setTransaction(Transaction transaction) {
this.txdata = new TransactionData(transaction);
}
public void setName(String name) {
this.txdata.setName(name);
}
public PSBT getPSBT() {
return txdata.getPsbt();
}
public void setPSBT(PSBT psbt) {
this.txdata = new TransactionData(psbt);
}
public BlockTransaction getBlockTransaction() {
return txdata.getBlockTransaction();
}
public void setBlockTransaction(BlockTransaction blockTransaction) {
this.txdata = new TransactionData(blockTransaction);
}
public void setInitialView(TransactionView initialView, Integer initialIndex) {
this.initialView = initialView;
this.initialIndex = initialIndex;
@@ -467,7 +456,9 @@ public class TransactionController implements Initializable {
@Subscribe
public void blockTransactionFetched(BlockTransactionFetchedEvent event) {
if(event.getTxId().equals(getTransaction().getTxId())) {
txdata.setBlockTransaction(event.getBlockTransaction());
if(event.getBlockTransaction() != null && (!Sha256Hash.ZERO_HASH.equals(event.getBlockTransaction().getBlockHash()) || txdata.getBlockTransaction() == null)) {
txdata.setBlockTransaction(event.getBlockTransaction());
}
if(txdata.getInputTransactions() == null) {
txdata.setInputTransactions(event.getInputTransactions());
} else {
@@ -501,4 +492,13 @@ public class TransactionController implements Initializable {
highlightTxHex();
}
}
@Subscribe
public void transactionTabsClosed(TransactionTabsClosedEvent event) {
for(TransactionTabData tabData : event.getClosedTransactionTabData()) {
if(tabData.getTransactionData() == txdata) {
EventManager.get().unregister(this);
}
}
}
}
@@ -32,17 +32,18 @@ public class TransactionData {
private final SimpleObjectProperty<Wallet> signingWallet = new SimpleObjectProperty<>(this, "signingWallet", null);
private final ObservableList<Keystore> signedKeystores = FXCollections.observableArrayList();
public TransactionData(PSBT psbt) {
this.transaction = psbt.getTransaction();
public TransactionData(String name, PSBT psbt) {
this(name, psbt.getTransaction());
this.psbt = psbt;
}
public TransactionData(BlockTransaction blockTransaction) {
this.transaction = blockTransaction.getTransaction();
public TransactionData(String name, BlockTransaction blockTransaction) {
this(name, blockTransaction.getTransaction());
this.blockTransaction = blockTransaction;
}
public TransactionData(Transaction transaction) {
public TransactionData(String name, Transaction transaction) {
this.name = name;
this.transaction = transaction;
}
@@ -58,10 +59,6 @@ public class TransactionData {
return name;
}
public void setName(String name) {
this.name = name;
}
public PSBT getPsbt() {
return psbt;
}
@@ -23,6 +23,10 @@ public abstract class TransactionForm {
this.txdata = txdata;
}
public TransactionData getTransactionData() {
return txdata;
}
public Transaction getTransaction() {
return txdata.getTransaction();
}
@@ -1,9 +1,13 @@
package com.sparrowwallet.sparrow.transaction;
import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.protocol.NonStandardScriptException;
import com.sparrowwallet.drongo.protocol.TransactionOutput;
import com.sparrowwallet.sparrow.BaseController;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.TransactionTabData;
import com.sparrowwallet.sparrow.event.TransactionTabsClosedEvent;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.chart.PieChart;
@@ -18,6 +22,8 @@ import java.util.List;
public abstract class TransactionFormController extends BaseController {
private static final int MAX_PIE_SEGMENTS = 200;
protected abstract TransactionForm getTransactionForm();
protected void addPieData(PieChart pie, List<TransactionOutput> outputs) {
ObservableList<PieChart.Data> outputsPieData = FXCollections.observableArrayList();
@@ -64,6 +70,15 @@ public abstract class TransactionFormController extends BaseController {
});
}
@Subscribe
public void transactionTabsClosed(TransactionTabsClosedEvent event) {
for(TransactionTabData tabData : event.getClosedTransactionTabData()) {
if(tabData.getTransactionData() == getTransactionForm().getTransactionData()) {
EventManager.get().unregister(this);
}
}
}
public static class TransactionReferenceContextMenu extends ContextMenu {
public TransactionReferenceContextMenu(String reference) {
MenuItem referenceItem = new MenuItem("Copy Reference");
@@ -94,7 +94,7 @@ public class HashIndexEntry extends Entry implements Comparable<HashIndexEntry>
}
if(getHashIndex().getHeight() != o.getHashIndex().getHeight()) {
return (o.getHashIndex().getHeight() > 0 ? o.getHashIndex().getHeight() : Integer.MAX_VALUE) - (getHashIndex().getHeight() > 0 ? getHashIndex().getHeight() : Integer.MAX_VALUE);
return o.getHashIndex().getComparisonHeight() - getHashIndex().getComparisonHeight();
}
return (int)o.getHashIndex().getIndex() - (int)getHashIndex().getIndex();
@@ -180,6 +180,7 @@ public class KeystoreController extends WalletFormController implements Initiali
viewSeedButton.setVisible(keystore.getSource() == KeystoreSource.SW_SEED);
importButton.setText(keystore.getSource() == KeystoreSource.SW_WATCH ? "Import..." : "Replace...");
importButton.setTooltip(new Tooltip(keystore.getSource() == KeystoreSource.SW_WATCH ? "Import a keystore from an external source" : "Replace this keystore with another source"));
boolean editable = (keystore.getSource() == KeystoreSource.SW_WATCH);
fingerprint.setEditable(editable);
@@ -101,7 +101,7 @@ public class ReceiveController extends WalletFormController implements Initializ
this.currentEntry = nodeEntry;
address.setText(nodeEntry.getAddress().toString());
label.textProperty().bindBidirectional(nodeEntry.labelProperty());
derivationPath.setText(nodeEntry.getNode().getDerivationPath());
updateDerivationPath(nodeEntry);
updateLastUsed();
@@ -119,6 +119,23 @@ public class ReceiveController extends WalletFormController implements Initializ
updateDisplayAddress(AppController.getDevices());
}
private void updateDerivationPath(NodeEntry nodeEntry) {
KeyDerivation firstDerivation = getWalletForm().getWallet().getKeystores().get(0).getKeyDerivation();
boolean singleDerivationPath = true;
for(Keystore keystore : getWalletForm().getWallet().getKeystores()) {
if(!keystore.getKeyDerivation().getDerivationPath().equals(firstDerivation.getDerivationPath())) {
singleDerivationPath = false;
break;
}
}
if(singleDerivationPath) {
derivationPath.setText(firstDerivation.extend(nodeEntry.getNode().getDerivation()).getDerivationPath());
} else {
derivationPath.setText(nodeEntry.getNode().getDerivationPath().replace("m", "multi"));
}
}
private void updateLastUsed() {
Set<BlockTransactionHashIndex> currentOutputs = currentEntry.getNode().getTransactionOutputs();
if(AppController.isOnline() && currentOutputs.isEmpty()) {
@@ -73,6 +73,9 @@ public class SendController extends WalletFormController implements Initializabl
@FXML
private ComboBox<BitcoinUnit> feeAmountUnit;
@FXML
private FiatLabel fiatFeeAmount;
@FXML
private FeeRatesChart feeRatesChart;
@@ -115,6 +118,12 @@ public class SendController extends WalletFormController implements Initializabl
@Override
public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) {
userFeeSet.set(true);
if(newValue.isEmpty()) {
fiatFeeAmount.setText("");
} else {
setFiatFeeAmount(AppController.getFiatCurrencyExchangeRate(), getFeeValueSats());
}
setTargetBlocks(getTargetBlocks());
updateTransaction();
}
@@ -390,6 +399,7 @@ public class SendController extends WalletFormController implements Initializabl
df.setMaximumFractionDigits(8);
fee.setText(df.format(feeAmountUnit.getValue().getValue(feeValue)));
fee.textProperty().addListener(feeListener);
setFiatFeeAmount(AppController.getFiatCurrencyExchangeRate(), feeValue);
}
private Integer getTargetBlocks() {
@@ -468,6 +478,12 @@ public class SendController extends WalletFormController implements Initializabl
}
}
private void setFiatFeeAmount(CurrencyRate currencyRate, Long amount) {
if(amount != null && currencyRate != null && currencyRate.isAvailable()) {
fiatFeeAmount.set(currencyRate, amount);
}
}
private long getRecipientDustThreshold() {
Address address;
try {
@@ -480,6 +496,25 @@ public class SendController extends WalletFormController implements Initializabl
return address.getScriptType().getDustThreshold(txOutput, getFeeRate());
}
public void scanQrAddress(ActionEvent event) {
QRScanDialog qrScanDialog = new QRScanDialog();
Optional<QRScanDialog.Result> optionalResult = qrScanDialog.showAndWait();
if(optionalResult.isPresent()) {
QRScanDialog.Result result = optionalResult.get();
if(result.uri != null) {
if(result.uri.getAddress() != null) {
address.setText(result.uri.getAddress().toString());
}
if(result.uri.getLabel() != null) {
label.setText(result.uri.getLabel());
}
if(result.uri.getAmount() != null) {
setRecipientValueSats(result.uri.getAmount());
}
}
}
}
public void clear(ActionEvent event) {
address.setText("");
label.setText("");
@@ -488,10 +523,14 @@ public class SendController extends WalletFormController implements Initializabl
amount.setText("");
amount.textProperty().addListener(amountListener);
fiatAmount.setText("");
fee.textProperty().removeListener(feeListener);
fee.setText("");
fee.textProperty().addListener(feeListener);
fiatFeeAmount.setText("");
userFeeSet.set(false);
targetBlocks.setValue(4);
utxoSelectorProperty.setValue(null);
@@ -569,5 +608,6 @@ public class SendController extends WalletFormController implements Initializabl
@Subscribe
public void exchangeRatesUpdated(ExchangeRatesUpdatedEvent event) {
setFiatAmount(event.getCurrencyRate(), getRecipientValueSats());
setFiatFeeAmount(event.getCurrencyRate(), getFeeValueSats());
}
}
@@ -7,8 +7,10 @@ import com.sparrowwallet.drongo.wallet.BlockTransactionHash;
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.WalletTabData;
import com.sparrowwallet.sparrow.event.WalletBlockHeightChangedEvent;
import com.sparrowwallet.sparrow.event.WalletEntryLabelChangedEvent;
import com.sparrowwallet.sparrow.event.WalletTabsClosedEvent;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.IntegerPropertyBase;
import javafx.beans.property.LongProperty;
@@ -204,4 +206,13 @@ public class TransactionEntry extends Entry implements Comparable<TransactionEnt
}
}
}
@Subscribe
public void walletTabsClosed(WalletTabsClosedEvent event) {
for(WalletTabData tabData : event.getClosedWalletTabData()) {
if(tabData.getWalletForm().getWallet() == wallet) {
EventManager.get().unregister(this);
}
}
}
}
@@ -14,7 +14,6 @@ import javafx.collections.ListChangeListener;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.TreeItem;
import javafx.scene.input.MouseEvent;
import java.net.URL;
import java.util.ResourceBundle;
@@ -47,8 +46,10 @@ public class TransactionsController extends WalletFormController implements Init
transactionsTable.initialize(walletTransactionsEntry);
balance.valueProperty().addListener((observable, oldValue, newValue) -> {
setFiatBalance(AppController.getFiatCurrencyExchangeRate(), newValue.longValue());
});
balance.setValue(walletTransactionsEntry.getBalance());
setFiatBalance(AppController.getFiatCurrencyExchangeRate(), walletTransactionsEntry.getBalance());
mempoolBalance.setValue(walletTransactionsEntry.getMempoolBalance());
balanceChart.initialize(walletTransactionsEntry);
@@ -6,6 +6,7 @@ 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.WalletTabData;
import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.net.ElectrumServer;
import com.sparrowwallet.sparrow.io.Storage;
@@ -71,7 +72,7 @@ public class WalletForm {
updateWallet(previousWallet, blockHeight);
});
historyService.setOnFailed(workerStateEvent -> {
workerStateEvent.getSource().getException().printStackTrace();
log.error("Error retrieving wallet history", workerStateEvent.getSource().getException());
});
historyService.start();
}
@@ -199,7 +200,10 @@ public class WalletForm {
@Subscribe
public void newBlock(NewBlockEvent event) {
updateWallet(wallet.copy(), event.getHeight());
//Check if wallet is valid to avoid saving wallets in initial setup
if(wallet.isValid()) {
updateWallet(wallet.copy(), event.getHeight());
}
}
@Subscribe
@@ -213,4 +217,13 @@ public class WalletForm {
refreshHistory(AppController.getCurrentBlockHeight());
}
}
@Subscribe
public void walletTabsClosed(WalletTabsClosedEvent event) {
for(WalletTabData tabData : event.getClosedWalletTabData()) {
if(tabData.getWalletForm() == this) {
EventManager.get().unregister(this);
}
}
}
}
@@ -1,6 +1,10 @@
package com.sparrowwallet.sparrow.wallet;
import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.sparrow.BaseController;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.WalletTabData;
import com.sparrowwallet.sparrow.event.WalletTabsClosedEvent;
public abstract class WalletFormController extends BaseController {
public WalletForm walletForm;
@@ -15,4 +19,15 @@ public abstract class WalletFormController extends BaseController {
}
public abstract void initializeView();
@Subscribe
public void walletTabsClosed(WalletTabsClosedEvent event) {
for(WalletTabData tabData : event.getClosedWalletTabData()) {
if(tabData.getWalletForm() == walletForm) {
EventManager.get().unregister(this);
} else if(walletForm instanceof SettingsWalletForm && tabData.getStorage() == walletForm.getStorage()) {
EventManager.get().unregister(this);
}
}
}
}
BIN
View File
Binary file not shown.
@@ -0,0 +1,15 @@
.title-area {
-fx-background-color: -fx-control-inner-background;
-fx-padding: 10 25 10 25;
-fx-border-width: 0px 0px 1px 0px;
-fx-border-color: #e5e5e6;
}
.title-label {
-fx-font-size: 20px;
}
.content-area, .button-area {
-fx-padding: 10 25 25 25;
}
@@ -0,0 +1,31 @@
<?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 javafx.scene.image.ImageView?>
<?import javafx.scene.image.Image?>
<StackPane prefHeight="420.0" prefWidth="600.0" stylesheets="@about.css" fx:controller="com.sparrowwallet.sparrow.AboutController" xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml">
<VBox spacing="20">
<HBox styleClass="title-area">
<HBox alignment="CENTER_LEFT">
<Label text="Sparrow 0.9.1" styleClass="title-label" />
</HBox>
<Region HBox.hgrow="ALWAYS"/>
<ImageView AnchorPane.rightAnchor="0">
<Image url="/image/sparrow-small.png" requestedWidth="50" requestedHeight="50" smooth="false" />
</ImageView>
</HBox>
<VBox spacing="10" styleClass="content-area">
<Label text="Sparrow is a Bitcoin wallet with the goal of providing greater transparency and usability on the path to full financial self sovereignty. It attempts to provide all of the detail about your wallet setup, transactions and UTXOs so that you can transact will a full understanding of your money." wrapText="true" />
<Label text="Sparrow can operate in both an online and offline mode. In the online mode it connects to your Electrum server to display transaction history. In the offline mode it is useful as a transaction editor and as an airgapped multisig coordinator." wrapText="true" />
<Label text="For privacy and security reasons it is not recommended to use a public Electrum server. Install an Electrum server that connects to your full node to index the blockchain and provide full privacy." wrapText="true" />
</VBox>
<HBox styleClass="button-area" alignment="BOTTOM_RIGHT" VBox.vgrow="SOMETIMES">
<Button text="Done" onAction="#close" />
</HBox>
</VBox>
</StackPane>
@@ -76,7 +76,11 @@
<StatusBar fx:id="statusBar" text="" minHeight="36">
<rightItems>
<UnlabeledToggleSwitch fx:id="serverToggle" />
<UnlabeledToggleSwitch fx:id="serverToggle">
<tooltip>
<Tooltip text="Disconnected" />
</tooltip>
</UnlabeledToggleSwitch>
</rightItems>
</StatusBar>
</children>
@@ -33,5 +33,13 @@
-fx-background-color: transparent;
}
.valid-checksum {
-fx-text-fill: #50a14f;
}
.invalid-checksum {
-fx-text-fill: rgb(202, 18, 67);
}
@@ -63,7 +63,7 @@
<UnlabeledToggleSwitch fx:id="groupByAddress" />
<HelpLabel helpText="Group UTXOs by address when sending to improve privacy by only sending from an address once"/>
</Field>
<Field text="Include mempool change:">
<Field text="Use mempool change:">
<UnlabeledToggleSwitch fx:id="includeMempoolChange" />
<HelpLabel helpText="Allow a wallet to spend UTXOs that are still in the mempool where all their inputs are from that wallet"/>
</Field>
@@ -59,6 +59,11 @@
<Glyph fontFamily="FontAwesome" icon="QUESTION_CIRCLE" prefWidth="13" />
</graphic>
</Button>
<Button fx:id="editConnection" graphicTextGap="5" text="Edit Existing Connection">
<graphic>
<Glyph fontFamily="FontAwesome" icon="EDIT" prefWidth="15" />
</graphic>
</Button>
</StackPane>
<StackPane GridPane.columnIndex="0" GridPane.rowIndex="2">
@@ -20,6 +20,15 @@
.locktime { -fx-fill: #986801 }
#locktimeCurrentHeight {
-fx-padding: 0 0 0 12;
}
.future-warning {
-fx-text-fill: rgb(238, 210, 2);
-fx-padding: 0 0 0 12;
}
.unfinalized-txid {
-fx-text-fill: #a0a1a7;
}
@@ -23,6 +23,7 @@
<?import javafx.scene.control.Hyperlink?>
<?import com.sparrowwallet.sparrow.control.SignaturesProgressBar?>
<?import javafx.scene.control.ProgressBar?>
<?import javafx.scene.control.Tooltip?>
<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>
@@ -80,9 +81,26 @@
</Field>
<Field fx:id="locktimeBlockField" text="Block:">
<Spinner fx:id="locktimeBlock" editable="true" prefWidth="120"/>
<Hyperlink fx:id="locktimeCurrentHeight" text="Set current height" onAction="#setLocktimeToCurrentHeight" />
<Label fx:id="futureBlockWarning">
<graphic>
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="EXCLAMATION_TRIANGLE" styleClass="future-warning" />
</graphic>
<tooltip>
<Tooltip text="Future block specified - transaction will not be mined until this block"/>
</tooltip>
</Label>
</Field>
<Field fx:id="locktimeDateField" text="Date:">
<DateTimePicker fx:id="locktimeDate" prefWidth="180"/>
<Label fx:id="futureDateWarning">
<graphic>
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="EXCLAMATION_TRIANGLE" styleClass="future-warning" />
</graphic>
<tooltip>
<Tooltip text="Future date specified - transaction will not be mined until this date"/>
</tooltip>
</Label>
</Field>
</Fieldset>
</Form>
@@ -21,16 +21,20 @@
<Fieldset inputGrow="SOMETIMES" text="">
<Field text="Type:">
<Label fx:id="type" graphicTextGap="8"/>
<Button fx:id="viewSeedButton" onAction="#showSeed">
<Button fx:id="viewSeedButton" text="View Seed..." graphicTextGap="5" onAction="#showSeed">
<graphic>
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="KEY" />
</graphic>
<tooltip>
<Tooltip text="View Seed..."/>
<Tooltip text="View mnemonic seed words"/>
</tooltip>
</Button>
<Pane HBox.hgrow="ALWAYS" />
<Button fx:id="importButton" text="Import..." onAction="#importKeystore"/>
<Button fx:id="importButton" text="Import..." graphicTextGap="5" onAction="#importKeystore">
<graphic>
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="UNDO" />
</graphic>
</Button>
</Field>
<Field text="Label:">
<TextField fx:id="label" maxWidth="160"/>
@@ -56,8 +56,8 @@
<Separator styleClass="form-separator" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.rowIndex="1" />
<Form GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.rowIndex="2">
<Fieldset inputGrow="SOMETIMES" text="Required Script">
<Field text="ScriptPubKey">
<Fieldset inputGrow="SOMETIMES" text="Required ScriptPubKey">
<Field text="Script:">
<VirtualizedScrollPane>
<content>
<ScriptArea fx:id="scriptPubKeyArea" editable="false" wrapText="true" prefHeight="42" maxHeight="42" styleClass="uneditable-codearea" />
@@ -7,11 +7,18 @@
}
.amount-field {
-fx-max-width: 150px;
-fx-pref-width: 140px;
-fx-min-width: 140px;
-fx-max-width: 140px;
}
.amount-unit {
-fx-min-width: 75px;
-fx-max-width: 75px;
}
#feeRatesChart {
-fx-max-width: 350px;
-fx-max-width: 335px;
-fx-max-height: 130px;
}
@@ -20,6 +20,7 @@
<?import com.sparrowwallet.sparrow.control.TransactionDiagram?>
<?import com.sparrowwallet.drongo.BitcoinUnit?>
<?import com.sparrowwallet.sparrow.control.FiatLabel?>
<?import org.controlsfx.glyphfont.Glyph?>
<BorderPane stylesheets="@send.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.SendController">
<center>
@@ -29,8 +30,8 @@
<Insets left="25.0" right="25.0" top="25.0" />
</padding>
<columnConstraints>
<ColumnConstraints prefWidth="385" />
<ColumnConstraints prefWidth="225" />
<ColumnConstraints prefWidth="410" />
<ColumnConstraints prefWidth="200" />
<ColumnConstraints prefWidth="140" />
</columnConstraints>
<rowConstraints>
@@ -46,7 +47,7 @@
</Field>
<Field text="Amount:">
<TextField fx:id="amount" styleClass="amount-field" />
<ComboBox fx:id="amountUnit">
<ComboBox fx:id="amountUnit" styleClass="amount-unit">
<items>
<FXCollections fx:factory="observableArrayList">
<BitcoinUnit fx:constant="BTC" />
@@ -54,16 +55,25 @@
</FXCollections>
</items>
</ComboBox>
<Region style="-fx-pref-width: 20" />
<Label style="-fx-pref-width: 15" />
<FiatLabel fx:id="fiatAmount" />
<Region style="-fx-pref-width: 20" />
<Button fx:id="maxButton" text="Max" onAction="#setMaxInput" />
</Field>
</Fieldset>
</Form>
<Form GridPane.columnIndex="2" GridPane.rowIndex="0">
<Fieldset inputGrow="SOMETIMES" text="">
<Button text="Scan QR" onAction="#scanQrAddress">
<graphic>
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="CAMERA" />
</graphic>
</Button>
</Fieldset>
</Form>
<Form GridPane.columnIndex="0" GridPane.rowIndex="1">
<Fieldset inputGrow="SOMETIMES" text="Fee">
<Field text="Block Target:">
<Field text="Block target">
<Slider fx:id="targetBlocks" snapToTicks="true" showTickLabels="true" showTickMarks="true" />
</Field>
<Field fx:id="feeRateField" text="Rate:">
@@ -71,7 +81,7 @@
</Field>
<Field text="Fee:">
<TextField fx:id="fee" styleClass="amount-field"/>
<ComboBox fx:id="feeAmountUnit">
<ComboBox fx:id="feeAmountUnit" styleClass="amount-unit">
<items>
<FXCollections fx:factory="observableArrayList">
<BitcoinUnit fx:constant="BTC" />
@@ -79,6 +89,8 @@
</FXCollections>
</items>
</ComboBox>
<Label style="-fx-pref-width: 15" />
<FiatLabel fx:id="fiatFeeAmount" />
</Field>
</Fieldset>
</Form>
@@ -106,7 +118,11 @@
<HBox AnchorPane.rightAnchor="10">
<Button fx:id="clearButton" text="Clear" cancelButton="true" onAction="#clear" />
<Region HBox.hgrow="ALWAYS" style="-fx-min-width: 20px" />
<Button fx:id="createButton" text="Create Transaction" defaultButton="true" disable="true" onAction="#createTransaction" />
<Button fx:id="createButton" text="Create Transaction" defaultButton="true" disable="true" contentDisplay="RIGHT" graphicTextGap="5" onAction="#createTransaction">
<graphic>
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="ANGLE_DOUBLE_RIGHT" />
</graphic>
</Button>
</HBox>
</AnchorPane>
</bottom>
Binary file not shown.
+4
View File
@@ -1,6 +1,10 @@
<configuration>
<statusListener class="ch.qos.logback.core.status.NopStatusListener" />
<logger name="com.github.sarxos.webcam.Webcam" level="OFF"/>
<logger name="com.github.sarxos.webcam.ds.cgt.WebcamOpenTask" level="OFF"/>
<logger name="com.github.sarxos.webcam.ds.cgt.WebcamCloseTask" level="OFF"/>
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>${user.home}/.sparrow/sparrow.log</file>
<encoder>