mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2026-07-31 03:56:16 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa082e892f | ||
|
|
c814b8e350 | ||
|
|
2b48b4cd6e | ||
|
|
d49f85fe5b | ||
|
|
af525797ff | ||
|
|
e9e10de266 | ||
|
|
ab5fdef919 | ||
|
|
f723d20c9a | ||
|
|
03c488cf4d | ||
|
|
ca64a1d307 | ||
|
|
4c8f7bb711 | ||
|
|
77dd8ca5d7 | ||
|
|
d5aba35184 | ||
|
|
30e2ab5e2a | ||
|
|
e93fcf7fbd | ||
|
|
198672a06e | ||
|
|
133d771c09 | ||
|
|
9a80065605 | ||
|
|
ccf4e98968 | ||
|
|
df15129f64 | ||
|
|
e878648587 | ||
|
|
75bf63d936 | ||
|
|
304bdd5c48 | ||
|
|
e1934c99bc |
+2
-2
@@ -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
Vendored
BIN
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);
|
||||
|
||||
+42
-23
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
BIN
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>
|
||||
|
||||
+8
@@ -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.
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user