mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2026-07-30 19:46:16 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ebd629db3a | ||
|
|
c18a2f4388 | ||
|
|
6f95dbe309 | ||
|
|
576253e651 | ||
|
|
b9d6cb17d4 | ||
|
|
3b730a1711 | ||
|
|
8c0a1932cf | ||
|
|
35b57f9d69 | ||
|
|
d6ad7f4808 | ||
|
|
3e47a49f49 | ||
|
|
4c817d243d | ||
|
|
22b7b659f3 | ||
|
|
9dd6068e69 | ||
|
|
54baee57e1 | ||
|
|
581aaf288e | ||
|
|
2fa47e640d | ||
|
|
ea03dece72 | ||
|
|
8e0b9a3ea0 | ||
|
|
67179127e3 | ||
|
|
4ebee8a8f3 | ||
|
|
2548e77d90 | ||
|
|
58e3b9dcdd | ||
|
|
429b733140 | ||
|
|
56e3a54ae0 | ||
|
|
a934ffa76c | ||
|
|
86a49e0d9a | ||
|
|
761e9c9b7e | ||
|
|
712241873f | ||
|
|
31f9cca33a | ||
|
|
395e90e2a5 |
+3
-3
@@ -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')
|
||||
|
||||
@@ -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,5 +1,6 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
cd `dirname $0`
|
||||
args="$*"
|
||||
args="${args%"${args##*[![:space:]]}"}"
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+25
-2
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+11
-26
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+10
-10
@@ -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.
Reference in New Issue
Block a user