Compare commits

...
30 Commits
Author SHA1 Message Date
Craig Raw ebd629db3a v1.5.1 2021-10-07 13:11:28 +02:00
Craig Raw c18a2f4388 improve tor identity management 2021-10-07 12:23:28 +02:00
Craig Raw 6f95dbe309 support bitcoin core connections over tor 2021-10-06 20:37:58 +02:00
Craig Raw 576253e651 fix remaining issue when utxo date is null 2021-10-06 10:08:56 +02:00
Craig Raw b9d6cb17d4 indicate when entered seed is of unsupported electrum type 2021-10-06 08:23:08 +02:00
Craig Raw 3b730a1711 preserve check menu item state across windows 2021-10-05 16:38:56 +02:00
Craig Raw 8c0a1932cf add prevent computer sleep functionality 2021-10-05 16:16:32 +02:00
Craig Raw 35b57f9d69 add txid to transactions csv download 2021-10-05 10:55:20 +02:00
Craig Raw d6ad7f4808 handle null dates in date cell 2021-10-05 10:39:11 +02:00
Craig Raw 3e47a49f49 core connection: default to cookie authentication when user/pass are not filled in 2021-10-05 10:08:51 +02:00
Craig Raw 4c817d243d fix long specter desktop keystore labels 2021-10-04 14:45:02 +02:00
Craig Raw 22b7b659f3 include amount in transaction pie chart tooltip 2021-10-04 14:44:19 +02:00
Craig Raw 9dd6068e69 make temp backup permanent when created in a previous process 2021-10-04 13:49:41 +02:00
craigrawandGitHub 54baee57e1 Merge pull request #235 from romanz/master
Allow running `sparrow` script from other paths
2021-10-04 12:54:48 +02:00
Roman Zeyde 581aaf288e Allow running sparrow script from other paths
IIUC, it currently assumes that it's run locally using `./sparrow`.
2021-10-04 13:44:42 +03:00
Craig Raw 2fa47e640d various whirlpool-related improvements 2021-10-04 12:16:03 +02:00
Craig Raw ea03dece72 add lock wallet functionality 2021-10-01 15:47:01 +02:00
Craig Raw 8e0b9a3ea0 ask for passphrase re-entry when creating a bip39 wallet 2021-09-30 12:49:03 +02:00
Craig Raw 67179127e3 minor whirlpool related fixes 2021-09-30 10:52:40 +02:00
Craig Raw 4ebee8a8f3 dont allow double click to receive on whirlpool wallet 2021-09-29 10:41:16 +02:00
Craig Raw 2548e77d90 followup to disable adding accounts to p2sh legacy multisig 2021-09-29 10:32:43 +02:00
Craig Raw 58e3b9dcdd add multiple account functionality 2021-09-29 10:11:51 +02:00
Craig Raw 429b733140 prevent setting separate passwords on child wallets 2021-09-27 13:24:29 +02:00
Craig Raw 56e3a54ae0 clarify and improve wallet password and keystore passphrase entry 2021-09-27 12:49:46 +02:00
Craig Raw a934ffa76c fix issues when removing selected items from utxotreetable 2021-09-27 11:46:50 +02:00
Craig Raw 86a49e0d9a fix npe showing the mix to dialog when non-standard wallets are loaded 2021-09-27 10:08:48 +02:00
Craig Raw 761e9c9b7e use wallet unit when displaying pool denomination 2021-09-27 09:33:41 +02:00
Craig Raw 712241873f show reason for mix error in tooltip 2021-09-27 08:54:42 +02:00
Craig Raw 31f9cca33a handle electrs batching detection better 2021-09-27 08:15:20 +02:00
Craig Raw 395e90e2a5 undo #181: always create bitcoin core wallet if missing 2021-09-27 08:14:35 +02:00
70 changed files with 1126 additions and 306 deletions
+3 -3
View File
@@ -5,7 +5,7 @@ plugins {
id 'org.beryx.jlink' version '2.24.0'
}
def sparrowVersion = '1.5.0'
def sparrowVersion = '1.5.1'
def os = org.gradle.internal.os.OperatingSystem.current()
def osName = os.getFamilyName()
if(os.macOsX) {
@@ -91,7 +91,7 @@ dependencies {
implementation('org.slf4j:jul-to-slf4j:1.7.30') {
exclude group: 'org.slf4j'
}
implementation('com.sparrowwallet.nightjar:nightjar:0.2.17-SNAPSHOT')
implementation('com.sparrowwallet.nightjar:nightjar:0.2.18-SNAPSHOT')
testImplementation('junit:junit:4.12')
}
@@ -449,7 +449,7 @@ extraJavaModuleInfo {
module('cbor-0.9.jar', 'co.nstant.in.cbor', '0.9') {
exports('co.nstant.in.cbor')
}
module('nightjar-0.2.17-SNAPSHOT.jar', 'com.sparrowwallet.nightjar', '0.2.17-SNAPSHOT') {
module('nightjar-0.2.18-SNAPSHOT.jar', 'com.sparrowwallet.nightjar', '0.2.18-SNAPSHOT') {
requires('com.google.common')
requires('net.sourceforge.streamsupport')
requires('org.slf4j')
+1 -1
View File
@@ -51,7 +51,7 @@ sudo apt install -y rpm fakeroot binutils
The project can cloned for a specific release tag as follows:
```shell
GIT_TAG="1.5.0"
GIT_TAG="1.5.1"
git clone --recursive --branch "${GIT_TAG}" git@github.com:sparrowwallet/sparrow.git
```
+1 -1
Submodule drongo updated: c9e57fad01...025af05758
+1
View File
@@ -1,5 +1,6 @@
#!/usr/bin/env sh
cd `dirname $0`
args="$*"
args="${args%"${args##*[![:space:]]}"}"
+1 -1
View File
@@ -21,7 +21,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.5.0</string>
<string>1.5.1</string>
<key>CFBundleSignature</key>
<string>????</string>
<!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories -->
@@ -35,6 +35,8 @@ import de.codecentric.centerdevice.MenuToolkit;
import javafx.animation.*;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.WeakChangeListener;
import javafx.collections.ListChangeListener;
@@ -118,31 +120,44 @@ public class AppController implements Initializable {
@FXML
private CheckMenuItem openWalletsInNewWindows;
private static final BooleanProperty openWalletsInNewWindowsProperty = new SimpleBooleanProperty();
@FXML
private CheckMenuItem hideEmptyUsedAddresses;
private static final BooleanProperty hideEmptyUsedAddressesProperty = new SimpleBooleanProperty();
@FXML
private CheckMenuItem useHdCameraResolution;
private static final BooleanProperty useHdCameraResolutionProperty = new SimpleBooleanProperty();
@FXML
private CheckMenuItem showLoadingLog;
private static final BooleanProperty showLoadingLogProperty = new SimpleBooleanProperty();
@FXML
private CheckMenuItem showUtxosChart;
private static final BooleanProperty showUtxosChartProperty = new SimpleBooleanProperty();
@FXML
private CheckMenuItem showTxHex;
private static final BooleanProperty showTxHexProperty = new SimpleBooleanProperty();
@FXML
private MenuItem minimizeToTray;
@FXML
private MenuItem lockWallet;
@FXML
private MenuItem refreshWallet;
@FXML
private MenuItem sendToMany;
@FXML
private CheckMenuItem preventSleep;
private static final BooleanProperty preventSleepProperty = new SimpleBooleanProperty();
@FXML
private StackPane rootStack;
@@ -273,16 +288,26 @@ public class AppController implements Initializable {
selectedThemeToggle.ifPresent(toggle -> theme.selectToggle(toggle));
setTheme(null);
openWalletsInNewWindows.setSelected(Config.get().isOpenWalletsInNewWindows());
hideEmptyUsedAddresses.setSelected(Config.get().isHideEmptyUsedAddresses());
useHdCameraResolution.setSelected(Config.get().isHdCapture());
showTxHex.setSelected(Config.get().isShowTransactionHex());
showLoadingLog.setSelected(Config.get().isShowLoadingLog());
showUtxosChart.setSelected(Config.get().isShowUtxosChart());
openWalletsInNewWindowsProperty.set(Config.get().isOpenWalletsInNewWindows());
openWalletsInNewWindows.selectedProperty().bindBidirectional(openWalletsInNewWindowsProperty);
hideEmptyUsedAddressesProperty.set(Config.get().isHideEmptyUsedAddresses());
hideEmptyUsedAddresses.selectedProperty().bindBidirectional(hideEmptyUsedAddressesProperty);
useHdCameraResolutionProperty.set(Config.get().isHdCapture());
useHdCameraResolution.selectedProperty().bindBidirectional(useHdCameraResolutionProperty);
showTxHexProperty.set(Config.get().isShowTransactionHex());
showTxHex.selectedProperty().bindBidirectional(showTxHexProperty);
showLoadingLogProperty.set(Config.get().isShowLoadingLog());
showLoadingLog.selectedProperty().bindBidirectional(showLoadingLogProperty);
showUtxosChartProperty.set(Config.get().isShowUtxosChart());
showUtxosChart.selectedProperty().bindBidirectional(showUtxosChartProperty);
preventSleepProperty.set(Config.get().isPreventSleep());
preventSleep.selectedProperty().bindBidirectional(preventSleepProperty);
saveTransaction.setDisable(true);
savePSBT.visibleProperty().bind(saveTransaction.visibleProperty().not());
savePSBTBinary.disableProperty().bind(saveTransaction.visibleProperty());
exportWallet.setDisable(true);
lockWallet.setDisable(true);
refreshWallet.disableProperty().bind(Bindings.or(exportWallet.disableProperty(), Bindings.or(serverToggle.disableProperty(), AppServices.onlineProperty().not())));
sendToMany.disableProperty().bind(exportWallet.disableProperty());
@@ -738,6 +763,12 @@ public class AppController implements Initializable {
EventManager.get().post(new BitcoinUnitChangedEvent(unit));
}
public void preventSleep(ActionEvent event) {
CheckMenuItem item = (CheckMenuItem)event.getSource();
Config.get().setPreventSleep(item.isSelected());
AppServices.get().setPreventSleep(item.isSelected());
}
public void openFile(File file) {
if(isWalletFile(file)) {
openWalletFile(file, true);
@@ -908,6 +939,11 @@ public class AppController implements Initializable {
whirlpool.setHDWallet(storage.getWalletId(wallet), copy);
}
StandardAccount standardAccount = wallet.getStandardAccountType();
if(standardAccount != null && standardAccount.getMinimumGapLimit() != null && wallet.gapLimit() == null) {
wallet.setGapLimit(standardAccount.getMinimumGapLimit());
}
for(int i = 0; i < wallet.getKeystores().size(); i++) {
Keystore keystore = wallet.getKeystores().get(i);
if(keystore.hasSeed()) {
@@ -1140,6 +1176,13 @@ public class AppController implements Initializable {
AppServices.get().minimizeStage((Stage)tabs.getScene().getWindow());
}
public void lockWallet(ActionEvent event) {
WalletForm selectedWalletForm = getSelectedWalletForm();
if(selectedWalletForm != null) {
EventManager.get().post(new WalletLockEvent(selectedWalletForm.getMasterWallet()));
}
}
public void refreshWallet(ActionEvent event) {
WalletForm selectedWalletForm = getSelectedWalletForm();
if(selectedWalletForm != null) {
@@ -1189,7 +1232,6 @@ public class AppController implements Initializable {
tabLabel.setGraphic(glyph);
tabLabel.setGraphicTextGap(5.0);
tab.setGraphic(tabLabel);
tab.setContextMenu(getTabContextMenu(tab));
tab.setClosable(true);
tab.setOnCloseRequest(event -> {
if(AppServices.getWhirlpoolServices().getWhirlpoolForMixToWallet(((WalletTabData)tab.getUserData()).getWalletForm().getWalletId()) != null) {
@@ -1202,13 +1244,17 @@ public class AppController implements Initializable {
TabPane subTabs = new TabPane();
subTabs.setSide(Side.RIGHT);
subTabs.getStyleClass().add("master-only");
setSubTabsVisible(subTabs, false);
subTabs.rotateGraphicProperty().set(true);
tab.setContent(subTabs);
WalletForm walletForm = addWalletSubTab(subTabs, storage, wallet, backupWallet);
TabData tabData = new WalletTabData(TabData.TabType.WALLET, walletForm);
tab.setUserData(tabData);
tab.setContextMenu(getTabContextMenu(tab));
walletForm.lockedProperty().addListener((observable, oldValue, newValue) -> {
setSubTabsVisible(subTabs, !newValue && subTabs.getTabs().size() > 1);
});
subTabs.getSelectionModel().selectedItemProperty().addListener((observable, old_val, selectedTab) -> {
if(selectedTab != null) {
@@ -1232,12 +1278,11 @@ public class AppController implements Initializable {
if(walletTabData.getWallet() == wallet.getMasterWallet()) {
TabPane subTabs = (TabPane)walletTab.getContent();
addWalletSubTab(subTabs, storage, wallet, backupWallet);
Tab masterTab = subTabs.getTabs().get(0);
Tab masterTab = subTabs.getTabs().stream().filter(tab -> ((WalletTabData)tab.getUserData()).getWallet().isMasterWallet()).findFirst().orElse(subTabs.getTabs().get(0));
Label masterLabel = (Label)masterTab.getGraphic();
masterLabel.setText(getAutomaticName(wallet.getMasterWallet()));
Platform.runLater(() -> {
subTabs.getStyleClass().remove("master-only");
subTabs.getStyleClass().add("wallet-subtabs");
setSubTabsVisible(subTabs, true);
});
}
}
@@ -1247,6 +1292,20 @@ public class AppController implements Initializable {
EventManager.get().post(new WalletOpenedEvent(storage, wallet));
}
private void setSubTabsVisible(TabPane subTabs, boolean visible) {
if(visible) {
subTabs.getStyleClass().remove("master-only");
if(!subTabs.getStyleClass().contains("wallet-subtabs")) {
subTabs.getStyleClass().add("wallet-subtabs");
}
} else {
if(!subTabs.getStyleClass().contains("master-only")) {
subTabs.getStyleClass().add("master-only");
}
subTabs.getStyleClass().remove("wallet-subtabs");
}
}
public WalletForm addWalletSubTab(TabPane subTabs, Storage storage, Wallet wallet, Wallet backupWallet) {
try {
Tab subTab = new Tab();
@@ -1271,6 +1330,11 @@ public class AppController implements Initializable {
subTab.setUserData(tabData);
subTabs.getTabs().add(subTab);
subTabs.getTabs().sort((o1, o2) -> {
WalletTabData tabData1 = (WalletTabData) o1.getUserData();
WalletTabData tabData2 = (WalletTabData) o2.getUserData();
return tabData1.getWallet().compareTo(tabData2.getWallet());
});
subTabs.getSelectionModel().select(subTab);
return walletForm;
@@ -1298,7 +1362,7 @@ public class AppController implements Initializable {
private String getAutomaticName(Wallet wallet) {
int account = wallet.getAccountIndex();
return account < 0 ? wallet.getName() : (!wallet.isWhirlpoolMasterWallet() || account > 1 ? "Account #" + account : "Deposit");
return (account < 0 || account > 9) ? wallet.getName() : (!wallet.isWhirlpoolMasterWallet() || account > 1 ? "Account #" + account : "Deposit");
}
public WalletForm getSelectedWalletForm() {
@@ -1480,6 +1544,18 @@ public class AppController implements Initializable {
private ContextMenu getTabContextMenu(Tab tab) {
ContextMenu contextMenu = new ContextMenu();
if(tab.getUserData() instanceof WalletTabData walletTabData) {
MenuItem lock = new MenuItem("Lock");
Glyph lockGlyph = new Glyph("FontAwesome", FontAwesome.Glyph.LOCK);
lockGlyph.setFontSize(12);
lock.setGraphic(lockGlyph);
lock.disableProperty().bind(walletTabData.getWalletForm().lockedProperty());
lock.setOnAction(event -> {
EventManager.get().post(new WalletLockEvent(walletTabData.getWallet()));
});
contextMenu.getItems().addAll(lock);
}
MenuItem close = new MenuItem("Close");
close.setOnAction(event -> {
tabs.getTabs().remove(tab);
@@ -1665,6 +1741,7 @@ public class AppController implements Initializable {
} else {
saveTransaction.setVisible(false);
}
lockWallet.setDisable(true);
exportWallet.setDisable(true);
showLoadingLog.setDisable(true);
showUtxosChart.setDisable(true);
@@ -1674,6 +1751,7 @@ public class AppController implements Initializable {
WalletTabData walletTabData = walletTabEvent.getWalletTabData();
saveTransaction.setVisible(true);
saveTransaction.setDisable(true);
lockWallet.setDisable(walletTabData.getWalletForm().lockedProperty().get());
exportWallet.setDisable(walletTabData.getWallet() == null || !walletTabData.getWallet().isValid());
showLoadingLog.setDisable(false);
showUtxosChart.setDisable(false);
@@ -1709,6 +1787,20 @@ public class AppController implements Initializable {
@Subscribe
public void newWalletTransactions(NewWalletTransactionsEvent event) {
if(Config.get().isNotifyNewTransactions() && getOpenWallets().containsKey(event.getWallet())) {
for(Tab tab : tabs.getTabs()) {
if(tab.getUserData() instanceof WalletTabData) {
TabPane subTabs = (TabPane)tab.getContent();
for(Tab subTab : subTabs.getTabs()) {
TabData tabData = (TabData)subTab.getUserData();
if(tabData instanceof WalletTabData walletTabData) {
if(walletTabData.getWallet().equals(event.getWallet()) && walletTabData.getWalletForm().lockedProperty().get()) {
return;
}
}
}
}
}
List<BlockTransaction> blockTransactions = new ArrayList<>(event.getBlockTransactions());
List<BlockTransaction> whirlpoolTransactions = event.getWhirlpoolMixTransactions();
blockTransactions.removeAll(whirlpoolTransactions);
@@ -2167,4 +2259,20 @@ public class AppController implements Initializable {
addWalletTab(storage, event.getChildWallet(), null);
}
@Subscribe
public void walletLock(WalletLockEvent event) {
WalletForm selectedWalletForm = getSelectedWalletForm();
if(selectedWalletForm != null && selectedWalletForm.getMasterWallet().equals(event.getWallet())) {
lockWallet.setDisable(true);
}
}
@Subscribe
public void walletUnlock(WalletUnlockEvent event) {
WalletForm selectedWalletForm = getSelectedWalletForm();
if(selectedWalletForm != null && selectedWalletForm.getMasterWallet().equals(event.getWallet())) {
lockWallet.setDisable(false);
}
}
}
@@ -2,7 +2,6 @@ package com.sparrowwallet.sparrow;
import com.google.common.eventbus.Subscribe;
import com.google.common.net.HostAndPort;
import com.samourai.whirlpool.client.wallet.WhirlpoolEventService;
import com.sparrowwallet.drongo.Network;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.protocol.BlockHeader;
@@ -24,6 +23,7 @@ import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.ScheduledService;
import javafx.concurrent.Task;
import javafx.concurrent.Worker;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
@@ -48,6 +48,7 @@ import org.slf4j.LoggerFactory;
import java.awt.*;
import java.awt.desktop.OpenFilesHandler;
import java.awt.desktop.OpenURIHandler;
import java.awt.event.KeyEvent;
import java.io.File;
import java.io.IOException;
import java.net.*;
@@ -94,6 +95,8 @@ public class AppServices {
private TorService torService;
private ScheduledService<Void> preventSleepService;
private static Integer currentBlockHeight;
private static BlockHeader latestBlockHeader;
@@ -156,6 +159,7 @@ public class AppServices {
ratesService = createRatesService(config.getExchangeSource(), config.getFiatCurrency());
versionCheckService = createVersionCheckService();
torService = createTorService();
preventSleepService = createPreventSleepService();
onlineProperty.addListener(onlineServicesListener);
@@ -183,6 +187,10 @@ public class AppServices {
if(config.isCheckNewVersions() && Network.get() == Network.MAINNET) {
restartService(versionCheckService);
}
if(config.isPreventSleep()) {
restartService(preventSleepService);
}
}
private void restartService(ScheduledService<?> service) {
@@ -382,6 +390,39 @@ public class AppServices {
return torService;
}
private ScheduledService<Void> createPreventSleepService() {
ScheduledService<Void> preventSleepService = new ScheduledService<Void>() {
@Override
protected Task<Void> createTask() {
return new Task<>() {
protected Void call() {
try {
Robot robot = new Robot();
robot.keyRelease(KeyEvent.VK_F16);
} catch(Exception e) {
log.debug("Error preventing sleep", e);
}
return null;
}
};
}
};
preventSleepService.setPeriod(Duration.minutes(1));
return preventSleepService;
}
public void setPreventSleep(boolean preventSleep) {
if(preventSleepService != null) {
if(preventSleep) {
restartService(preventSleepService);
} else {
preventSleepService.cancel();
}
}
}
public static boolean isTorRunning() {
return Tor.getDefault() != null;
}
@@ -31,7 +31,7 @@ import java.util.stream.Collectors;
public class MainApp extends Application {
public static final String APP_ID = "com.sparrowwallet.sparrow";
public static final String APP_NAME = "Sparrow";
public static final String APP_VERSION = "1.5.0";
public static final String APP_VERSION = "1.5.1";
public static final String APP_VERSION_SUFFIX = "";
public static final String APP_HOME_PROPERTY = "sparrow.home";
public static final String NETWORK_ENV_PROPERTY = "SPARROW_NETWORK";
@@ -70,7 +70,6 @@ public class MainApp extends Application {
if(optionalMode.isPresent()) {
mode = optionalMode.get();
Config.get().setMode(mode);
Config.get().setCoreWallet(Bwt.DEFAULT_CORE_WALLET);
if(mode.equals(Mode.ONLINE)) {
PreferencesDialog preferencesDialog = new PreferencesDialog(PreferenceGroup.SERVER, true);
@@ -85,9 +84,6 @@ public class MainApp extends Application {
if(Config.get().getServerType() == null && Config.get().getCoreServer() == null && Config.get().getElectrumServer() != null) {
Config.get().setServerType(ServerType.ELECTRUM_SERVER);
} else if(Config.get().getServerType() == ServerType.BITCOIN_CORE && Config.get().getCoreWallet() == null) {
Config.get().setCoreMultiWallet(Boolean.TRUE);
Config.get().setCoreWallet("");
}
if(Config.get().getHdCapture() == null && Platform.getCurrent() == Platform.OSX) {
@@ -2,11 +2,16 @@ package com.sparrowwallet.sparrow;
import com.sparrowwallet.drongo.LogHandler;
import com.sparrowwallet.sparrow.event.TorStatusEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.event.Level;
public class TorLogHandler implements LogHandler {
private static final Logger log = LoggerFactory.getLogger(TorLogHandler.class);
@Override
public void handleLog(String threadName, Level level, String message, String loggerName, long timestamp, StackTraceElement[] callerData) {
log.debug(message);
EventManager.get().post(new TorStatusEvent(message));
}
}
@@ -0,0 +1,91 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.wallet.StandardAccount;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolServices;
import javafx.collections.FXCollections;
import javafx.scene.control.*;
import javafx.scene.layout.VBox;
import javafx.util.StringConverter;
import org.controlsfx.glyphfont.Glyph;
import java.util.ArrayList;
import java.util.List;
public class AddAccountDialog extends Dialog<StandardAccount> {
private final ComboBox<StandardAccount> standardAccountCombo;
public AddAccountDialog(Wallet wallet) {
final DialogPane dialogPane = getDialogPane();
setTitle("Add Account");
dialogPane.setHeaderText("Choose an account to add:");
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
AppServices.setStageIcon(dialogPane.getScene().getWindow());
dialogPane.getButtonTypes().addAll(ButtonType.CANCEL, ButtonType.OK);
dialogPane.setPrefWidth(380);
dialogPane.setPrefHeight(200);
AppServices.moveToActiveWindowScreen(this);
Glyph key = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.SORT_NUMERIC_DOWN);
key.setFontSize(50);
dialogPane.setGraphic(key);
final VBox content = new VBox(10);
content.setPrefHeight(50);
standardAccountCombo = new ComboBox<>();
standardAccountCombo.setMaxWidth(Double.MAX_VALUE);
List<Integer> existingIndexes = new ArrayList<>();
Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet();
existingIndexes.add(masterWallet.getAccountIndex());
for(Wallet childWallet : masterWallet.getChildWallets()) {
existingIndexes.add(childWallet.getAccountIndex());
}
List<StandardAccount> availableAccounts = new ArrayList<>();
for(StandardAccount standardAccount : StandardAccount.values()) {
if(!existingIndexes.contains(standardAccount.getAccountNumber()) && !StandardAccount.WHIRLPOOL_ACCOUNTS.contains(standardAccount)) {
availableAccounts.add(standardAccount);
}
}
if(WhirlpoolServices.canWalletMix(masterWallet) && !masterWallet.isWhirlpoolMasterWallet()) {
availableAccounts.add(StandardAccount.WHIRLPOOL_PREMIX);
}
standardAccountCombo.setItems(FXCollections.observableList(availableAccounts));
standardAccountCombo.setConverter(new StringConverter<>() {
@Override
public String toString(StandardAccount account) {
if(account == null) {
return "None Available";
}
if(StandardAccount.WHIRLPOOL_ACCOUNTS.contains(account)) {
return "Whirlpool Accounts";
}
return account.getName();
}
@Override
public StandardAccount fromString(String string) {
return null;
}
});
if(standardAccountCombo.getItems().isEmpty()) {
Button okButton = (Button) dialogPane.lookupButton(ButtonType.OK);
okButton.setDisable(true);
} else {
standardAccountCombo.getSelectionModel().select(0);
}
content.getChildren().add(standardAccountCombo);
dialogPane.setContent(content);
setResultConverter(dialogButton -> dialogButton == ButtonType.OK ? standardAccountCombo.getValue() : null);
}
}
@@ -68,21 +68,23 @@ public class AddressTreeTable extends CoinTreeTable {
}
}
setOnMouseClicked(mouseEvent -> {
if(mouseEvent.getButton().equals(MouseButton.PRIMARY)){
if(mouseEvent.getClickCount() == 2) {
TreeItem<Entry> treeItem = getSelectionModel().getSelectedItem();
if(treeItem != null && treeItem.getChildren().isEmpty()) {
Entry entry = getSelectionModel().getSelectedItem().getValue();
if(entry instanceof NodeEntry) {
NodeEntry nodeEntry = (NodeEntry)entry;
EventManager.get().post(new ReceiveActionEvent(nodeEntry));
Platform.runLater(() -> EventManager.get().post(new ReceiveToEvent(nodeEntry)));
if(!rootEntry.getWallet().isWhirlpoolChildWallet()) {
setOnMouseClicked(mouseEvent -> {
if(mouseEvent.getButton().equals(MouseButton.PRIMARY)){
if(mouseEvent.getClickCount() == 2) {
TreeItem<Entry> treeItem = getSelectionModel().getSelectedItem();
if(treeItem != null && treeItem.getChildren().isEmpty()) {
Entry entry = getSelectionModel().getSelectedItem().getValue();
if(entry instanceof NodeEntry) {
NodeEntry nodeEntry = (NodeEntry)entry;
EventManager.get().post(new ReceiveActionEvent(nodeEntry));
Platform.runLater(() -> EventManager.get().post(new ReceiveToEvent(nodeEntry)));
}
}
}
}
}
});
});
}
rootEntry.getChildren().addListener((ListChangeListener<Entry>) c -> {
this.refresh();
@@ -35,10 +35,14 @@ public class DateCell extends TreeTableCell<Entry, Entry> {
UtxoEntry utxoEntry = (UtxoEntry)entry;
if(utxoEntry.getHashIndex().getHeight() <= 0) {
setText("Unconfirmed " + (utxoEntry.getHashIndex().getHeight() < 0 ? "Parent " : "") + (utxoEntry.getWallet().isWhirlpoolMixWallet() ? "(Not yet mixable)" : (utxoEntry.isSpendable() ? "(Spendable)" : "(Not yet spendable)")));
} else {
setContextMenu(null);
} else if(utxoEntry.getHashIndex().getDate() != null) {
String date = DATE_FORMAT.format(utxoEntry.getHashIndex().getDate());
setText(date);
setContextMenu(new DateContextMenu(date, utxoEntry.getHashIndex()));
} else {
setText("Unknown");
setContextMenu(null);
}
Tooltip tooltip = new Tooltip();
@@ -17,6 +17,10 @@ public class DateLabel extends CopyableLabel {
}
public static String getShortDateFormat(Date date) {
if(date == null) {
return "Unknown";
}
Date now = new Date();
long elapsed = (now.getTime() - date.getTime()) / 1000;
@@ -52,7 +52,7 @@ public class DevicePane extends TitledDescriptionPane {
private Button unlockButton;
private Button enterPinButton;
private Button setPassphraseButton;
private SplitMenuButton importButton;
private ButtonBase importButton;
private Button signButton;
private Button displayAddressButton;
private Button signMessageButton;
@@ -61,13 +61,13 @@ public class DevicePane extends TitledDescriptionPane {
private boolean defaultDevice;
public DevicePane(Wallet wallet, Device device, boolean defaultDevice) {
public DevicePane(Wallet wallet, Device device, boolean defaultDevice, KeyDerivation requiredDerivation) {
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
this.deviceOperation = DeviceOperation.IMPORT;
this.wallet = wallet;
this.psbt = null;
this.outputDescriptor = null;
this.keyDerivation = null;
this.keyDerivation = requiredDerivation;
this.message = null;
this.device = device;
this.defaultDevice = defaultDevice;
@@ -199,35 +199,39 @@ public class DevicePane extends TitledDescriptionPane {
}
private void createImportButton() {
importButton = new SplitMenuButton();
importButton = keyDerivation == null ? new SplitMenuButton() : new Button();
importButton.setAlignment(Pos.CENTER_RIGHT);
importButton.setText("Import Keystore");
importButton.setOnAction(event -> {
importButton.setDisable(true);
importKeystore(wallet.getScriptType() == null ? ScriptType.P2WPKH.getDefaultDerivation() : wallet.getScriptType().getDefaultDerivation());
List<ChildNumber> defaultDerivation = wallet.getScriptType() == null ? ScriptType.P2WPKH.getDefaultDerivation() : wallet.getScriptType().getDefaultDerivation();
importKeystore(keyDerivation == null ? defaultDerivation : keyDerivation.getDerivation());
});
if(wallet.getScriptType() == null) {
ScriptType[] scriptTypes = new ScriptType[] {ScriptType.P2WPKH, ScriptType.P2SH_P2WPKH, ScriptType.P2PKH};
for(ScriptType scriptType : scriptTypes) {
MenuItem item = new MenuItem(scriptType.getDescription());
final List<ChildNumber> derivation = scriptType.getDefaultDerivation();
item.setOnAction(event -> {
importButton.setDisable(true);
importKeystore(derivation);
});
importButton.getItems().add(item);
}
} else {
String[] accounts = new String[] {"Default Account #0", "Account #1", "Account #2", "Account #3", "Account #4", "Account #5", "Account #6", "Account #7", "Account #8", "Account #9"};
int scriptAccountsLength = ScriptType.P2SH.equals(wallet.getScriptType()) ? 1 : accounts.length;
for(int i = 0; i < scriptAccountsLength; i++) {
MenuItem item = new MenuItem(accounts[i]);
final List<ChildNumber> derivation = wallet.getScriptType().getDefaultDerivation(i);
item.setOnAction(event -> {
importButton.setDisable(true);
importKeystore(derivation);
});
importButton.getItems().add(item);
if(importButton instanceof SplitMenuButton importMenuButton) {
if(wallet.getScriptType() == null) {
ScriptType[] scriptTypes = new ScriptType[] {ScriptType.P2WPKH, ScriptType.P2SH_P2WPKH, ScriptType.P2PKH};
for(ScriptType scriptType : scriptTypes) {
MenuItem item = new MenuItem(scriptType.getDescription());
final List<ChildNumber> derivation = scriptType.getDefaultDerivation();
item.setOnAction(event -> {
importMenuButton.setDisable(true);
importKeystore(derivation);
});
importMenuButton.getItems().add(item);
}
} else {
String[] accounts = new String[] {"Default Account #0", "Account #1", "Account #2", "Account #3", "Account #4", "Account #5", "Account #6", "Account #7", "Account #8", "Account #9"};
int scriptAccountsLength = ScriptType.P2SH.equals(wallet.getScriptType()) ? 1 : accounts.length;
for(int i = 0; i < scriptAccountsLength; i++) {
MenuItem item = new MenuItem(accounts[i]);
final List<ChildNumber> derivation = wallet.getScriptType().getDefaultDerivation(i);
item.setOnAction(event -> {
importMenuButton.setDisable(true);
importKeystore(derivation);
});
importMenuButton.getItems().add(item);
}
}
}
importButton.managedProperty().bind(importButton.visibleProperty());
@@ -430,7 +434,9 @@ public class DevicePane extends TitledDescriptionPane {
setExpanded(true);
} else {
showOperationButton();
setContent(getTogglePassphraseOn());
if(!deviceOperation.equals(DeviceOperation.IMPORT)) {
setContent(getTogglePassphraseOn());
}
}
} else {
setError("Incorrect PIN", null);
@@ -622,7 +628,8 @@ public class DevicePane extends TitledDescriptionPane {
importButton.setVisible(true);
showHideLink.setText("Show derivation...");
showHideLink.setVisible(true);
setContent(getDerivationEntry(wallet.getScriptType() == null ? ScriptType.P2WPKH.getDefaultDerivation() : wallet.getScriptType().getDefaultDerivation()));
List<ChildNumber> defaultDerivation = wallet.getScriptType() == null ? ScriptType.P2WPKH.getDefaultDerivation() : wallet.getScriptType().getDefaultDerivation();
setContent(getDerivationEntry(keyDerivation == null ? defaultDerivation : keyDerivation.getDerivation()));
} else if(deviceOperation.equals(DeviceOperation.SIGN)) {
signButton.setDefaultButton(defaultDevice);
signButton.setVisible(true);
@@ -642,6 +649,7 @@ public class DevicePane extends TitledDescriptionPane {
TextField derivationField = new TextField();
derivationField.setPromptText("Derivation path");
derivationField.setText(KeyDerivation.writePath(derivation));
derivationField.setDisable(keyDerivation != null);
HBox.setHgrow(derivationField, Priority.ALWAYS);
ValidationSupport validationSupport = new ValidationSupport();
@@ -651,7 +659,8 @@ public class DevicePane extends TitledDescriptionPane {
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid derivation", !KeyDerivation.isValid(newValue))
));
Button importDerivationButton = new Button("Import");
Button importDerivationButton = new Button("Import Custom Derivation");
importDerivationButton.setDisable(true);
importDerivationButton.setOnAction(event -> {
showHideLink.setVisible(true);
setExpanded(false);
@@ -660,7 +669,8 @@ public class DevicePane extends TitledDescriptionPane {
});
derivationField.textProperty().addListener((observable, oldValue, newValue) -> {
importDerivationButton.setDisable(newValue.isEmpty() || !KeyDerivation.isValid(newValue));
importButton.setDisable(newValue.isEmpty() || !KeyDerivation.isValid(newValue) || !KeyDerivation.parsePath(newValue).equals(derivation));
importDerivationButton.setDisable(newValue.isEmpty() || !KeyDerivation.isValid(newValue) || KeyDerivation.parsePath(newValue).equals(derivation));
});
HBox contentBox = new HBox();
@@ -1,5 +1,6 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.EventManager;
@@ -12,11 +13,13 @@ import java.io.*;
public class FileKeystoreImportPane extends FileImportPane {
protected final Wallet wallet;
private final KeystoreFileImport importer;
private final KeyDerivation requiredDerivation;
public FileKeystoreImportPane(Wallet wallet, KeystoreFileImport importer) {
public FileKeystoreImportPane(Wallet wallet, KeystoreFileImport importer, KeyDerivation requiredDerivation) {
super(importer, importer.getName(), "Keystore import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png", importer.isKeystoreImportScannable());
this.wallet = wallet;
this.importer = importer;
this.requiredDerivation = requiredDerivation;
}
protected void importFile(String fileName, InputStream inputStream, String password) throws ImportException {
@@ -25,6 +28,10 @@ public class FileKeystoreImportPane extends FileImportPane {
keystore = importer.getKeystore(wallet.getScriptType(), inputStream, password);
}
EventManager.get().post(new KeystoreImportEvent(keystore));
if(requiredDerivation != null && !requiredDerivation.getDerivation().equals(keystore.getKeyDerivation().getDerivation())) {
setError("Incorrect derivation", "This account requires a derivation of " + requiredDerivation.getDerivationPath() + ", but the imported keystore has a derivation of " + keystore.getKeyDerivation().getDerivationPath() + ".");
} else {
EventManager.get().post(new KeystoreImportEvent(keystore));
}
}
}
@@ -7,6 +7,7 @@ import javafx.application.Platform;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Dialog;
import javafx.scene.control.DialogPane;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import org.controlsfx.control.textfield.CustomPasswordField;
import org.controlsfx.control.textfield.TextFields;
@@ -20,11 +21,15 @@ public class KeystorePassphraseDialog extends Dialog<String> {
}
public KeystorePassphraseDialog(String walletName, Keystore keystore) {
this(walletName, keystore, false);
}
public KeystorePassphraseDialog(String walletName, Keystore keystore, boolean confirm) {
this.passphrase = (CustomPasswordField) TextFields.createClearablePasswordField();
final DialogPane dialogPane = getDialogPane();
setTitle("Keystore Passphrase" + (walletName != null ? " - " + walletName : ""));
dialogPane.setHeaderText("Please enter the passphrase for keystore: \n" + keystore.getLabel());
dialogPane.setHeaderText((confirm ? "Re-enter" : "Enter") + " the BIP39 passphrase\n" + (confirm ? "to confirm:" : "for keystore: " + keystore.getLabel()));
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
AppServices.setStageIcon(dialogPane.getScene().getWindow());
dialogPane.getButtonTypes().addAll(ButtonType.CANCEL, ButtonType.OK);
@@ -40,6 +45,13 @@ public class KeystorePassphraseDialog extends Dialog<String> {
content.setPrefHeight(50);
content.getChildren().add(passphrase);
Glyph warnGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_TRIANGLE);
warnGlyph.getStyleClass().add("warn-icon");
warnGlyph.setFontSize(12);
Label warnLabel = new Label("A BIP39 passphrase is not a wallet password!", warnGlyph);
warnLabel.setGraphicTextGap(5);
content.getChildren().add(warnLabel);
dialogPane.setContent(content);
Platform.runLater(passphrase::requestFocus);
@@ -13,6 +13,7 @@ import com.sparrowwallet.sparrow.whirlpool.WhirlpoolException;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import org.controlsfx.glyphfont.Glyph;
import org.controlsfx.tools.Platform;
public class MixStatusCell extends TreeTableCell<Entry, UtxoEntry.MixStatus> {
public MixStatusCell() {
@@ -40,20 +41,15 @@ public class MixStatusCell extends TreeTableCell<Entry, UtxoEntry.MixStatus> {
setContextMenu(null);
}
if(mixStatus.getMixProgress() != null) {
Tooltip tooltip = new Tooltip();
tooltip.setText("Pool: " + mixStatus.getMixProgress().getPoolId().replace("btc", " BTC"));
setTooltip(tooltip);
}
if(mixStatus.getNextMixUtxo() != null) {
setMixSuccess(mixStatus.getNextMixUtxo());
} else if(mixStatus.getMixFailReason() != null) {
setMixFail(mixStatus.getMixFailReason());
setMixFail(mixStatus.getMixFailReason(), mixStatus.getMixError());
} else if(mixStatus.getMixProgress() != null) {
setMixProgress(mixStatus.getMixProgress());
} else {
setGraphic(null);
setTooltip(null);
}
}
}
@@ -67,17 +63,32 @@ public class MixStatusCell extends TreeTableCell<Entry, UtxoEntry.MixStatus> {
setTooltip(tt);
}
private void setMixFail(MixFailReason mixFailReason) {
private void setMixFail(MixFailReason mixFailReason, String mixError) {
if(mixFailReason != MixFailReason.CANCEL) {
setGraphic(getFailGlyph());
Tooltip tt = new Tooltip();
tt.setText(mixFailReason.getMessage());
tt.setText(mixFailReason.getMessage() + (mixError == null ? "" : ": " + mixError) +
"\nMix failures are generally caused by peers disconnecting during a mix." +
"\nMake sure your internet connection is stable and the computer is configured to prevent sleeping." +
"\nTo prevent sleeping, use the " + getPlatformSleepConfig() + " or enable the function in the Tools menu.");
setTooltip(tt);
} else {
setGraphic(null);
setTooltip(null);
}
}
private String getPlatformSleepConfig() {
Platform platform = Platform.getCurrent();
if(platform == Platform.OSX) {
return "OSX System Preferences";
} else if(platform == Platform.WINDOWS) {
return "Windows Control Panel";
}
return "system power settings";
}
private void setMixProgress(MixProgress mixProgress) {
if(mixProgress.getMixStep() != MixStep.FAIL) {
ProgressIndicator progressIndicator = getProgressIndicator();
@@ -88,6 +99,7 @@ public class MixStatusCell extends TreeTableCell<Entry, UtxoEntry.MixStatus> {
setTooltip(tt);
} else {
setGraphic(null);
setTooltip(null);
}
}
@@ -38,6 +38,7 @@ import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
protected final Wallet wallet;
@@ -261,7 +262,13 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
importer.getKeystore(wallet.getScriptType().getDefaultDerivation(), wordEntriesProperty.get(), passphraseProperty.get());
validChecksum = true;
} catch(ImportException e) {
//ignore
if(e.getCause() instanceof MnemonicException.MnemonicTypeException) {
invalidLabel.setText("Unsupported Electrum seed");
invalidLabel.setTooltip(new Tooltip("Seeds created in Electrum do not follow the BIP39 standard. Import the Electrum wallet file directly."));
} else {
invalidLabel.setText("Invalid checksum");
invalidLabel.setTooltip(null);
}
}
}
@@ -384,6 +391,14 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
try {
Keystore keystore = importer.getKeystore(derivation, wordEntriesProperty.get(), passphraseProperty.get());
if(!dryrun) {
if(passphraseProperty.get() != null && !passphraseProperty.get().isEmpty()) {
KeystorePassphraseDialog keystorePassphraseDialog = new KeystorePassphraseDialog(null, keystore, true);
Optional<String> optPassphrase = keystorePassphraseDialog.showAndWait();
if(optPassphrase.isEmpty() || !optPassphrase.get().equals(passphraseProperty.get())) {
throw new ImportException("Re-entered passphrase did not match");
}
}
EventManager.get().post(new KeystoreImportEvent(keystore));
}
return true;
@@ -394,7 +409,7 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
} else if(e.getCause() != null && e.getCause().getMessage() != null && !e.getCause().getMessage().isEmpty()) {
errorMessage = e.getCause().getMessage();
}
setError("Import Error", errorMessage);
setError("Import Error", errorMessage + ".");
importButton.setDisable(false);
return false;
}
@@ -95,7 +95,7 @@ public class WalletImportDialog extends Dialog<Wallet> {
List<Device> devices = enumerateService.getValue();
importAccordion.getPanes().removeIf(titledPane -> titledPane instanceof DevicePane);
for(Device device : devices) {
DevicePane devicePane = new DevicePane(new Wallet(), device, devices.size() == 1);
DevicePane devicePane = new DevicePane(new Wallet(), device, devices.size() == 1, null);
importAccordion.getPanes().add(0, devicePane);
}
Platform.runLater(() -> EventManager.get().post(new UsbDeviceEvent(devices)));
@@ -131,10 +131,10 @@ public class WalletPasswordDialog extends Dialog<SecureString> {
}
public enum PasswordRequirement {
LOAD("Please enter the wallet password:", "Unlock"),
LOAD("Enter the wallet password:", "Unlock"),
UPDATE_NEW("Add a password to the wallet?\nLeave empty for no password:", "No Password"),
UPDATE_EMPTY("This wallet has no password.\nAdd a password to the wallet?\nLeave empty for no password:", "No Password"),
UPDATE_SET("Please re-enter the wallet password:", "Verify Password"),
UPDATE_SET("Re-enter the wallet password:", "Verify Password"),
UPDATE_CHANGE("Enter the new wallet password.\nLeave empty for no password:", "No Password");
private final String description;
@@ -0,0 +1,15 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet;
public class WalletLockEvent {
private final Wallet wallet;
public WalletLockEvent(Wallet wallet) {
this.wallet = wallet;
}
public Wallet getWallet() {
return wallet;
}
}
@@ -0,0 +1,15 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet;
public class WalletUnlockEvent {
private final Wallet wallet;
public WalletUnlockEvent(Wallet wallet) {
this.wallet = wallet;
}
public Wallet getWallet() {
return wallet;
}
}
@@ -12,6 +12,7 @@ public class WhirlpoolMixEvent {
private final MixProgress mixProgress;
private final Utxo nextUtxo;
private final MixFailReason mixFailReason;
private final String mixError;
public WhirlpoolMixEvent(Wallet wallet, BlockTransactionHashIndex utxo, MixProgress mixProgress) {
this.wallet = wallet;
@@ -19,6 +20,7 @@ public class WhirlpoolMixEvent {
this.mixProgress = mixProgress;
this.nextUtxo = null;
this.mixFailReason = null;
this.mixError = null;
}
public WhirlpoolMixEvent(Wallet wallet, BlockTransactionHashIndex utxo, Utxo nextUtxo) {
@@ -27,14 +29,16 @@ public class WhirlpoolMixEvent {
this.mixProgress = null;
this.nextUtxo = nextUtxo;
this.mixFailReason = null;
this.mixError = null;
}
public WhirlpoolMixEvent(Wallet wallet, BlockTransactionHashIndex utxo, MixFailReason mixFailReason) {
public WhirlpoolMixEvent(Wallet wallet, BlockTransactionHashIndex utxo, MixFailReason mixFailReason, String mixError) {
this.wallet = wallet;
this.utxo = utxo;
this.mixProgress = null;
this.nextUtxo = null;
this.mixFailReason = mixFailReason;
this.mixError = mixError;
}
public Wallet getWallet() {
@@ -56,4 +60,8 @@ public class WhirlpoolMixEvent {
public MixFailReason getMixFailReason() {
return mixFailReason;
}
public String getMixError() {
return mixError;
}
}
@@ -62,6 +62,7 @@ public class FontAwesome5 extends GlyphFont {
SIGN_OUT_ALT('\uf2f5'),
SQUARE('\uf0c8'),
SNOWFLAKE('\uf2dc'),
SORT_NUMERIC_DOWN('\uf162'),
SUN('\uf185'),
THEATER_MASKS('\uf630'),
TIMES_CIRCLE('\uf057'),
@@ -1,10 +1,7 @@
package com.sparrowwallet.sparrow.io;
import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.wallet.Bip39MnemonicCode;
import com.sparrowwallet.drongo.wallet.DeterministicSeed;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.WalletModel;
import com.sparrowwallet.drongo.wallet.*;
import java.util.List;
@@ -31,6 +28,15 @@ public class Bip39 implements KeystoreMnemonicImport {
DeterministicSeed seed = new DeterministicSeed(mnemonicWords, passphrase, System.currentTimeMillis(), DeterministicSeed.Type.BIP39);
return Keystore.fromSeed(seed, derivation);
} catch (Exception e) {
try {
ElectrumMnemonicCode.INSTANCE.check(mnemonicWords);
throw new ImportException(new MnemonicException.MnemonicTypeException(DeterministicSeed.Type.ELECTRUM));
} catch(Exception ex) {
if(ex instanceof ImportException && ex.getCause() instanceof MnemonicException.MnemonicTypeException) {
throw (ImportException)ex;
}
}
throw new ImportException(e);
}
}
@@ -42,23 +42,27 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
keystore.setSource(KeystoreSource.HW_AIRGAPPED);
keystore.setWalletModel(WalletModel.COLDCARD);
if(cck.xpub != null && cck.path != null) {
ExtendedKey.Header header = ExtendedKey.Header.fromExtendedKey(cck.xpub);
if(header.getDefaultScriptType() != scriptType) {
throw new ImportException("This wallet's script type (" + scriptType + ") does not match the " + getName() + " script type (" + header.getDefaultScriptType() + ")");
try {
if(cck.xpub != null && cck.path != null) {
ExtendedKey.Header header = ExtendedKey.Header.fromExtendedKey(cck.xpub);
if(header.getDefaultScriptType() != scriptType) {
throw new ImportException("This wallet's script type (" + scriptType + ") does not match the " + getName() + " script type (" + header.getDefaultScriptType() + ")");
}
keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.path));
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(cck.xpub));
} else if(scriptType.equals(ScriptType.P2SH)) {
keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.p2sh_deriv));
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(cck.p2sh));
} else if(scriptType.equals(ScriptType.P2SH_P2WSH)) {
keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.p2wsh_p2sh_deriv != null ? cck.p2wsh_p2sh_deriv : cck.p2sh_p2wsh_deriv));
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(cck.p2wsh_p2sh != null ? cck.p2wsh_p2sh : cck.p2sh_p2wsh));
} else if(scriptType.equals(ScriptType.P2WSH)) {
keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.p2wsh_deriv));
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(cck.p2wsh));
} else {
throw new ImportException("Correct derivation not found for script type: " + scriptType);
}
keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.path));
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(cck.xpub));
} else if(scriptType.equals(ScriptType.P2SH)) {
keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.p2sh_deriv));
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(cck.p2sh));
} else if(scriptType.equals(ScriptType.P2SH_P2WSH)) {
keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.p2wsh_p2sh_deriv != null ? cck.p2wsh_p2sh_deriv : cck.p2sh_p2wsh_deriv));
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(cck.p2wsh_p2sh != null ? cck.p2wsh_p2sh : cck.p2sh_p2wsh));
} else if(scriptType.equals(ScriptType.P2WSH)) {
keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.p2wsh_deriv));
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(cck.p2wsh));
} else {
} catch(NullPointerException e) {
throw new ImportException("Correct derivation not found for script type: " + scriptType);
}
@@ -42,6 +42,7 @@ public class Config {
private boolean showTransactionHex = true;
private boolean showLoadingLog = false;
private boolean showUtxosChart = true;
private boolean preventSleep = false;
private List<File> recentWalletFiles;
private Integer keyDerivationPeriod;
private File hwi;
@@ -279,6 +280,14 @@ public class Config {
flush();
}
public boolean isPreventSleep() {
return preventSleep;
}
public void setPreventSleep(boolean preventSleep) {
this.preventSleep = preventSleep;
}
public List<File> getRecentWalletFiles() {
return recentWalletFiles;
}
@@ -416,24 +425,6 @@ public class Config {
flush();
}
public Boolean getCoreMultiWallet() {
return coreMultiWallet;
}
public void setCoreMultiWallet(Boolean coreMultiWallet) {
this.coreMultiWallet = coreMultiWallet;
flush();
}
public String getCoreWallet() {
return coreWallet;
}
public void setCoreWallet(String coreWallet) {
this.coreWallet = coreWallet;
flush();
}
public String getElectrumServer() {
return electrumServer;
}
@@ -62,11 +62,11 @@ public class SpecterDesktop implements WalletImport, WalletExport {
wallet.setName(specterWallet.label);
if(specterWallet.devices != null && specterWallet.devices.size() == wallet.getKeystores().size()) {
boolean uniqueLabels = specterWallet.devices.stream().map(d -> d.label).distinct().count() == specterWallet.devices.size();
boolean uniqueLabels = specterWallet.devices.stream().map(SpecterWalletDevice::getLabel).distinct().count() == specterWallet.devices.size();
for(int i = 0; i < specterWallet.devices.size(); i++) {
SpecterWalletDevice device = specterWallet.devices.get(i);
Keystore keystore = wallet.getKeystores().get(i);
keystore.setLabel(device.label + (uniqueLabels ? "" : " " + i));
keystore.setLabel(device.getLabel() + (uniqueLabels ? "" : " " + i));
WalletModel walletModel = device.getWalletModel();
if(walletModel != null) {
@@ -161,5 +161,13 @@ public class SpecterDesktop implements WalletImport, WalletExport {
return null;
}
public String getLabel() {
if(label == null) {
label = "Keystore";
}
return label.length() > Keystore.MAX_LABEL_LENGTH - 3 ? label.substring(0, Keystore.MAX_LABEL_LENGTH - 3) : label;
}
}
}
@@ -62,6 +62,10 @@ public class Storage {
}
public boolean isEncrypted() throws IOException {
if(!walletFile.exists()) {
return false;
}
return persistence.isEncrypted(walletFile);
}
@@ -169,6 +173,20 @@ public class Storage {
}
public void deleteTempBackups() {
File[] backups = getBackups(Storage.TEMP_BACKUP_PREFIX);
if(backups.length > 0) {
try {
Date date = BACKUP_DATE_FORMAT.parse(getBackupDate(backups[0].getName()));
ProcessHandle.Info processInfo = ProcessHandle.current().info();
if(processInfo.startInstant().isPresent() && processInfo.startInstant().get().isAfter(date.toInstant())) {
File permanent = new File(backups[0].getParent(), backups[0].getName().substring(Storage.TEMP_BACKUP_PREFIX.length() + 1));
backups[0].renameTo(permanent);
}
} catch(Exception e) {
log.error("Error copying temporary to permanent backup", e);
}
}
deleteBackups(Storage.TEMP_BACKUP_PREFIX);
}
@@ -531,10 +549,18 @@ public class Storage {
public static class KeyDerivationService extends Service<ECKey> {
private final Storage storage;
private final SecureString password;
private final boolean verifyPassword;
public KeyDerivationService(Storage storage, SecureString password) {
this.storage = storage;
this.password = password;
this.verifyPassword = false;
}
public KeyDerivationService(Storage storage, SecureString password, boolean verifyPassword) {
this.storage = storage;
this.password = password;
this.verifyPassword = verifyPassword;
}
@Override
@@ -542,7 +568,12 @@ public class Storage {
return new Task<>() {
protected ECKey call() throws IOException, StorageException {
try {
return storage.getEncryptionKey(password);
ECKey encryptionFullKey = storage.getEncryptionKey(password);
if(verifyPassword && !ECKey.fromPublicOnly(encryptionFullKey).equals(storage.getEncryptionPubKey())) {
throw new InvalidPasswordException("Derived pubkey does not match stored pubkey");
}
return encryptionFullKey;
} finally {
password.clear();
}
@@ -51,10 +51,6 @@ public class WalletBackupAndKey implements Comparable<WalletBackupAndKey> {
@Override
public int compareTo(WalletBackupAndKey other) {
if(wallet.getStandardAccountType() != null && other.wallet.getStandardAccountType() != null) {
return wallet.getStandardAccountType().ordinal() - other.wallet.getStandardAccountType().ordinal();
}
return wallet.getAccountIndex() - other.wallet.getAccountIndex();
return wallet.compareTo(other.wallet);
}
}
@@ -88,6 +88,7 @@ public class DbPersistence implements Persistence {
Persistence backupPersistence = PersistenceType.DB.getInstance();
backupPersistence.setKeyDeriver(keyDeriver);
backupWallet = backupPersistence.loadWallet(new Storage(backupPersistence, backupFile), password, encryptionKey).getWallet();
backupPersistence.close();
}
Map<WalletBackupAndKey, Storage> childWallets = loadChildWallets(storage, masterWallet, backupWallet, encryptionKey);
@@ -23,9 +23,11 @@ public class HwAirgappedController extends KeystoreImportDetailController {
importers = List.of(new ColdcardMultisig(), new CoboVaultMultisig(), new KeystoneMultisig(), new PassportMultisig(), new SeedSigner(), new SpecterDIY());
}
for(KeystoreImport importer : importers) {
FileKeystoreImportPane importPane = new FileKeystoreImportPane(getMasterController().getWallet(), (KeystoreFileImport)importer);;
importAccordion.getPanes().add(importPane);
for(KeystoreFileImport importer : importers) {
FileKeystoreImportPane importPane = new FileKeystoreImportPane(getMasterController().getWallet(), importer, getMasterController().getRequiredDerivation());
if(getMasterController().getRequiredModel() == null || getMasterController().getRequiredModel() == importer.getWalletModel()) {
importAccordion.getPanes().add(importPane);
}
}
importAccordion.getPanes().sort(Comparator.comparing(o -> ((TitledDescriptionPane) o).getTitle()));
@@ -13,8 +13,10 @@ public class HwUsbDevicesController extends KeystoreImportDetailController {
public void initializeView(List<Device> devices) {
for(Device device : devices) {
DevicePane devicePane = new DevicePane(getMasterController().getWallet(), device, devices.size() == 1);
deviceAccordion.getPanes().add(devicePane);
DevicePane devicePane = new DevicePane(getMasterController().getWallet(), device, devices.size() == 1, getMasterController().getRequiredDerivation());
if(getMasterController().getRequiredModel() == null || getMasterController().getRequiredModel() == device.getModel()) {
deviceAccordion.getPanes().add(devicePane);
}
}
}
}
@@ -1,7 +1,9 @@
package com.sparrowwallet.sparrow.keystoreimport;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.wallet.KeystoreSource;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletModel;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.io.Device;
import javafx.application.Platform;
@@ -10,6 +12,7 @@ import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.scene.Node;
import javafx.scene.control.Toggle;
import javafx.scene.control.ToggleButton;
import javafx.scene.control.ToggleGroup;
import javafx.scene.layout.StackPane;
@@ -27,6 +30,9 @@ public class KeystoreImportController implements Initializable {
@FXML
private StackPane importPane;
private KeyDerivation requiredDerivation;
private WalletModel requiredModel;
@Override
public void initialize(URL location, ResourceBundle resources) {
@@ -53,11 +59,12 @@ public class KeystoreImportController implements Initializable {
});
}
public void selectSource(KeystoreSource keystoreSource) {
public void selectSource(KeystoreSource keystoreSource, boolean required) {
for(Toggle toggle : importMenu.getToggles()) {
if(toggle.getUserData().equals(keystoreSource)) {
Platform.runLater(() -> importMenu.selectToggle(toggle));
return;
} else if(required) {
((ToggleButton)toggle).setDisable(true);
}
}
}
@@ -96,4 +103,20 @@ public class KeystoreImportController implements Initializable {
throw new IllegalStateException("Can't find pane", e);
}
}
public KeyDerivation getRequiredDerivation() {
return requiredDerivation;
}
public void setRequiredDerivation(KeyDerivation requiredDerivation) {
this.requiredDerivation = requiredDerivation;
}
public WalletModel getRequiredModel() {
return requiredModel;
}
public void setRequiredModel(WalletModel requiredModel) {
this.requiredModel = requiredModel;
}
}
@@ -26,6 +26,10 @@ public class KeystoreImportDialog extends Dialog<Keystore> {
}
public KeystoreImportDialog(Wallet wallet, KeystoreSource initialSource) {
this(wallet, initialSource, null, null, false);
}
public KeystoreImportDialog(Wallet wallet, KeystoreSource initialSource, KeyDerivation requiredDerivation, WalletModel requiredModel, boolean restrictSource) {
EventManager.get().register(this);
setOnCloseRequest(event -> {
EventManager.get().unregister(this);
@@ -39,11 +43,16 @@ public class KeystoreImportDialog extends Dialog<Keystore> {
dialogPane.setContent(Borders.wrap(ksiLoader.load()).emptyBorder().buildAll());
keystoreImportController = ksiLoader.getController();
keystoreImportController.initializeView(wallet);
keystoreImportController.selectSource(initialSource);
keystoreImportController.selectSource(initialSource, restrictSource);
keystoreImportController.setRequiredDerivation(requiredDerivation);
keystoreImportController.setRequiredModel(requiredModel);
final ButtonType watchOnlyButtonType = new javafx.scene.control.ButtonType(Network.get().getXpubHeader().getDisplayName() + " / Watch Only Wallet", ButtonBar.ButtonData.LEFT);
if(!restrictSource) {
dialogPane.getButtonTypes().add(watchOnlyButtonType);
}
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
dialogPane.getButtonTypes().addAll(watchOnlyButtonType, cancelButtonType);
dialogPane.getButtonTypes().add(cancelButtonType);
dialogPane.setPrefWidth(650);
dialogPane.setPrefHeight(690);
AppServices.moveToActiveWindowScreen(this);
@@ -21,7 +21,7 @@ public class SwController extends KeystoreImportDetailController {
TitledDescriptionPane importPane = null;
if(importer instanceof KeystoreFileImport) {
importPane = new FileKeystoreImportPane(getMasterController().getWallet(), (KeystoreFileImport)importer);
importPane = new FileKeystoreImportPane(getMasterController().getWallet(), (KeystoreFileImport)importer, getMasterController().getRequiredDerivation());
} else if(importer instanceof KeystoreMnemonicImport) {
importPane = new MnemonicKeystoreImportPane(getMasterController().getWallet(), (KeystoreMnemonicImport)importer);
} else if(importer instanceof KeystoreXprvImport) {
@@ -1,5 +1,6 @@
package com.sparrowwallet.sparrow.net;
import com.google.common.net.HostAndPort;
import com.google.gson.Gson;
import com.google.gson.annotations.SerializedName;
import com.sparrowwallet.drongo.KeyPurpose;
@@ -7,6 +8,7 @@ import com.sparrowwallet.drongo.Network;
import com.sparrowwallet.drongo.OutputDescriptor;
import com.sparrowwallet.drongo.wallet.BlockTransactionHash;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.io.Config;
@@ -100,6 +102,7 @@ public class Bwt {
bwtConfig.gapLimit = gapLimit;
} else {
bwtConfig.requireAddresses = false;
bwtConfig.bitcoindTimeout = 30;
}
bwtConfig.verbose = log.isDebugEnabled() ? 2 : 0;
@@ -112,15 +115,20 @@ public class Bwt {
Config config = Config.get();
bwtConfig.bitcoindUrl = config.getCoreServer();
if(config.getCoreAuthType() == CoreAuthType.COOKIE && config.getCoreDataDir() != null) {
HostAndPort torProxy = getTorProxy();
if(Protocol.isOnionAddress(bwtConfig.bitcoindUrl) && torProxy != null) {
bwtConfig.bitcoindProxy = torProxy.toString();
}
if((config.getCoreAuthType() == CoreAuthType.COOKIE || config.getCoreAuth() == null || config.getCoreAuth().length() < 2) && config.getCoreDataDir() != null) {
bwtConfig.bitcoindDir = config.getCoreDataDir().getAbsolutePath() + "/";
} else {
bwtConfig.bitcoindAuth = config.getCoreAuth();
}
if(config.getCoreMultiWallet() != Boolean.FALSE) {
bwtConfig.bitcoindWallet = config.getCoreWallet();
}
bwtConfig.createWalletIfMissing = !outputDescriptors.isEmpty();
bwtConfig.bitcoindWallet = DEFAULT_CORE_WALLET;
bwtConfig.createWalletIfMissing = true;
Gson gson = new Gson();
String jsonConfig = gson.toJson(bwtConfig);
@@ -129,6 +137,12 @@ public class Bwt {
NativeBwtDaemon.start(jsonConfig, callback);
}
private HostAndPort getTorProxy() {
return AppServices.isTorRunning() ?
HostAndPort.fromParts("127.0.0.1", TorService.PROXY_PORT) :
(Config.get().getProxyServer() == null || Config.get().getProxyServer().isEmpty() || !Config.get().isUseProxy() ? null : HostAndPort.fromString(Config.get().getProxyServer().replace("localhost", "127.0.0.1")));
}
/**
* Shut down the BWT daemon
*
@@ -184,6 +198,12 @@ public class Bwt {
@SerializedName("bitcoind_wallet")
public String bitcoindWallet;
@SerializedName("bitcoind_proxy")
public String bitcoindProxy;
@SerializedName("bitcoind_timeout")
public Integer bitcoindTimeout;
@SerializedName("create_wallet_if_missing")
public Boolean createWalletIfMissing;
@@ -705,7 +705,7 @@ public class ElectrumServer {
}
public void copyPostmixLabels(Wallet wallet, Set<BlockTransactionHashIndex> newTransactionOutputs) {
if(wallet.getStandardAccountType() == StandardAccount.WHIRLPOOL_POSTMIX) {
if(wallet.getStandardAccountType() == StandardAccount.WHIRLPOOL_POSTMIX && wallet.getMasterWallet() != null) {
for(BlockTransactionHashIndex newRef : newTransactionOutputs) {
BlockTransactionHashIndex prevRef = wallet.getWalletTxos().keySet().stream()
.filter(txo -> wallet.getMasterWallet().getUtxoMixData(txo) != null && txo.isSpent() && txo.getSpentBy().getHash().equals(newRef.getHash())).findFirst().orElse(null);
@@ -891,6 +891,10 @@ public class ElectrumServer {
if(server.startsWith("electrs/")) {
String electrsVersion = server.substring("electrs/".length());
int dashIndex = electrsVersion.indexOf('-');
if(dashIndex > -1) {
electrsVersion = electrsVersion.substring(0, dashIndex);
}
try {
Version version = new Version(electrsVersion);
if(version.compareTo(ELECTRS_MIN_BATCHING_VERSION) >= 0) {
@@ -102,6 +102,6 @@ public final class IpAddressMatcher {
}
public static boolean isLocalNetworkAddress(String address) {
return LOCAL_RANGE_1.matches(address) || LOCAL_RANGE_2.matches(address) || LOCAL_RANGE_3.matches(address);
return "localhost".equals(address) || "127.0.0.1".equals(address) || LOCAL_RANGE_1.matches(address) || LOCAL_RANGE_2.matches(address) || LOCAL_RANGE_3.matches(address);
}
}
@@ -116,10 +116,21 @@ public enum Protocol {
return toUrlString() + hostAndPort.toString();
}
public boolean isOnionAddress(HostAndPort server) {
public static boolean isOnionAddress(HostAndPort server) {
return server.getHost().toLowerCase().endsWith(TorService.TOR_ADDRESS_SUFFIX);
}
public static boolean isOnionAddress(String address) {
if(address != null) {
Protocol protocol = Protocol.getProtocol(address);
if(protocol != null) {
return isOnionAddress(protocol.getServerHostAndPort(address));
}
}
return false;
}
public static Protocol getProtocol(String url) {
if(url.startsWith("tcp://")) {
return TCP;
@@ -26,11 +26,13 @@ public class TcpTransport implements Transport, Closeable {
public static final int DEFAULT_PORT = 50001;
private static final int[] BASE_READ_TIMEOUT_SECS = {3, 8, 16, 34};
private static final int[] SLOW_READ_TIMEOUT_SECS = {34, 68, 124, 208};
public static final long PER_REQUEST_READ_TIMEOUT_MILLIS = 50;
public static final int SOCKET_READ_TIMEOUT_MILLIS = 5000;
protected final HostAndPort server;
protected final SocketFactory socketFactory;
protected final int[] readTimeouts;
private Socket socket;
@@ -59,6 +61,7 @@ public class TcpTransport implements Transport, Closeable {
public TcpTransport(HostAndPort server, HostAndPort proxy) {
this.server = server;
this.socketFactory = (proxy == null ? SocketFactory.getDefault() : new ProxySocketFactory(proxy));
this.readTimeouts = (Config.get().getServerType() == ServerType.BITCOIN_CORE && Protocol.isOnionAddress(Config.get().getCoreServer()) ? SLOW_READ_TIMEOUT_SECS : BASE_READ_TIMEOUT_SECS);
}
@Override
@@ -91,16 +94,16 @@ public class TcpTransport implements Transport, Closeable {
private String readResponse() throws IOException {
try {
if(!readLock.tryLock((BASE_READ_TIMEOUT_SECS[readTimeoutIndex] * 1000) + (requestIdCount * PER_REQUEST_READ_TIMEOUT_MILLIS), TimeUnit.MILLISECONDS)) {
readTimeoutIndex = Math.min(readTimeoutIndex + 1, BASE_READ_TIMEOUT_SECS.length - 1);
log.warn("No response from server, setting read timeout to " + BASE_READ_TIMEOUT_SECS[readTimeoutIndex] + " secs");
if(!readLock.tryLock((readTimeouts[readTimeoutIndex] * 1000L) + (requestIdCount * PER_REQUEST_READ_TIMEOUT_MILLIS), TimeUnit.MILLISECONDS)) {
readTimeoutIndex = Math.min(readTimeoutIndex + 1, readTimeouts.length - 1);
log.warn("No response from server, setting read timeout to " + readTimeouts[readTimeoutIndex] + " secs");
throw new IOException("No response from server");
}
} catch(InterruptedException e) {
throw new IOException("Read thread interrupted");
}
if(readTimeoutIndex == BASE_READ_TIMEOUT_SECS.length - 1) {
if(readTimeoutIndex == readTimeouts.length - 1) {
readTimeoutIndex--;
}
@@ -3,15 +3,14 @@ package com.sparrowwallet.sparrow.net;
import javafx.concurrent.ScheduledService;
import javafx.concurrent.Task;
import net.freehaven.tor.control.TorControlError;
import org.berndpruenster.netlayer.tor.NativeTor;
import org.berndpruenster.netlayer.tor.Tor;
import org.berndpruenster.netlayer.tor.TorCtlException;
import org.berndpruenster.netlayer.tor.Torrc;
import org.berndpruenster.netlayer.tor.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.net.Socket;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
@@ -40,6 +39,7 @@ public class TorService extends ScheduledService<NativeTor> {
try {
LinkedHashMap<String, String> torrcOptionsMap = new LinkedHashMap<>();
torrcOptionsMap.put("SocksPort", Integer.toString(PROXY_PORT));
torrcOptionsMap.put("HashedControlPassword", "16:D780432418F09B06609940000924317D3B9DF522A3191F8F4E597E9329");
torrcOptionsMap.put("DisableNetwork", "0");
Torrc override = new Torrc(torrcOptionsMap);
@@ -62,4 +62,25 @@ public class TorService extends ScheduledService<NativeTor> {
}
};
}
public static Socket getControlSocket() {
Tor tor = Tor.getDefault();
if(tor != null) {
try {
Class<?> torClass = Class.forName("org.berndpruenster.netlayer.tor.Tor");
Field torControllerField = torClass.getDeclaredField("torController");
torControllerField.setAccessible(true);
TorController torController = (TorController)torControllerField.get(tor);
Class<?> torControllerClass = Class.forName("org.berndpruenster.netlayer.tor.TorController");
Field socketField = torControllerClass.getDeclaredField("socket");
socketField.setAccessible(true);
return (Socket)socketField.get(torController);
} catch(Exception e) {
log.error("Error retrieving Tor control socket", e);
}
}
return null;
}
}
@@ -102,10 +102,13 @@ public class ServerPreferencesController extends PreferencesDetailController {
private PasswordField corePass;
@FXML
private UnlabeledToggleSwitch coreMultiWallet;
private UnlabeledToggleSwitch coreUseProxy;
@FXML
private TextField coreWallet;
private TextField coreProxyHost;
@FXML
private TextField coreProxyPort;
@FXML
private Form electrumForm;
@@ -209,7 +212,9 @@ public class ServerPreferencesController extends PreferencesDetailController {
coreUser.textProperty().addListener(getBitcoinAuthListener(config));
corePass.textProperty().addListener(getBitcoinAuthListener(config));
coreWallet.textProperty().addListener(getBitcoinWalletListener(config));
coreUseProxy.selectedProperty().bindBidirectional(useProxy.selectedProperty());
coreProxyHost.textProperty().bindBidirectional(proxyHost.textProperty());
coreProxyPort.textProperty().bindBidirectional(proxyPort.textProperty());
electrumHost.textProperty().addListener(getElectrumServerListener(config));
electrumPort.textProperty().addListener(getElectrumServerListener(config));
@@ -250,11 +255,6 @@ public class ServerPreferencesController extends PreferencesDetailController {
}
});
coreMultiWallet.selectedProperty().addListener((observable, oldValue, newValue) -> {
config.setCoreMultiWallet(newValue);
coreWallet.setDisable(!newValue);
});
electrumUseSsl.selectedProperty().addListener((observable, oldValue, newValue) -> {
setElectrumServerInConfig(config);
electrumCertificate.setDisable(!newValue);
@@ -358,15 +358,6 @@ public class ServerPreferencesController extends PreferencesDetailController {
}
}
coreWallet.setPromptText("Default");
if(config.getCoreWallet() == null) {
coreWallet.setText(Bwt.DEFAULT_CORE_WALLET);
} else {
coreWallet.setText(config.getCoreWallet());
}
coreMultiWallet.setSelected(config.getCoreMultiWallet() != Boolean.FALSE);
String electrumServer = config.getElectrumServer();
if(electrumServer != null) {
Protocol protocol = Protocol.getProtocol(electrumServer);
@@ -519,8 +510,9 @@ public class ServerPreferencesController extends PreferencesDetailController {
coreDataDirSelect.setDisable(!editable);
coreUser.setDisable(!editable);
corePass.setDisable(!editable);
coreMultiWallet.setDisable(!editable);
coreWallet.setDisable(!editable);
coreUseProxy.setDisable(!editable);
coreProxyHost.setDisable(!editable);
coreProxyPort.setDisable(!editable);
electrumHost.setDisable(!editable);
electrumPort.setDisable(!editable);
@@ -665,13 +657,6 @@ public class ServerPreferencesController extends PreferencesDetailController {
};
}
@NotNull
private ChangeListener<String> getBitcoinWalletListener(Config config) {
return (observable, oldValue, newValue) -> {
config.setCoreWallet(coreWallet.getText());
};
}
@NotNull
private ChangeListener<String> getElectrumServerListener(Config config) {
return (observable, oldValue, newValue) -> {
@@ -1,13 +1,17 @@
package com.sparrowwallet.sparrow.transaction;
import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.protocol.NonStandardScriptException;
import com.sparrowwallet.drongo.protocol.Transaction;
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.control.CoinLabel;
import com.sparrowwallet.sparrow.event.TransactionTabsClosedEvent;
import com.sparrowwallet.sparrow.io.Config;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.chart.PieChart;
@@ -18,6 +22,7 @@ import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import java.util.List;
import java.util.Locale;
public abstract class TransactionFormController extends BaseController {
private static final int MAX_PIE_SEGMENTS = 200;
@@ -64,7 +69,9 @@ public abstract class TransactionFormController extends BaseController {
pie.getData().forEach(data -> {
Tooltip tooltip = new Tooltip();
double percent = 100.0 * (data.getPieValue() / totalSum);
tooltip.setText(data.getName() + " " + String.format("%.1f", percent) + "%");
String satsValue = String.format(Locale.ENGLISH, "%,d", (long)data.getPieValue()) + " sats";
String btcValue = CoinLabel.BTC_FORMAT.format(data.getPieValue() / Transaction.SATOSHIS_PER_BITCOIN) + " BTC";
tooltip.setText(data.getName() + "\n" + (Config.get().getBitcoinUnit() == BitcoinUnit.BTC ? btcValue : satsValue) + " (" + String.format("%.1f", percent) + "%)");
Tooltip.install(data.getNode(), tooltip);
data.pieValueProperty().addListener((observable, oldValue, newValue) -> tooltip.setText(newValue + "%"));
});
@@ -1,5 +1,5 @@
package com.sparrowwallet.sparrow.wallet;
public enum Function {
TRANSACTIONS, SEND, RECEIVE, ADDRESSES, UTXOS, SETTINGS;
TRANSACTIONS, SEND, RECEIVE, ADDRESSES, UTXOS, SETTINGS, LOCK;
}
@@ -5,17 +5,15 @@ import com.sparrowwallet.drongo.*;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.KeystoreSource;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletModel;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.*;
import com.sparrowwallet.sparrow.event.ChildWalletAddedEvent;
import com.sparrowwallet.sparrow.event.StorageEvent;
import com.sparrowwallet.sparrow.event.TimedEvent;
import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5Brands;
import com.sparrowwallet.sparrow.io.Storage;
import com.sparrowwallet.sparrow.keystoreimport.KeystoreImportDialog;
import com.sparrowwallet.sparrow.event.SettingsChangedEvent;
import javafx.application.Platform;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
@@ -104,6 +102,14 @@ public class KeystoreController extends WalletFormController implements Initiali
selectSourcePane.managedProperty().bind(selectSourcePane.visibleProperty());
if(keystore.isValid() || keystore.getExtendedPublicKey() != null) {
selectSourcePane.setVisible(false);
} else if(!getWalletForm().getWallet().isMasterWallet() && keystore.getKeyDerivation() != null) {
Wallet masterWallet = getWalletForm().getWallet().getMasterWallet();
int keystoreIndex = getWalletForm().getWallet().getKeystores().indexOf(keystore);
KeystoreSource keystoreSource = masterWallet.getKeystores().get(keystoreIndex).getSource();
for(Toggle toggle : keystoreSourceToggleGroup.getToggles()) {
ToggleButton toggleButton = (ToggleButton)toggle;
toggleButton.setDisable(toggleButton.getUserData() != keystoreSource);
}
}
viewSeedButton.managedProperty().bind(viewSeedButton.visibleProperty());
@@ -163,7 +169,7 @@ public class KeystoreController extends WalletFormController implements Initiali
scanXpubQR.setVisible(!valid);
});
setInputFieldsDisabled(!walletForm.getWallet().isMasterWallet() || !walletForm.getWallet().getChildWallets().isEmpty());
setInputFieldsDisabled(keystore.getSource() != KeystoreSource.SW_WATCH && (!walletForm.getWallet().isMasterWallet() || !walletForm.getWallet().getChildWallets().isEmpty()));
}
private void setXpubContext(ExtendedKey extendedKey) {
@@ -310,7 +316,10 @@ public class KeystoreController extends WalletFormController implements Initiali
}
private void launchImportDialog(KeystoreSource initialSource) {
KeystoreImportDialog dlg = new KeystoreImportDialog(getWalletForm().getWallet(), initialSource);
boolean restrictSource = keystoreSourceToggleGroup.getToggles().stream().anyMatch(toggle -> ((ToggleButton)toggle).isDisabled());
KeyDerivation requiredDerivation = restrictSource ? keystore.getKeyDerivation() : null;
WalletModel requiredModel = restrictSource ? keystore.getWalletModel() : null;
KeystoreImportDialog dlg = new KeystoreImportDialog(getWalletForm().getWallet(), initialSource, requiredDerivation, requiredModel, restrictSource);
Optional<Keystore> result = dlg.showAndWait();
if(result.isPresent()) {
selectSourcePane.setVisible(false);
@@ -421,7 +430,7 @@ public class KeystoreController extends WalletFormController implements Initiali
@Subscribe
public void childWalletAdded(ChildWalletAddedEvent event) {
if(event.getMasterWalletId().equals(walletForm.getWalletId())) {
setInputFieldsDisabled(true);
setInputFieldsDisabled(keystore.getSource() != KeystoreSource.SW_WATCH);
}
}
@@ -45,7 +45,7 @@ public class MixToController implements Initializable {
List<Wallet> destinationWallets = AppServices.get().getOpenWallets().keySet().stream().filter(openWallet -> openWallet.isValid()
&& openWallet != wallet && openWallet != wallet.getMasterWallet()
&& !StandardAccount.WHIRLPOOL_ACCOUNTS.contains(openWallet.getStandardAccountType())).collect(Collectors.toList());
&& (openWallet.getStandardAccountType() == null || !StandardAccount.WHIRLPOOL_ACCOUNTS.contains(openWallet.getStandardAccountType()))).collect(Collectors.toList());
allWallets.addAll(destinationWallets);
mixToWallets.setItems(FXCollections.observableList(allWallets));
@@ -134,7 +134,7 @@ public class ReceiveController extends WalletFormController implements Initializ
} else if(!currentOutputs.isEmpty()) {
long count = currentOutputs.size();
BlockTransactionHashIndex lastUsedReference = currentOutputs.stream().skip(count - 1).findFirst().get();
lastUsed.setText(lastUsedReference.getHeight() <= 0 ? "Unconfirmed Transaction" : DATE_FORMAT.format(lastUsedReference.getDate()));
lastUsed.setText(lastUsedReference.getHeight() <= 0 ? "Unconfirmed Transaction" : (lastUsedReference.getDate() == null ? "Unknown" : DATE_FORMAT.format(lastUsedReference.getDate())));
lastUsed.setGraphic(getWarningGlyph());
if(!address.getStyleClass().contains("error")) {
address.getStyleClass().add("error");
@@ -6,10 +6,7 @@ import com.sparrowwallet.drongo.crypto.*;
import com.sparrowwallet.drongo.policy.Policy;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.KeystoreSource;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletModel;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.hummingbird.UR;
import com.sparrowwallet.hummingbird.registry.*;
import com.sparrowwallet.sparrow.AppServices;
@@ -18,6 +15,8 @@ import com.sparrowwallet.sparrow.control.*;
import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.io.Storage;
import com.sparrowwallet.sparrow.io.StorageException;
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolServices;
import javafx.application.Platform;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.collections.FXCollections;
import javafx.event.ActionEvent;
@@ -32,12 +31,13 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import tornadofx.control.Fieldset;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.*;
import java.util.stream.Collectors;
import static com.sparrowwallet.sparrow.AppServices.showErrorDialog;
public class SettingsController extends WalletFormController implements Initializable {
private static final Logger log = LoggerFactory.getLogger(SettingsController.class);
@@ -79,7 +79,11 @@ public class SettingsController extends WalletFormController implements Initiali
private TabPane keystoreTabs;
@FXML Button export;
@FXML
private Button export;
@FXML
private Button addAccount;
@FXML
private Button apply;
@@ -254,6 +258,7 @@ public class SettingsController extends WalletFormController implements Initiali
scanDescriptorQR.setVisible(!walletForm.getWallet().isValid());
export.setDisable(!walletForm.getWallet().isValid());
addAccount.setDisable(!walletForm.getWallet().isValid() || walletForm.getWallet().getScriptType() == ScriptType.P2SH);
revert.setDisable(true);
apply.setDisable(true);
}
@@ -442,6 +447,86 @@ public class SettingsController extends WalletFormController implements Initiali
}
}
public void addAccount(ActionEvent event) {
Wallet openWallet = AppServices.get().getOpenWallets().entrySet().stream().filter(entry -> walletForm.getWalletFile().equals(entry.getValue().getWalletFile())).map(Map.Entry::getKey).findFirst().orElseThrow();
Wallet masterWallet = openWallet.isMasterWallet() ? openWallet : openWallet.getMasterWallet();
AddAccountDialog addAccountDialog = new AddAccountDialog(masterWallet);
Optional<StandardAccount> optAccount = addAccountDialog.showAndWait();
if(optAccount.isPresent()) {
StandardAccount standardAccount = optAccount.get();
if(masterWallet.getKeystores().stream().allMatch(ks -> ks.getSource() == KeystoreSource.SW_SEED)) {
if(masterWallet.isEncrypted()) {
String walletId = walletForm.getWalletId();
WalletPasswordDialog dlg = new WalletPasswordDialog(masterWallet.getName(), WalletPasswordDialog.PasswordRequirement.LOAD);
Optional<SecureString> password = dlg.showAndWait();
if(password.isPresent()) {
Storage.KeyDerivationService keyDerivationService = new Storage.KeyDerivationService(walletForm.getStorage(), password.get(), true);
keyDerivationService.setOnSucceeded(workerStateEvent -> {
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Done"));
ECKey encryptionFullKey = keyDerivationService.getValue();
Key key = new Key(encryptionFullKey.getPrivKeyBytes(), walletForm.getStorage().getKeyDeriver().getSalt(), EncryptionType.Deriver.ARGON2);
masterWallet.decrypt(key);
try {
addAndSaveAccount(masterWallet, standardAccount);
} finally {
masterWallet.encrypt(key);
for(Wallet childWallet : masterWallet.getChildWallets()) {
if(!childWallet.isEncrypted()) {
childWallet.encrypt(key);
}
}
key.clear();
encryptionFullKey.clear();
password.get().clear();
}
});
keyDerivationService.setOnFailed(workerStateEvent -> {
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Failed"));
if(keyDerivationService.getException() instanceof InvalidPasswordException) {
Optional<ButtonType> optResponse = showErrorDialog("Invalid Password", "The wallet password was invalid. Try again?", ButtonType.CANCEL, ButtonType.OK);
if(optResponse.isPresent() && optResponse.get().equals(ButtonType.OK)) {
Platform.runLater(() -> addAccount(null));
}
} else {
log.error("Error deriving wallet key", keyDerivationService.getException());
}
});
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.START, "Decrypting wallet..."));
keyDerivationService.start();
}
} else {
addAndSaveAccount(masterWallet, standardAccount);
}
} else {
Wallet childWallet = masterWallet.addChildWallet(standardAccount);
EventManager.get().post(new ChildWalletAddedEvent(getWalletForm().getStorage(), masterWallet, childWallet));
}
}
}
private void addAndSaveAccount(Wallet masterWallet, StandardAccount standardAccount) {
if(StandardAccount.WHIRLPOOL_ACCOUNTS.contains(standardAccount)) {
WhirlpoolServices.prepareWhirlpoolWallet(masterWallet, getWalletForm().getWalletId(), getWalletForm().getStorage());
} else {
Wallet childWallet = masterWallet.addChildWallet(standardAccount);
EventManager.get().post(new ChildWalletAddedEvent(getWalletForm().getStorage(), masterWallet, childWallet));
}
for(Wallet childWallet : masterWallet.getChildWallets()) {
Storage storage = AppServices.get().getOpenWallets().get(childWallet);
if(!storage.isPersisted(childWallet)) {
try {
storage.saveWallet(childWallet);
} catch(Exception e) {
AppServices.showErrorDialog("Error saving wallet " + childWallet.getName(), e.getMessage());
}
}
}
}
private void setInputFieldsDisabled(boolean disabled) {
policyType.setDisable(disabled);
scriptType.setDisable(disabled);
@@ -479,6 +564,7 @@ public class SettingsController extends WalletFormController implements Initiali
revert.setDisable(false);
apply.setDisable(!wallet.isValid());
export.setDisable(true);
addAccount.setDisable(true);
scanDescriptorQR.setVisible(!wallet.isValid());
}
}
@@ -487,6 +573,7 @@ public class SettingsController extends WalletFormController implements Initiali
public void walletSettingsChanged(WalletSettingsChangedEvent event) {
if(event.getWalletId().equals(walletForm.getWalletId())) {
export.setDisable(!event.getWallet().isValid());
addAccount.setDisable(!event.getWallet().isValid() || event.getWallet().getScriptType() == ScriptType.P2SH);
scanDescriptorQR.setVisible(!event.getWallet().isValid());
}
}
@@ -588,14 +675,15 @@ public class SettingsController extends WalletFormController implements Initiali
key = new Key(encryptionFullKey.getPrivKeyBytes(), walletForm.getStorage().getKeyDeriver().getSalt(), EncryptionType.Deriver.ARGON2);
Wallet masterWallet = walletForm.getWallet().isMasterWallet() ? walletForm.getWallet() : walletForm.getWallet().getMasterWallet();
if(dlg.isChangePassword()) {
if(dlg.isDeleteBackups()) {
walletForm.deleteBackups();
}
walletForm.getStorage().setEncryptionPubKey(null);
walletForm.getWallet().decrypt(key);
for(Wallet childWallet : walletForm.getWallet().getChildWallets()) {
masterWallet.decrypt(key);
for(Wallet childWallet : masterWallet.getChildWallets()) {
childWallet.decrypt(key);
}
saveWallet(true, false);
@@ -606,8 +694,8 @@ public class SettingsController extends WalletFormController implements Initiali
walletForm.deleteBackups();
}
walletForm.getWallet().encrypt(key);
for(Wallet childWallet : walletForm.getWallet().getChildWallets()) {
masterWallet.encrypt(key);
for(Wallet childWallet : masterWallet.getChildWallets()) {
childWallet.encrypt(key);
}
walletForm.getStorage().setEncryptionPubKey(encryptionPubKey);
@@ -132,13 +132,14 @@ public class TransactionsController extends WalletFormController implements Init
if(file != null) {
try(FileOutputStream outputStream = new FileOutputStream(file)) {
CsvWriter writer = new CsvWriter(outputStream, ',', StandardCharsets.UTF_8);
writer.writeRecord(new String[] {"Date", "Label", "Value", "Balance"});
writer.writeRecord(new String[] {"Date", "Label", "Value", "Balance", "Txid"});
for(Entry entry : walletTransactionsEntry.getChildren()) {
TransactionEntry txEntry = (TransactionEntry)entry;
writer.write(txEntry.getBlockTransaction().getDate() == null ? "Unconfirmed" : EntryCell.DATE_FORMAT.format(txEntry.getBlockTransaction().getDate()));
writer.write(txEntry.getLabel());
writer.write(getCoinValue(txEntry.getValue()));
writer.write(getCoinValue(txEntry.getBalance()));
writer.write(txEntry.getBlockTransaction().getHash().toString());
writer.endRecord();
}
writer.close();
@@ -102,8 +102,8 @@ public class UtxoEntry extends HashIndexEntry {
mixStatusProperty().set(new MixStatus(mixProgress));
}
public void setMixFailReason(MixFailReason mixFailReason) {
mixStatusProperty().set(new MixStatus(mixFailReason));
public void setMixFailReason(MixFailReason mixFailReason, String mixError) {
mixStatusProperty().set(new MixStatus(mixFailReason, mixError));
}
public void setNextMixUtxo(Utxo nextMixUtxo) {
@@ -126,6 +126,7 @@ public class UtxoEntry extends HashIndexEntry {
private MixProgress mixProgress;
private Utxo nextMixUtxo;
private MixFailReason mixFailReason;
private String mixError;
public MixStatus(MixProgress mixProgress) {
this.mixProgress = mixProgress;
@@ -135,8 +136,9 @@ public class UtxoEntry extends HashIndexEntry {
this.nextMixUtxo = nextMixUtxo;
}
public MixStatus(MixFailReason mixFailReason) {
public MixStatus(MixFailReason mixFailReason, String mixError) {
this.mixFailReason = mixFailReason;
this.mixError = mixError;
}
public UtxoEntry getUtxoEntry() {
@@ -175,5 +177,9 @@ public class UtxoEntry extends HashIndexEntry {
public MixFailReason getMixFailReason() {
return mixFailReason;
}
public String getMixError() {
return mixError;
}
}
}
@@ -21,6 +21,7 @@ import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.io.Storage;
import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolDialog;
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolServices;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.WeakChangeListener;
@@ -29,7 +30,9 @@ import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Tooltip;
import javafx.scene.control.TreeItem;
import javafx.scene.layout.HBox;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
@@ -45,6 +48,8 @@ import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.stream.Collectors;
import static com.sparrowwallet.sparrow.AppServices.showErrorDialog;
public class UtxosController extends WalletFormController implements Initializable {
private static final Logger log = LoggerFactory.getLogger(UtxosController.class);
@@ -90,6 +95,20 @@ public class UtxosController extends WalletFormController implements Initializab
mixTo.setDisable(newValue);
};
private final ChangeListener<Boolean> mixingListener = (observable, oldValue, newValue) -> {
if(!newValue) {
WalletUtxosEntry walletUtxosEntry = getWalletForm().getWalletUtxosEntry();
for(Entry entry : walletUtxosEntry.getChildren()) {
UtxoEntry utxoEntry = (UtxoEntry)entry;
if(utxoEntry.getMixStatus() != null && utxoEntry.getMixStatus().getMixProgress() != null
&& utxoEntry.getMixStatus().getMixProgress().getMixStep() != null
&& utxoEntry.getMixStatus().getMixProgress().getMixStep().isInterruptable()) {
utxoEntry.setMixProgress(null);
}
}
}
};
@Override
public void initialize(URL location, ResourceBundle resources) {
EventManager.get().register(this);
@@ -118,6 +137,7 @@ public class UtxosController extends WalletFormController implements Initializab
stopMix.visibleProperty().bind(whirlpool.mixingProperty());
whirlpool.startingProperty().addListener(new WeakChangeListener<>(mixingStartingListener));
whirlpool.stoppingProperty().addListener(new WeakChangeListener<>(mixingStoppingListener));
whirlpool.mixingProperty().addListener(new WeakChangeListener<>(mixingListener));
updateMixToButton();
}
}
@@ -140,10 +160,7 @@ public class UtxosController extends WalletFormController implements Initializab
}
private boolean canWalletMix() {
return Whirlpool.WHIRLPOOL_NETWORKS.contains(Network.get())
&& getWalletForm().getWallet().getKeystores().size() == 1
&& getWalletForm().getWallet().getKeystores().get(0).hasSeed()
&& !getWalletForm().getWallet().isWhirlpoolMixWallet();
return WhirlpoolServices.canWalletMix(getWalletForm().getWallet());
}
private void updateButtons(BitcoinUnit unit) {
@@ -191,7 +208,9 @@ public class UtxosController extends WalletFormController implements Initializab
}
private List<Entry> getSelectedEntries() {
return utxosTable.getSelectionModel().getSelectedCells().stream().map(tp -> (UtxoEntry)tp.getTreeItem().getValue())
return utxosTable.getSelectionModel().getSelectedCells().stream()
.filter(tp -> tp.getTreeItem() != null)
.map(tp -> (UtxoEntry)tp.getTreeItem().getValue())
.filter(utxoEntry -> utxoEntry.isSpendable() && !utxoEntry.isMixing())
.collect(Collectors.toList());
}
@@ -218,7 +237,7 @@ public class UtxosController extends WalletFormController implements Initializab
WalletPasswordDialog dlg = new WalletPasswordDialog(wallet.getMasterName(), WalletPasswordDialog.PasswordRequirement.LOAD);
Optional<SecureString> password = dlg.showAndWait();
if(password.isPresent()) {
Storage.KeyDerivationService keyDerivationService = new Storage.KeyDerivationService(walletForm.getStorage(), password.get());
Storage.KeyDerivationService keyDerivationService = new Storage.KeyDerivationService(walletForm.getStorage(), password.get(), true);
keyDerivationService.setOnSucceeded(workerStateEvent -> {
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Done"));
ECKey encryptionFullKey = keyDerivationService.getValue();
@@ -243,7 +262,14 @@ public class UtxosController extends WalletFormController implements Initializab
});
keyDerivationService.setOnFailed(workerStateEvent -> {
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Failed"));
AppServices.showErrorDialog("Incorrect Password", keyDerivationService.getException().getMessage());
if(keyDerivationService.getException() instanceof InvalidPasswordException) {
Optional<ButtonType> optResponse = showErrorDialog("Invalid Password", "The wallet password was invalid. Try again?", ButtonType.CANCEL, ButtonType.OK);
if(optResponse.isPresent() && optResponse.get().equals(ButtonType.OK)) {
Platform.runLater(() -> previewPremix(tx0Preview, utxoEntries));
}
} else {
log.error("Error deriving wallet key", keyDerivationService.getException());
}
});
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.START, "Decrypting wallet..."));
keyDerivationService.start();
@@ -258,16 +284,7 @@ public class UtxosController extends WalletFormController implements Initializab
}
private void prepareWhirlpoolWallet(Wallet decryptedWallet) {
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(getWalletForm().getWalletId());
whirlpool.setScode(decryptedWallet.getMasterMixConfig().getScode());
whirlpool.setHDWallet(getWalletForm().getWalletId(), decryptedWallet);
for(StandardAccount whirlpoolAccount : StandardAccount.WHIRLPOOL_ACCOUNTS) {
if(decryptedWallet.getChildWallet(whirlpoolAccount) == null) {
Wallet childWallet = decryptedWallet.addChildWallet(whirlpoolAccount);
EventManager.get().post(new ChildWalletAddedEvent(getWalletForm().getStorage(), decryptedWallet, childWallet));
}
}
WhirlpoolServices.prepareWhirlpoolWallet(decryptedWallet, getWalletForm().getWalletId(), getWalletForm().getStorage());
}
private void previewPremix(Wallet wallet, Tx0Preview tx0Preview, List<UtxoEntry> utxoEntries) {
@@ -371,11 +388,8 @@ public class UtxosController extends WalletFormController implements Initializab
updateMixToButton();
if(whirlpool.isStarted()) {
Whirlpool.RestartService restartService = new Whirlpool.RestartService(whirlpool);
restartService.setOnFailed(workerStateEvent -> {
log.error("Failed to restart whirlpool", workerStateEvent.getSource().getException());
});
restartService.start();
//Will automatically restart
AppServices.getWhirlpoolServices().stopWhirlpool(whirlpool, false);
}
}
}
@@ -444,9 +458,15 @@ public class UtxosController extends WalletFormController implements Initializab
if(event.getWallet().equals(walletForm.getWallet())) {
WalletUtxosEntry walletUtxosEntry = getWalletForm().getWalletUtxosEntry();
List<Entry> selectedEntries = utxosTable.getSelectionModel().getSelectedItems().stream().map(TreeItem::getValue).filter(Objects::nonNull).toList();
//Will automatically update utxosTable
walletUtxosEntry.updateUtxos();
if(!walletUtxosEntry.getChildren().containsAll(selectedEntries)) {
utxosTable.getSelectionModel().clearSelection();
}
utxosTable.updateHistory(event.getHistoryChangedNodes());
utxosChart.update(walletUtxosEntry);
}
@@ -528,7 +548,7 @@ public class UtxosController extends WalletFormController implements Initializab
if(event.getNextUtxo() != null) {
utxoEntry.setNextMixUtxo(event.getNextUtxo());
} else if(event.getMixFailReason() != null) {
utxoEntry.setMixFailReason(event.getMixFailReason());
utxoEntry.setMixFailReason(event.getMixFailReason(), event.getMixError());
} else {
utxoEntry.setMixProgress(event.getMixProgress());
}
@@ -1,33 +1,54 @@
package com.sparrowwallet.sparrow.wallet;
import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.drongo.SecureString;
import com.sparrowwallet.drongo.crypto.InvalidPasswordException;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.ReceiveActionEvent;
import com.sparrowwallet.sparrow.event.SendActionEvent;
import com.sparrowwallet.sparrow.event.WalletAddressesChangedEvent;
import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.io.Storage;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Toggle;
import javafx.scene.control.ToggleButton;
import javafx.scene.control.ToggleGroup;
import javafx.scene.control.*;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.controlsfx.control.textfield.CustomPasswordField;
import org.controlsfx.control.textfield.TextFields;
import org.controlsfx.glyphfont.FontAwesome;
import org.controlsfx.glyphfont.Glyph;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.URL;
import java.util.ResourceBundle;
import static com.sparrowwallet.sparrow.AppServices.showErrorDialog;
public class WalletController extends WalletFormController implements Initializable {
private static final Logger log = LoggerFactory.getLogger(WalletController.class);
@FXML
private StackPane walletPane;
@FXML
private VBox walletMenuBox;
@FXML
private ToggleGroup walletMenu;
private BorderPane lockPane;
private final BooleanProperty walletEncryptedProperty = new SimpleBooleanProperty(false);
@Override
public void initialize(URL location, ResourceBundle resources) {
EventManager.get().register(this);
@@ -47,7 +68,7 @@ public class WalletController extends WalletFormController implements Initializa
if(walletFunction.getUserData().equals(function)) {
existing = true;
walletFunction.setViewOrder(0);
} else {
} else if(function != Function.LOCK) {
walletFunction.setViewOrder(1);
}
}
@@ -78,6 +99,9 @@ public class WalletController extends WalletFormController implements Initializa
toggleButton.managedProperty().bind(toggleButton.visibleProperty());
}
walletMenuBox.managedProperty().bind(walletMenuBox.visibleProperty());
walletMenuBox.visibleProperty().bind(getWalletForm().lockedProperty().not());
configure(walletForm.getWallet());
}
@@ -111,6 +135,76 @@ public class WalletController extends WalletFormController implements Initializa
});
}
private void initializeLockScreen() {
lockPane = new BorderPane();
lockPane.setUserData(Function.LOCK);
lockPane.getStyleClass().add("wallet-pane");
VBox vBox = new VBox(20);
vBox.setAlignment(Pos.CENTER);
Glyph lock = new Glyph("FontAwesome", FontAwesome.Glyph.LOCK);
lock.setFontSize(80);
vBox.getChildren().add(lock);
Label label = new Label("Enter password to unlock:");
label.managedProperty().bind(label.visibleProperty());
label.visibleProperty().bind(walletEncryptedProperty);
CustomPasswordField passwordField = (CustomPasswordField)TextFields.createClearablePasswordField();
passwordField.setMaxWidth(300);
passwordField.managedProperty().bind(passwordField.visibleProperty());
passwordField.visibleProperty().bind(walletEncryptedProperty);
passwordField.setOnAction(event -> {
unlockWallet(passwordField);
});
Button unlockButton = new Button("Unlock");
unlockButton.setPrefWidth(300);
unlockButton.setOnAction(event -> {
unlockWallet(passwordField);
});
vBox.getChildren().addAll(label, passwordField, unlockButton);
StackPane stackPane = new StackPane();
stackPane.getChildren().add(vBox);
lockPane.setCenter(stackPane);
walletPane.getChildren().add(lockPane);
}
private void unlockWallet(CustomPasswordField passwordField) {
if(walletEncryptedProperty.get()) {
String walletId = walletForm.getWalletId();
SecureString password = new SecureString(passwordField.getText());
Storage.KeyDerivationService keyDerivationService = new Storage.KeyDerivationService(walletForm.getStorage(), password, true);
keyDerivationService.setOnSucceeded(workerStateEvent -> {
passwordField.clear();
password.clear();
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Done"));
unlockWallet();
});
keyDerivationService.setOnFailed(workerStateEvent -> {
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Failed"));
if(keyDerivationService.getException() instanceof InvalidPasswordException) {
showErrorDialog("Invalid Password", "The wallet password was invalid.");
} else {
log.error("Error deriving wallet key", keyDerivationService.getException());
}
});
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.START, "Decrypting wallet..."));
keyDerivationService.start();
} else {
unlockWallet();
}
}
private void unlockWallet() {
Wallet masterWallet = getWalletForm().getWallet().isMasterWallet() ? getWalletForm().getWallet() : getWalletForm().getWallet().getMasterWallet();
EventManager.get().post(new WalletUnlockEvent(masterWallet));
}
private void updateWalletEncryptedStatus() {
try {
walletEncryptedProperty.set(getWalletForm().getStorage().isEncrypted());
} catch(IOException e) {
log.warn("Error determining if wallet is locked", e);
}
}
@Subscribe
public void walletAddressesChanged(WalletAddressesChangedEvent event) {
if(event.getWalletId().equals(walletForm.getWalletId())) {
@@ -118,6 +212,13 @@ public class WalletController extends WalletFormController implements Initializa
}
}
@Subscribe
public void walletSettingsChanged(WalletSettingsChangedEvent event) {
if(event.getWalletId().equals(walletForm.getWalletId())) {
Platform.runLater(this::updateWalletEncryptedStatus);
}
}
@Subscribe
public void receiveAction(ReceiveActionEvent event) {
if(event.getWallet().equals(walletForm.getWallet())) {
@@ -131,4 +232,27 @@ public class WalletController extends WalletFormController implements Initializa
selectFunction(Function.SEND);
}
}
@Subscribe
public void walletLock(WalletLockEvent event) {
if(event.getWallet().equals(walletForm.getMasterWallet())) {
if(lockPane == null) {
updateWalletEncryptedStatus();
initializeLockScreen();
}
getWalletForm().setLocked(true);
lockPane.setViewOrder(-1);
}
}
@Subscribe
public void walletUnlock(WalletUnlockEvent event) {
if(event.getWallet().equals(walletForm.getMasterWallet())) {
getWalletForm().setLocked(false);
if(lockPane != null) {
lockPane.setViewOrder(2);
}
}
}
}
@@ -14,6 +14,8 @@ import com.sparrowwallet.sparrow.net.ElectrumServer;
import com.sparrowwallet.sparrow.io.Storage;
import com.sparrowwallet.sparrow.net.ServerType;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.util.Duration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -37,6 +39,8 @@ public class WalletForm {
private ElectrumServer.TransactionMempoolService transactionMempoolService;
private final BooleanProperty lockedProperty = new SimpleBooleanProperty(false);
public WalletForm(Storage storage, Wallet currentWallet, Wallet backupWallet) {
this(storage, currentWallet, backupWallet, true);
}
@@ -58,6 +62,10 @@ public class WalletForm {
return wallet;
}
public Wallet getMasterWallet() {
return wallet.isMasterWallet() ? wallet : wallet.getMasterWallet();
}
public Storage getStorage() {
return storage;
}
@@ -298,6 +306,14 @@ public class WalletForm {
return walletUtxosEntry;
}
public BooleanProperty lockedProperty() {
return lockedProperty;
}
public void setLocked(boolean locked) {
this.lockedProperty.set(locked);
}
@Subscribe
public void walletDataChanged(WalletDataChangedEvent event) {
if(event.getWallet().equals(wallet)) {
@@ -42,9 +42,11 @@ import com.sparrowwallet.sparrow.whirlpool.dataPersister.SparrowDataPersister;
import com.sparrowwallet.sparrow.whirlpool.dataSource.SparrowDataSource;
import com.sparrowwallet.sparrow.whirlpool.dataSource.SparrowMinerFeeSupplier;
import com.sparrowwallet.sparrow.whirlpool.dataSource.SparrowPostmixHandler;
import com.sparrowwallet.sparrow.whirlpool.tor.SparrowTorClientService;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.concurrent.ScheduledService;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import org.slf4j.Logger;
@@ -76,6 +78,8 @@ public class Whirlpool {
private String walletId;
private String mixToWalletId;
private StartupService startupService;
private final BooleanProperty startingProperty = new SimpleBooleanProperty(false);
private final BooleanProperty stoppingProperty = new SimpleBooleanProperty(false);
private final BooleanProperty mixingProperty = new SimpleBooleanProperty(false);
@@ -84,7 +88,7 @@ public class Whirlpool {
this.whirlpoolServer = WhirlpoolServer.valueOf(network.getName().toUpperCase());
this.httpClientService = new JavaHttpClientService(torProxy);
this.stompClientService = new JavaStompClientService(httpClientService);
this.torClientService = new WhirlpoolTorClientService();
this.torClientService = new SparrowTorClientService(this);
this.whirlpoolWalletService = new WhirlpoolWalletService();
this.config = computeWhirlpoolWalletConfig(torProxy);
@@ -261,6 +265,15 @@ public class Whirlpool {
httpClientService.shutdown();
}
public StartupService createStartupService() {
if(startupService != null) {
startupService.cancel();
}
startupService = new StartupService(this);
return startupService;
}
private WalletUtxo getUtxo(WhirlpoolUtxo whirlpoolUtxo) {
Wallet wallet = AppServices.get().getWallet(walletId);
if(wallet != null) {
@@ -442,7 +455,7 @@ public class Whirlpool {
WalletUtxo walletUtxo = getUtxo(e.getWhirlpoolUtxo());
if(walletUtxo != null) {
log.debug("Mix failed for utxo " + e.getWhirlpoolUtxo().getUtxo().tx_hash + ":" + e.getWhirlpoolUtxo().getUtxo().tx_output_n + " " + e.getMixFailReason());
Platform.runLater(() -> EventManager.get().post(new WhirlpoolMixEvent(walletUtxo.wallet, walletUtxo.utxo, e.getMixFailReason())));
Platform.runLater(() -> EventManager.get().post(new WhirlpoolMixEvent(walletUtxo.wallet, walletUtxo.utxo, e.getMixFailReason(), e.getError())));
}
}
@@ -467,6 +480,15 @@ public class Whirlpool {
public void onWalletStop(WalletStopEvent e) {
if(e.getWhirlpoolWallet() == whirlpoolWalletService.whirlpoolWallet()) {
mixingProperty.set(false);
Wallet wallet = AppServices.get().getWallet(walletId);
if(wallet != null) {
Platform.runLater(() -> {
if(AppServices.isConnected()) {
AppServices.getWhirlpoolServices().startWhirlpool(wallet, this, false);
}
});
}
}
}
@@ -539,7 +561,7 @@ public class Whirlpool {
}
}
public static class StartupService extends Service<WhirlpoolWallet> {
public static class StartupService extends ScheduledService<WhirlpoolWallet> {
private final Whirlpool whirlpool;
public StartupService(Whirlpool whirlpool) {
@@ -588,37 +610,6 @@ public class Whirlpool {
}
}
public static class RestartService extends Service<Boolean> {
private final Whirlpool whirlpool;
public RestartService(Whirlpool whirlpool) {
this.whirlpool = whirlpool;
}
@Override
protected Task<Boolean> createTask() {
return new Task<>() {
protected Boolean call() throws Exception {
updateProgress(-1, 1);
updateMessage("Disconnecting from Whirlpool...");
whirlpool.stoppingProperty.set(true);
whirlpool.shutdown();
whirlpool.stoppingProperty.set(false);
updateMessage("Starting Whirlpool...");
whirlpool.startingProperty.set(true);
WhirlpoolWallet whirlpoolWallet = whirlpool.getWhirlpoolWallet();
if(AppServices.onlineProperty().get()) {
whirlpoolWallet.start();
}
whirlpool.startingProperty.set(false);
return true;
}
};
}
}
public static class WalletUtxo {
public final Wallet wallet;
public final BlockTransactionHashIndex utxo;
@@ -115,8 +115,18 @@ public class WhirlpoolController {
pool.setConverter(new StringConverter<Pool>() {
@Override
public String toString(Pool pool) {
return pool == null ? "Fetching pools..." : pool.getPoolId().replace("btc", " BTC");
public String toString(Pool selectedPool) {
if(selectedPool == null) {
pool.setTooltip(null);
return "Fetching pools...";
}
BitcoinUnit bitcoinUnit = wallet.getAutoUnit();
String satsValue = String.format(Locale.ENGLISH, "%,d", selectedPool.getDenomination()) + " sats";
String btcValue = CoinLabel.BTC_FORMAT.format((double)selectedPool.getDenomination() / Transaction.SATOSHIS_PER_BITCOIN) + " BTC";
pool.setTooltip(bitcoinUnit == BitcoinUnit.BTC ? new Tooltip(satsValue) : new Tooltip(btcValue));
return bitcoinUnit == BitcoinUnit.BTC ? btcValue : satsValue;
}
@Override
@@ -4,14 +4,18 @@ import com.google.common.eventbus.Subscribe;
import com.google.common.net.HostAndPort;
import com.samourai.whirlpool.client.wallet.WhirlpoolEventService;
import com.sparrowwallet.drongo.Network;
import com.sparrowwallet.drongo.wallet.DeterministicSeed;
import com.sparrowwallet.drongo.wallet.MixConfig;
import com.sparrowwallet.drongo.wallet.StandardAccount;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.WalletTabData;
import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.io.Storage;
import com.sparrowwallet.sparrow.net.TorService;
import javafx.util.Duration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -83,7 +87,11 @@ public class WhirlpoolServices {
}
}
Whirlpool.StartupService startupService = new Whirlpool.StartupService(whirlpool);
Whirlpool.StartupService startupService = whirlpool.createStartupService();
startupService.setPeriod(Duration.minutes(2));
startupService.setOnSucceeded(workerStateEvent -> {
startupService.cancel();
});
startupService.setOnFailed(workerStateEvent -> {
log.error("Failed to start whirlpool", workerStateEvent.getSource().getException());
});
@@ -123,6 +131,27 @@ public class WhirlpoolServices {
return whirlpoolMap.values().stream().filter(whirlpool -> walletId.equals(whirlpool.getMixToWalletId())).findFirst().orElse(null);
}
public static boolean canWalletMix(Wallet wallet) {
return Whirlpool.WHIRLPOOL_NETWORKS.contains(Network.get())
&& wallet.getKeystores().size() == 1
&& wallet.getKeystores().get(0).hasSeed()
&& wallet.getKeystores().get(0).getSeed().getType() == DeterministicSeed.Type.BIP39
&& !wallet.isWhirlpoolMixWallet();
}
public static void prepareWhirlpoolWallet(Wallet decryptedWallet, String walletId, Storage storage) {
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(walletId);
whirlpool.setScode(decryptedWallet.getMasterMixConfig().getScode());
whirlpool.setHDWallet(walletId, decryptedWallet);
for(StandardAccount whirlpoolAccount : StandardAccount.WHIRLPOOL_ACCOUNTS) {
if(decryptedWallet.getChildWallet(whirlpoolAccount) == null) {
Wallet childWallet = decryptedWallet.addChildWallet(whirlpoolAccount);
EventManager.get().post(new ChildWalletAddedEvent(storage, decryptedWallet, childWallet));
}
}
}
@Subscribe
public void newConnection(ConnectionEvent event) {
startAllWhirlpool();
@@ -151,11 +180,8 @@ public class WhirlpoolServices {
if(mixFromWhirlpool != null) {
mixFromWhirlpool.setMixToWallet(walletId, AppServices.get().getWallet(mixFromWhirlpool.getWalletId()).getMasterMixConfig().getMinMixes());
if(mixFromWhirlpool.isStarted()) {
Whirlpool.RestartService restartService = new Whirlpool.RestartService(mixFromWhirlpool);
restartService.setOnFailed(workerStateEvent -> {
log.error("Failed to restart whirlpool", workerStateEvent.getSource().getException());
});
restartService.start();
//Will automatically restart
stopWhirlpool(mixFromWhirlpool, false);
}
}
}
@@ -186,11 +212,8 @@ public class WhirlpoolServices {
if(mixToWhirlpool != null && event.getClosedWalletTabData().stream().noneMatch(walletTabData1 -> walletTabData1.getWalletForm().getWalletId().equals(mixToWhirlpool.getWalletId()))) {
mixToWhirlpool.setMixToWallet(null, null);
if(mixToWhirlpool.isStarted()) {
Whirlpool.RestartService restartService = new Whirlpool.RestartService(mixToWhirlpool);
restartService.setOnFailed(workerStateEvent -> {
log.error("Failed to restart whirlpool", workerStateEvent.getSource().getException());
});
restartService.start();
//Will automatically restart
stopWhirlpool(mixToWhirlpool, false);
}
}
}
@@ -18,10 +18,7 @@ import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.protocol.TransactionInput;
import com.sparrowwallet.drongo.protocol.TransactionOutput;
import com.sparrowwallet.drongo.wallet.BlockTransaction;
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletNode;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.NewBlockEvent;
@@ -99,7 +96,7 @@ public class SparrowDataSource extends WalletResponseDataSource {
for(Map.Entry<BlockTransactionHashIndex, WalletNode> utxo : wallet.getWalletUtxos().entrySet()) {
BlockTransaction blockTransaction = wallet.getTransactions().get(utxo.getKey().getHash());
if(blockTransaction != null) {
if(blockTransaction != null && utxo.getKey().getStatus() != Status.FROZEN) {
unspentOutputs.add(Whirlpool.getUnspentOutput(wallet, utxo.getValue(), blockTransaction, (int)utxo.getKey().getIndex()));
}
}
@@ -179,12 +176,15 @@ public class SparrowDataSource extends WalletResponseDataSource {
static Wallet getWallet(String zpub) {
return AppServices.get().getOpenWallets().keySet().stream()
.filter(Wallet::isValid)
.filter(wallet -> {
List<ExtendedKey.Header> headers = ExtendedKey.Header.getHeaders(Network.get());
ExtendedKey.Header header = headers.stream().filter(head -> head.getDefaultScriptType().equals(wallet.getScriptType()) && !head.isPrivateKey()).findFirst().orElse(ExtendedKey.Header.xpub);
ExtendedKey extPubKey = wallet.getKeystores().get(0).getExtendedPublicKey();
return extPubKey.toString(header).equals(zpub);
try {
List<ExtendedKey.Header> headers = ExtendedKey.Header.getHeaders(Network.get());
ExtendedKey.Header header = headers.stream().filter(head -> head.getDefaultScriptType().equals(wallet.getScriptType()) && !head.isPrivateKey()).findFirst().orElse(ExtendedKey.Header.xpub);
ExtendedKey extPubKey = wallet.getKeystores().get(0).getExtendedPublicKey();
return extPubKey.toString(header).equals(zpub);
} catch(Exception e) {
return false;
}
})
.findFirst()
.orElse(null);
@@ -0,0 +1,49 @@
package com.sparrowwallet.sparrow.whirlpool.tor;
import com.google.common.net.HostAndPort;
import com.samourai.tor.client.TorClientService;
import com.sparrowwallet.sparrow.net.TorService;
import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.Socket;
public class SparrowTorClientService extends TorClientService {
private static final Logger log = LoggerFactory.getLogger(SparrowTorClientService.class);
private final Whirlpool whirlpool;
public SparrowTorClientService(Whirlpool whirlpool) {
this.whirlpool = whirlpool;
}
@Override
public void changeIdentity() {
HostAndPort proxy = whirlpool.getTorProxy();
if(proxy != null) {
Socket controlSocket = TorService.getControlSocket();
if(controlSocket != null) {
try {
writeNewNym(controlSocket);
} catch(Exception e) {
log.warn("Error sending NEWNYM to " + controlSocket, e);
}
} else {
HostAndPort control = HostAndPort.fromParts(proxy.getHost(), proxy.getPort() + 1);
try(Socket socket = new Socket(control.getHost(), control.getPort())) {
writeNewNym(socket);
} catch(Exception e) {
log.warn("Error connecting to " + control + ", no Tor ControlPort configured?");
}
}
}
}
private void writeNewNym(Socket socket) throws IOException {
log.debug("Sending NEWNYM to " + socket);
socket.getOutputStream().write("AUTHENTICATE \"\"\r\n".getBytes());
socket.getOutputStream().write("SIGNAL NEWNYM\r\n".getBytes());
}
}
@@ -38,13 +38,13 @@
.wallet-subtabs > .tab-header-area .tab {
-fx-pref-height: 50;
-fx-pref-width: 80;
-fx-pref-width: 90;
-fx-alignment: CENTER;
}
.wallet-subtabs > .tab-header-area .tab-label {
-fx-pref-height: 50;
-fx-pref-width: 80;
-fx-pref-width: 90;
-fx-alignment: CENTER;
-fx-translate-x: -6;
}
@@ -100,6 +100,7 @@
<CheckMenuItem fx:id="showTxHex" mnemonicParsing="false" text="Show Transaction Hex" onAction="#showTxHex"/>
<SeparatorMenuItem />
<MenuItem fx:id="minimizeToTray" mnemonicParsing="false" text="Minimize to System Tray" accelerator="Shortcut+Y" onAction="#minimizeToTray"/>
<MenuItem fx:id="lockWallet" mnemonicParsing="false" text="Lock Wallet" accelerator="Shortcut+L" onAction="#lockWallet"/>
<MenuItem fx:id="refreshWallet" mnemonicParsing="false" text="Refresh Wallet" accelerator="Shortcut+R" onAction="#refreshWallet"/>
</items>
</Menu>
@@ -107,6 +108,8 @@
<MenuItem mnemonicParsing="false" text="Sign/Verify Message" accelerator="Shortcut+M" onAction="#signVerifyMessage"/>
<MenuItem fx:id="sendToMany" mnemonicParsing="false" text="Send To Many" onAction="#sendToMany"/>
<MenuItem styleClass="osxHide,windowsHide" mnemonicParsing="false" text="Install Udev Rules" onAction="#installUdevRules"/>
<SeparatorMenuItem />
<CheckMenuItem fx:id="preventSleep" mnemonicParsing="false" text="Prevent Computer Sleep" onAction="#preventSleep"/>
</Menu>
<Menu fx:id="helpMenu" mnemonicParsing="false" text="Help">
<MenuItem mnemonicParsing="false" text="Show Introduction" onAction="#showIntroduction"/>
@@ -164,6 +164,10 @@
-fx-text-fill: rgb(202, 18, 67);
}
.warn-icon {
-fx-text-fill: rgb(238, 210, 2);
}
.root .header-panel {
-fx-background-color: -fx-box-border, derive(-fx-background, 10%);
}
@@ -49,3 +49,6 @@
-fx-text-fill: linear-gradient(to bottom, derive(#0b99c9, 30%), #0b99c9);
}
#electrumUseSsl {
-fx-padding: 4 0 2 0;
}
@@ -144,11 +144,12 @@
<TextField fx:id="coreUser"/>
<PasswordField fx:id="corePass"/>
</Field>
<Field text="Multi-Wallet:">
<UnlabeledToggleSwitch fx:id="coreMultiWallet"/> <HelpLabel helpText="Creates a new Bitcoin Core wallet with the following name (recommended to avoid conflicts)" />
<Field text="Use Proxy:">
<UnlabeledToggleSwitch fx:id="coreUseProxy"/><HelpLabel helpText="Bitcoin Core RPC onion URLs, and all other non-RPC external addresses will be connected via this proxy if configured." />
</Field>
<Field text="Wallet Name:" styleClass="label-button">
<TextField fx:id="coreWallet"/>
<Field text="Proxy URL:">
<TextField fx:id="coreProxyHost" />
<TextField fx:id="coreProxyPort" maxWidth="100" />
</Field>
</Fieldset>
</Form>
@@ -171,7 +172,7 @@
</Button>
</Field>
<Field text="Use Proxy:">
<UnlabeledToggleSwitch fx:id="useProxy"/>
<UnlabeledToggleSwitch fx:id="useProxy"/><HelpLabel helpText="All external addresses will be connected via this proxy if configured." />
</Field>
<Field text="Proxy URL:">
<TextField fx:id="proxyHost" />
@@ -88,7 +88,7 @@
<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 text="Future block specified - transaction cannot be broadcast until this block height"/>
</tooltip>
</Label>
</Field>
@@ -99,7 +99,7 @@
<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 text="Future date specified - transaction cannot be broadcast until this date"/>
</tooltip>
</Label>
</Field>
@@ -123,6 +123,7 @@
</padding>
<HBox AnchorPane.leftAnchor="0" spacing="20">
<Button fx:id="export" text="Export..." onAction="#exportWallet" />
<Button fx:id="addAccount" text="Add Account..." onAction="#addAccount" />
</HBox>
<HBox AnchorPane.rightAnchor="10" spacing="20">
<Button text="Advanced..." onAction="#showAdvanced" />
@@ -10,7 +10,7 @@
<BorderPane stylesheets="@wallet.css, @../general.css" xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml" fx:controller="com.sparrowwallet.sparrow.wallet.WalletController">
<left>
<VBox styleClass="list-menu">
<VBox fx:id="walletMenuBox" styleClass="list-menu">
<ToggleButton VBox.vgrow="ALWAYS" text="Transactions" contentDisplay="TOP" styleClass="list-item" maxHeight="Infinity">
<toggleGroup>
<ToggleGroup fx:id="walletMenu" />
Binary file not shown.
Binary file not shown.
Binary file not shown.