Compare commits

...
36 Commits
Author SHA1 Message Date
Craig Raw 83a7f5c562 v1.1.0 2021-02-11 11:41:37 +02:00
Craig Raw 55e0648fc8 fix help text to match updated cobo firmware 2021-02-11 11:16:51 +02:00
Craig Raw b517b575da add custom dmg installer background 2021-02-11 11:15:38 +02:00
Craig Raw 890b4098d7 add refresh wallet menu item 2021-02-10 10:23:42 +02:00
Craig Raw 11a0e3765b avoid losing tx, address and txo labels when performing a full wallet refresh 2021-02-09 17:32:36 +02:00
Craig Raw 3ffb09c5e6 fix java version 2021-02-08 19:58:03 +02:00
Craig Raw 622b681109 add blue wallet vault import and export and add wallet export via qr 2021-02-08 16:29:59 +02:00
Craig Raw c27a576b3d make passphrase entry a masked password field 2021-02-08 12:42:46 +02:00
Craig Raw 7bc8bdc13e provide help tooltips on policy types and script types 2021-02-08 12:31:34 +02:00
Craig Raw 0097a1ecbc also show legacy qr display for imported keystores 2021-02-08 09:36:24 +02:00
Craig Raw 525d57ec73 handle rbf fee increase for consolidation outputs 2021-02-04 12:14:42 +02:00
Craig Raw e3d7bb57ee move internal tor to a top level app service, support non-ssl proxying 2021-02-04 12:00:49 +02:00
Craig Raw 095518e858 fix bitcoin core cookie dir not present npe 2021-01-28 12:45:12 +02:00
Craig Raw ac470f64c1 revert dynamic tx detail pane 2021-01-28 12:34:32 +02:00
Craig Raw 30215b0dac dynamically resize components on window height change 2021-01-28 12:29:20 +02:00
Craig Raw 563252c823 v1.0.0 2021-01-28 11:51:38 +02:00
Craig Raw 3a7ffe95e8 fix core wallet field - disabled when connected 2021-01-28 11:41:28 +02:00
Craig Raw b588a4ee02 always show scan time remaining 2021-01-28 11:19:57 +02:00
Craig Raw ab0611fabc update bwt to latest HEAD 2021-01-28 11:02:19 +02:00
Craig Raw a121bb5f26 add passport hww single and multisig importing 2021-01-28 10:43:54 +02:00
Craig Raw 2fc01e0345 resize on initial scene change 2021-01-27 10:08:47 +02:00
Craig Raw 650c3657a5 set tx hex divider lower when window height is reduced 2021-01-27 09:16:58 +02:00
Craig Raw d836eb515e further reduce min height to 730px on all platforms 2021-01-27 08:57:46 +02:00
Craig Raw 717bc9fec2 set default core wallet name if offline mode is initially selected 2021-01-26 18:28:07 +02:00
Craig Raw 1ad6da86b1 reduce tx diagram height to fit min resolution of 768px 2021-01-26 18:25:34 +02:00
Craig Raw 54680a5692 fix bug with trezor/keepkey keystore import, show hwi errors on all related dialogs 2021-01-26 14:39:49 +02:00
Craig Raw 6ef5e79b39 greater min window height on windows and linux 2021-01-26 13:23:46 +02:00
Craig Raw 1dbf51b406 reduce min window height by 50px 2021-01-26 12:19:25 +02:00
Craig Raw 8a3aa0d06e revert to java 14.0.2 on windows due to https://bugs.openjdk.java.net/browse/JDK-8254920 2021-01-25 16:55:21 +02:00
Craig Raw ca99efa215 only open mac-specific module on mac build 2021-01-25 15:54:12 +02:00
Craig Raw cc202ce4ca minor ui tweaks 2021-01-25 15:15:10 +02:00
Craig Raw ad44fe2335 use java major version only 2021-01-25 14:44:38 +02:00
Craig Raw 452242f208 upgrade github actions jdk to 15.0.1.hs.adpt 2021-01-25 14:41:33 +02:00
Craig Raw ff83af2e0d add hd capture toggle for qr scanning 2021-01-25 14:00:17 +02:00
Craig Raw aacecc8517 initialize bwt only on connection, default to core multi-wallet use 2021-01-20 13:03:47 +02:00
Craig Raw a9a3eef157 truncate long electrum wallet labels 2021-01-18 13:39:12 +02:00
71 changed files with 1038 additions and 318 deletions
+6
View File
@@ -12,7 +12,13 @@ jobs:
- uses: actions/checkout@v2
with:
submodules: true
- name: Set up JDK 15.0.2
if: ${{ runner.os == 'Linux' }}
uses: joschi/setup-jdk@v2
with:
java-version: 15
- name: Set up JDK 14.0.2
if: ${{ runner.os == 'Windows' }}
uses: actions/setup-java@v1
with:
java-version: 14.0.2
+1 -1
View File
@@ -28,7 +28,7 @@ If you prefer to run Sparrow directly from source, it can be launched with
`./sparrow`
Java 14 must be installed.
Java 14 or higher must be installed.
## Configuration
+6 -3
View File
@@ -5,7 +5,7 @@ plugins {
id 'org.beryx.jlink' version '2.22.0'
}
def sparrowVersion = '0.9.10'
def sparrowVersion = '1.1.0'
def os = org.gradle.internal.os.OperatingSystem.current()
def osName = os.getFamilyName()
if(os.macOsX) {
@@ -69,7 +69,7 @@ dependencies {
exclude group: 'org.openjfx', module: 'javafx-web'
exclude group: 'org.openjfx', module: 'javafx-media'
}
implementation('dev.bwt:bwt-jni:0.1.6')
implementation('dev.bwt:bwt-jni:0.1.7')
testImplementation('junit:junit:4.12')
}
@@ -145,10 +145,13 @@ jlink {
"--add-opens=javafx.controls/com.sun.javafx.scene.control=com.sparrowwallet.merged.module",
"--add-opens=javafx.graphics/com.sun.javafx.menu=com.sparrowwallet.merged.module",
"--add-opens=javafx.graphics/com.sun.glass.ui=com.sparrowwallet.sparrow",
"--add-opens=javafx.graphics/com.sun.glass.ui.mac=com.sparrowwallet.merged.module",
"--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow",
"--add-opens=java.base/java.net=com.sparrowwallet.sparrow",
"--add-reads=com.sparrowwallet.merged.module=java.desktop"]
if(os.macOsX) {
jvmArgs += "--add-opens=javafx.graphics/com.sun.glass.ui.mac=com.sparrowwallet.merged.module"
}
}
addExtraDependencies("javafx")
jpackage {
+1 -1
Submodule drongo updated: 3e91bdb46c...3882a4b4bd
+2 -2
View File
@@ -21,7 +21,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.9.10</string>
<string>1.1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories -->
@@ -30,7 +30,7 @@
<key>CFBundleVersion</key>
<string>100</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright (C) 2020</string>
<string>Copyright (C) 2021</string>
<key>NSHighResolutionCapable</key>
<string>true</string>
<key>NSCameraUsageDescription</key>
Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

@@ -33,6 +33,7 @@ import com.sparrowwallet.sparrow.wallet.WalletForm;
import de.codecentric.centerdevice.MenuToolkit;
import javafx.animation.*;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.collections.ListChangeListener;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
@@ -99,6 +100,9 @@ public class AppController implements Initializable {
@FXML
private CheckMenuItem showTxHex;
@FXML
private MenuItem refreshWallet;
@FXML
private StackPane rootStack;
@@ -223,6 +227,7 @@ public class AppController implements Initializable {
hideEmptyUsedAddresses.setSelected(Config.get().isHideEmptyUsedAddresses());
showTxHex.setSelected(Config.get().isShowTransactionHex());
exportWallet.setDisable(true);
refreshWallet.disableProperty().bind(Bindings.or(exportWallet.disableProperty(), Bindings.or(serverToggle.disableProperty(), AppServices.onlineProperty().not())));
setServerType(Config.get().getServerType());
serverToggle.setSelected(isConnected());
@@ -686,7 +691,11 @@ public class AppController implements Initializable {
}
private boolean attemptImportWallet(File file, SecureString password) {
List<WalletImport> walletImporters = List.of(new ColdcardSinglesig(), new ColdcardMultisig(), new Electrum(), new SpecterDesktop(), new CoboVaultSinglesig(), new CoboVaultMultisig());
List<WalletImport> walletImporters = List.of(new ColdcardSinglesig(), new ColdcardMultisig(),
new Electrum(),
new SpecterDesktop(),
new CoboVaultSinglesig(), new CoboVaultMultisig(),
new PassportSinglesig());
for(WalletImport importer : walletImporters) {
try(FileInputStream inputStream = new FileInputStream(file)) {
if(importer.isEncrypted(file) && password == null) {
@@ -824,6 +833,18 @@ public class AppController implements Initializable {
messageSignDialog.showAndWait();
}
public void refreshWallet(ActionEvent event) {
Tab selectedTab = tabs.getSelectionModel().getSelectedItem();
TabData tabData = (TabData)selectedTab.getUserData();
if(tabData.getType() == TabData.TabType.WALLET) {
WalletTabData walletTabData = (WalletTabData) tabData;
Wallet wallet = walletTabData.getWallet();
Wallet pastWallet = wallet.copy();
wallet.clearHistory();
EventManager.get().post(new WalletSettingsChangedEvent(wallet, pastWallet, walletTabData.getStorage().getWalletFile()));
}
}
public void addWalletTabOrWindow(Storage storage, Wallet wallet, boolean forceSameWindow) {
Window existingWalletWindow = AppServices.get().getWindowForWallet(storage);
if(existingWalletWindow instanceof Stage) {
@@ -1269,12 +1290,43 @@ public class AppController implements Initializable {
}
}
@Subscribe
public void connectionStart(ConnectionStartEvent event) {
statusUpdated(new StatusEvent(event.getStatus(), 120));
}
@Subscribe
public void connectionFailed(ConnectionFailedEvent event) {
String reason = event.getException().getCause() != null ? event.getException().getCause().getMessage() : event.getException().getMessage();
String status = "Connection error: " + reason;
statusUpdated(new StatusEvent(status));
}
@Subscribe
public void connection(ConnectionEvent event) {
String status = "Connected to " + Config.get().getServerAddress() + " at height " + event.getBlockHeight();
statusUpdated(new StatusEvent(status));
}
@Subscribe
public void disconnection(DisconnectionEvent event) {
serverToggle.setDisable(false);
if(!AppServices.isConnecting() && !AppServices.isConnected() && !statusBar.getText().startsWith("Connection error")) {
statusUpdated(new StatusEvent("Disconnected"));
}
if(statusTimeline == null || statusTimeline.getStatus() != Animation.Status.RUNNING) {
statusBar.setProgress(0);
}
}
@Subscribe
public void bwtBootStatus(BwtBootStatusEvent event) {
serverToggle.setDisable(true);
statusUpdated(new StatusEvent(event.getStatus(), 60));
if(statusTimeline == null || statusTimeline.getStatus() != Animation.Status.RUNNING) {
statusBar.setProgress(0.01);
if(AppServices.isConnecting()) {
statusUpdated(new StatusEvent(event.getStatus(), 60));
if(statusTimeline == null || statusTimeline.getStatus() != Animation.Status.RUNNING) {
statusBar.setProgress(0.01);
}
}
}
@@ -1293,7 +1345,7 @@ public class AppController implements Initializable {
public void bwtScanStatus(BwtScanStatusEvent event) {
serverToggle.setDisable(true);
if((AppServices.isConnecting() || AppServices.isConnected()) && !event.isCompleted()) {
statusUpdated(new StatusEvent("Scanning... (" + event.getProgress() + "% complete" + (event.getProgress() > 30 ? ", " + event.getRemainingAsString() + " remaining)" : ")")));
statusUpdated(new StatusEvent("Scanning... (" + event.getProgress() + "% complete, " + event.getRemainingAsString() + " remaining)"));
if(event.getProgress() > 0 && (statusTimeline == null || statusTimeline.getStatus() != Animation.Status.RUNNING)) {
statusBar.setProgress((double)event.getProgress() / 100);
}
@@ -1309,14 +1361,21 @@ public class AppController implements Initializable {
}
@Subscribe
public void disconnection(DisconnectionEvent event) {
public void torBootStatus(TorBootStatusEvent event) {
serverToggle.setDisable(true);
statusUpdated(new StatusEvent(event.getStatus(), 120));
}
@Subscribe
public void torFailedStatus(TorFailedStatusEvent event) {
serverToggle.setDisable(false);
if(!AppServices.isConnecting() && !AppServices.isConnected() && !statusBar.getText().startsWith("Connection error")) {
statusUpdated(new StatusEvent("Disconnected"));
}
if(statusTimeline == null || statusTimeline.getStatus() != Animation.Status.RUNNING) {
statusBar.setProgress(0);
}
statusUpdated(new StatusEvent(event.getStatus()));
}
@Subscribe
public void torReadyStatus(TorReadyStatusEvent event) {
serverToggle.setDisable(false);
statusUpdated(new StatusEvent(event.getStatus()));
}
@Subscribe
@@ -1,6 +1,7 @@
package com.sparrowwallet.sparrow;
import com.google.common.eventbus.Subscribe;
import com.google.common.net.HostAndPort;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.psbt.PSBT;
@@ -21,6 +22,7 @@ import javafx.beans.value.ObservableValue;
import javafx.concurrent.ScheduledService;
import javafx.concurrent.Worker;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
@@ -30,11 +32,14 @@ import javafx.scene.text.Font;
import javafx.stage.Stage;
import javafx.stage.Window;
import javafx.util.Duration;
import org.berndpruenster.netlayer.tor.Tor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
@@ -44,9 +49,9 @@ import java.util.stream.Collectors;
public class AppServices {
private static final Logger log = LoggerFactory.getLogger(AppServices.class);
private static final int SERVER_PING_PERIOD = 1 * 60 * 1000;
private static final int ENUMERATE_HW_PERIOD = 30 * 1000;
private static final int RATES_PERIOD = 5 * 60 * 1000;
private static final int SERVER_PING_PERIOD_SECS = 60;
private static final int ENUMERATE_HW_PERIOD_SECS = 30;
private static final int RATES_PERIOD_SECS = 5 * 60;
private static final int VERSION_CHECK_PERIOD_HOURS = 24;
private static final ExchangeSource DEFAULT_EXCHANGE_SOURCE = ExchangeSource.COINGECKO;
private static final Currency DEFAULT_FIAT_CURRENCY = Currency.getInstance("USD");
@@ -67,6 +72,8 @@ public class AppServices {
private VersionCheckService versionCheckService;
private TorService torService;
private static Integer currentBlockHeight;
private static Map<Integer, Double> targetBlockFeeRates;
@@ -85,14 +92,10 @@ public class AppServices {
@Override
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean online) {
if(online) {
restartService(connectionService);
if(ratesService.getExchangeSource() != ExchangeSource.NONE) {
restartService(ratesService);
}
if(Config.get().isCheckNewVersions()) {
restartService(versionCheckService);
if(Config.get().requiresTor() && !isTorRunning()) {
torService.start();
} else {
restartServices();
}
} else {
connectionService.cancel();
@@ -102,6 +105,44 @@ public class AppServices {
}
};
public AppServices(MainApp application) {
this.application = application;
EventManager.get().register(this);
}
public void start() {
Config config = Config.get();
connectionService = createConnectionService();
ratesService = createRatesService(config.getExchangeSource(), config.getFiatCurrency());
versionCheckService = createVersionCheckService();
torService = createTorService();
onlineProperty.addListener(onlineServicesListener);
if(config.getMode() == Mode.ONLINE) {
if(config.requiresTor()) {
torService.start();
} else {
restartServices();
}
}
}
private void restartServices() {
Config config = Config.get();
if(config.hasServerAddress()) {
restartService(connectionService);
}
if(config.isFetchRates()) {
restartService(ratesService);
}
if(config.isCheckNewVersions()) {
restartService(versionCheckService);
}
}
private void restartService(ScheduledService<?> service) {
if(service.isRunning()) {
service.cancel();
@@ -116,33 +157,6 @@ public class AppServices {
}
}
public AppServices(MainApp application) {
this.application = application;
EventManager.get().register(this);
}
public void start() {
Config config = Config.get();
connectionService = createConnectionService();
if(config.getMode() == Mode.ONLINE && config.getServerAddress() != null && !config.getServerAddress().isEmpty()) {
connectionService.start();
}
ExchangeSource source = config.getExchangeSource() != null ? config.getExchangeSource() : DEFAULT_EXCHANGE_SOURCE;
Currency currency = config.getFiatCurrency() != null ? config.getFiatCurrency() : DEFAULT_FIAT_CURRENCY;
ratesService = createRatesService(source, currency);
if(config.getMode() == Mode.ONLINE && source != ExchangeSource.NONE) {
ratesService.start();
}
versionCheckService = createVersionCheckService();
if(config.getMode() == Mode.ONLINE && config.isCheckNewVersions()) {
versionCheckService.start();
}
onlineProperty.addListener(onlineServicesListener);
}
public void stop() {
if(connectionService != null) {
connectionService.cancel();
@@ -155,20 +169,23 @@ public class AppServices {
if(versionCheckService != null) {
versionCheckService.cancel();
}
if(Tor.getDefault() != null) {
Tor.getDefault().shutdown();
}
}
private ElectrumServer.ConnectionService createConnectionService() {
ElectrumServer.ConnectionService connectionService = new ElectrumServer.ConnectionService();
connectionService.setPeriod(new Duration(SERVER_PING_PERIOD));
connectionService.setPeriod(Duration.seconds(SERVER_PING_PERIOD_SECS));
connectionService.setRestartOnFailure(true);
EventManager.get().register(connectionService);
connectionService.statusProperty().addListener((observable, oldValue, newValue) -> {
if(connectionService.isRunning()) {
EventManager.get().post(new StatusEvent(newValue));
connectionService.setOnRunning(workerStateEvent -> {
if(!ElectrumServer.isConnected()) {
EventManager.get().post(new ConnectionStartEvent(Config.get().getServerAddress()));
}
});
connectionService.setOnSucceeded(successEvent -> {
connectionService.setRestartOnFailure(true);
@@ -200,8 +217,10 @@ public class AppServices {
}
private ExchangeSource.RatesService createRatesService(ExchangeSource exchangeSource, Currency currency) {
ExchangeSource.RatesService ratesService = new ExchangeSource.RatesService(exchangeSource, currency);
ratesService.setPeriod(new Duration(RATES_PERIOD));
ExchangeSource.RatesService ratesService = new ExchangeSource.RatesService(
exchangeSource == null ? DEFAULT_EXCHANGE_SOURCE : exchangeSource,
currency == null ? DEFAULT_FIAT_CURRENCY : currency);
ratesService.setPeriod(Duration.seconds(RATES_PERIOD_SECS));
ratesService.setRestartOnFailure(true);
ratesService.setOnSucceeded(successEvent -> {
@@ -229,7 +248,7 @@ public class AppServices {
private Hwi.ScheduledEnumerateService createDeviceEnumerateService() {
Hwi.ScheduledEnumerateService enumerateService = new Hwi.ScheduledEnumerateService(null);
enumerateService.setPeriod(new Duration(ENUMERATE_HW_PERIOD));
enumerateService.setPeriod(Duration.seconds(ENUMERATE_HW_PERIOD_SECS));
enumerateService.setOnSucceeded(workerStateEvent -> {
List<Device> devices = enumerateService.getValue();
@@ -246,6 +265,45 @@ public class AppServices {
return enumerateService;
}
private TorService createTorService() {
TorService torService = new TorService();
torService.setPeriod(Duration.hours(1000));
torService.setRestartOnFailure(true);
torService.setOnRunning(workerStateEvent -> {
EventManager.get().post(new TorBootStatusEvent());
});
torService.setOnSucceeded(workerStateEvent -> {
Tor.setDefault(torService.getValue());
torService.cancel();
restartServices();
EventManager.get().post(new TorReadyStatusEvent());
});
torService.setOnFailed(workerStateEvent -> {
EventManager.get().post(new TorFailedStatusEvent(workerStateEvent.getSource().getException()));
});
return torService;
}
public static boolean isTorRunning() {
return Tor.getDefault() != null;
}
public static Proxy getProxy() {
Config config = Config.get();
if(config.isUseProxy()) {
HostAndPort proxy = HostAndPort.fromString(config.getProxyServer());
InetSocketAddress proxyAddress = new InetSocketAddress(proxy.getHost(), proxy.getPortOrDefault(ProxyTcpOverTlsTransport.DEFAULT_PROXY_PORT));
return new Proxy(Proxy.Type.SOCKS, proxyAddress);
} else if(AppServices.isTorRunning()) {
InetSocketAddress proxyAddress = new InetSocketAddress("localhost", TorService.PROXY_PORT);
return new Proxy(Proxy.Type.SOCKS, proxyAddress);
}
return null;
}
static void initialize(MainApp application) {
INSTANCE = new AppServices(application);
}
@@ -265,7 +323,7 @@ public class AppServices {
stage.setTitle("Sparrow");
stage.setMinWidth(650);
stage.setMinHeight(800);
stage.setMinHeight(730);
stage.setScene(scene);
stage.getIcons().add(new Image(MainApp.class.getResourceAsStream("/image/sparrow.png")));
@@ -278,6 +336,10 @@ public class AppServices {
}
}
public static boolean isReducedWindowHeight(Node node) {
return (node.getScene() != null && node.getScene().getWindow().getHeight() < 768);
}
public MainApp getApplication() {
return application;
}
@@ -399,16 +461,6 @@ public class AppServices {
targetBlockFeeRates = event.getTargetBlockFeeRates();
addMempoolRateSizes(event.getMempoolRateSizes());
minimumRelayFeeRate = event.getMinimumRelayFeeRate();
String banner = event.getServerBanner();
String status = "Connected to " + Config.get().getServerAddress() + " at height " + event.getBlockHeight();
EventManager.get().post(new StatusEvent(status));
}
@Subscribe
public void connectionFailed(ConnectionFailedEvent event) {
String reason = event.getException().getCause() != null ? event.getException().getCause().getMessage() : event.getException().getMessage();
String status = "Connection error: " + reason;
EventManager.get().post(new StatusEvent(status));
}
@Subscribe
@@ -9,6 +9,7 @@ import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.io.FileType;
import com.sparrowwallet.sparrow.io.IOUtils;
import com.sparrowwallet.sparrow.io.Storage;
import com.sparrowwallet.sparrow.net.Bwt;
import com.sparrowwallet.sparrow.net.ServerType;
import com.sparrowwallet.sparrow.preferences.PreferenceGroup;
import com.sparrowwallet.sparrow.preferences.PreferencesDialog;
@@ -25,7 +26,7 @@ import java.util.stream.Collectors;
public class MainApp extends Application {
public static final String APP_NAME = "Sparrow";
public static final String APP_VERSION = "0.9.10";
public static final String APP_VERSION = "1.1.0";
public static final String APP_HOME_PROPERTY = "sparrow.home";
public static final String NETWORK_ENV_PROPERTY = "SPARROW_NETWORK";
@@ -55,6 +56,7 @@ 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);
@@ -66,6 +68,9 @@ 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("");
}
AppController appController = AppServices.newAppWindow(stage);
@@ -84,6 +84,7 @@ public class CoinTreeTable extends TreeTableView<Entry> {
WalletBirthDateDialog dlg = new WalletBirthDateDialog(wallet.getBirthDate());
Optional<Date> optDate = dlg.showAndWait();
if(optDate.isPresent()) {
Wallet pastWallet = wallet.copy();
wallet.setBirthDate(optDate.get());
Storage storage = AppServices.get().getOpenWallets().get(wallet);
if(storage != null) {
@@ -91,7 +92,7 @@ public class CoinTreeTable extends TreeTableView<Entry> {
EventManager.get().post(new WalletDataChangedEvent(wallet));
//Trigger full wallet rescan
wallet.clearHistory();
EventManager.get().post(new WalletSettingsChangedEvent(wallet, storage.getWalletFile()));
EventManager.get().post(new WalletSettingsChangedEvent(wallet, pastWallet, storage.getWalletFile()));
}
}
});
@@ -89,6 +89,8 @@ public abstract class DeviceDialog<R> extends Dialog<R> {
dialogPane.setPrefWidth(500);
dialogPane.setPrefHeight(360);
setResultConverter(dialogButton -> dialogButton == cancelButtonType ? null : getResult());
}
private void scan() {
@@ -73,18 +73,7 @@ public class DevicePane extends TitledDescriptionPane {
createSetPassphraseButton();
createImportButton();
if(device.getError() != null) {
setError("Error", device.getError());
Platform.runLater(() -> {
setExpanded(true);
});
} else if(device.getNeedsPinSent() != null && device.getNeedsPinSent()) {
unlockButton.setVisible(true);
} else if(device.getNeedsPassphraseSent() != null && device.getNeedsPassphraseSent()) {
setPassphraseButton.setVisible(true);
} else {
showOperationButton();
}
initialise(device);
buttonBox.getChildren().addAll(setPassphraseButton, importButton);
}
@@ -104,13 +93,7 @@ public class DevicePane extends TitledDescriptionPane {
createSetPassphraseButton();
createSignButton();
if (device.getNeedsPinSent() != null && device.getNeedsPinSent()) {
unlockButton.setVisible(true);
} else if(device.getNeedsPassphraseSent() != null && device.getNeedsPassphraseSent()) {
setPassphraseButton.setVisible(true);
} else {
showOperationButton();
}
initialise(device);
buttonBox.getChildren().addAll(setPassphraseButton, signButton);
}
@@ -130,13 +113,7 @@ public class DevicePane extends TitledDescriptionPane {
createSetPassphraseButton();
createDisplayAddressButton();
if (device.getNeedsPinSent() != null && device.getNeedsPinSent()) {
unlockButton.setVisible(true);
} else if(device.getNeedsPassphraseSent() != null && device.getNeedsPassphraseSent()) {
setPassphraseButton.setVisible(true);
} else {
showOperationButton();
}
initialise(device);
buttonBox.getChildren().addAll(setPassphraseButton, displayAddressButton);
}
@@ -156,15 +133,24 @@ public class DevicePane extends TitledDescriptionPane {
createSetPassphraseButton();
createSignMessageButton();
if (device.getNeedsPinSent() != null && device.getNeedsPinSent()) {
initialise(device);
buttonBox.getChildren().addAll(setPassphraseButton, signMessageButton);
}
private void initialise(Device device) {
if(device.getNeedsPinSent() != null && device.getNeedsPinSent()) {
unlockButton.setVisible(true);
} else if(device.getNeedsPassphraseSent() != null && device.getNeedsPassphraseSent()) {
setPassphraseButton.setVisible(true);
} else if(device.getError() != null) {
setError("Error", device.getError());
Platform.runLater(() -> {
setExpanded(true);
});
} else {
showOperationButton();
}
buttonBox.getChildren().addAll(setPassphraseButton, signMessageButton);
}
@Override
@@ -316,7 +302,7 @@ public class DevicePane extends TitledDescriptionPane {
}
private Node getPassphraseEntry() {
CustomTextField passphraseField = (CustomTextField)TextFields.createClearableTextField();
CustomPasswordField passphraseField = (CustomPasswordField)TextFields.createClearablePasswordField();
passphrase.bind(passphraseField.textProperty());
HBox.setHgrow(passphraseField, Priority.ALWAYS);
@@ -1,5 +1,6 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.protocol.Transaction;
@@ -173,6 +174,13 @@ class EntryCell extends TreeTableCell<Entry, Entry> {
.map(e -> e.getBlockTransaction().getTransaction().getOutputs().get((int)e.getHashIndex().getIndex()))
.collect(Collectors.toList());
List<TransactionOutput> consolidationOutputs = transactionEntry.getChildren().stream()
.filter(e -> e instanceof HashIndexEntry)
.map(e -> (HashIndexEntry)e)
.filter(e -> e.getType().equals(HashIndexEntry.Type.OUTPUT) && e.getKeyPurpose() == KeyPurpose.RECEIVE)
.map(e -> e.getBlockTransaction().getTransaction().getOutputs().get((int)e.getHashIndex().getIndex()))
.collect(Collectors.toList());
long changeTotal = ourOutputs.stream().mapToLong(TransactionOutput::getValue).sum();
Transaction tx = blockTransaction.getTransaction();
int vSize = tx.getVirtualSize();
@@ -189,6 +197,7 @@ class EntryCell extends TreeTableCell<Entry, Entry> {
List<TransactionOutput> externalOutputs = new ArrayList<>(blockTransaction.getTransaction().getOutputs());
externalOutputs.removeAll(ourOutputs);
externalOutputs.addAll(consolidationOutputs);
List<Payment> payments = externalOutputs.stream().map(txOutput -> {
try {
return new Payment(txOutput.getScript().getToAddresses()[0], transactionEntry.getLabel(), txOutput.getValue(), false);
@@ -2,44 +2,81 @@ package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.SecureString;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.hummingbird.registry.RegistryType;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.StorageEvent;
import com.sparrowwallet.sparrow.event.TimedEvent;
import com.sparrowwallet.sparrow.event.WalletExportEvent;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.CoboVaultMultisig;
import com.sparrowwallet.sparrow.io.Storage;
import com.sparrowwallet.sparrow.io.WalletExport;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.Control;
import javafx.scene.control.ToggleButton;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import org.controlsfx.control.SegmentedButton;
import org.controlsfx.glyphfont.Glyph;
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.Optional;
public class FileWalletExportPane extends TitledDescriptionPane {
private final Wallet wallet;
private final WalletExport exporter;
private final boolean scannable;
public FileWalletExportPane(Wallet wallet, WalletExport exporter) {
super(exporter.getName(), "Wallet file export", exporter.getWalletExportDescription(), "image/" + exporter.getWalletModel().getType() + ".png");
this.wallet = wallet;
this.exporter = exporter;
this.scannable = exporter.isWalletExportScannable();
buttonBox.getChildren().clear();
buttonBox.getChildren().add(createButton());
}
@Override
protected Control createButton() {
Button exportButton = new Button("Export Wallet...");
exportButton.setAlignment(Pos.CENTER_RIGHT);
exportButton.setOnAction(event -> {
exportWallet();
});
return exportButton;
if(scannable) {
ToggleButton showButton = new ToggleButton("Show...");
Glyph cameraGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CAMERA);
cameraGlyph.setFontSize(12);
showButton.setGraphic(cameraGlyph);
showButton.setOnAction(event -> {
showButton.setSelected(false);
exportQR();
});
ToggleButton fileButton = new ToggleButton("Export File...");
fileButton.setAlignment(Pos.CENTER_RIGHT);
fileButton.setOnAction(event -> {
fileButton.setSelected(false);
exportFile();
});
SegmentedButton segmentedButton = new SegmentedButton();
segmentedButton.getButtons().addAll(showButton, fileButton);
return segmentedButton;
} else {
Button exportButton = new Button("Export File...");
exportButton.setAlignment(Pos.CENTER_RIGHT);
exportButton.setOnAction(event -> {
exportFile();
});
return exportButton;
}
}
private void exportWallet() {
private void exportQR() {
exportWallet(null);
}
private void exportFile() {
Stage window = new Stage();
FileChooser fileChooser = new FileChooser();
@@ -59,17 +96,18 @@ public class FileWalletExportPane extends TitledDescriptionPane {
WalletPasswordDialog dlg = new WalletPasswordDialog(WalletPasswordDialog.PasswordRequirement.LOAD);
Optional<SecureString> password = dlg.showAndWait();
if(password.isPresent()) {
final File walletFile = AppServices.get().getOpenWallets().get(wallet).getWalletFile();
Storage.DecryptWalletService decryptWalletService = new Storage.DecryptWalletService(copy, password.get());
decryptWalletService.setOnSucceeded(workerStateEvent -> {
EventManager.get().post(new StorageEvent(file, TimedEvent.Action.END, "Done"));
EventManager.get().post(new StorageEvent(walletFile, TimedEvent.Action.END, "Done"));
Wallet decryptedWallet = decryptWalletService.getValue();
exportWallet(file, decryptedWallet);
});
decryptWalletService.setOnFailed(workerStateEvent -> {
EventManager.get().post(new StorageEvent(file, TimedEvent.Action.END, "Failed"));
EventManager.get().post(new StorageEvent(walletFile, TimedEvent.Action.END, "Failed"));
setError("Export Error", decryptWalletService.getException().getMessage());
});
EventManager.get().post(new StorageEvent(file, TimedEvent.Action.START, "Decrypting wallet..."));
EventManager.get().post(new StorageEvent(walletFile, TimedEvent.Action.START, "Decrypting wallet..."));
decryptWalletService.start();
}
} else {
@@ -79,9 +117,21 @@ public class FileWalletExportPane extends TitledDescriptionPane {
private void exportWallet(File file, Wallet decryptedWallet) {
try {
OutputStream outputStream = new FileOutputStream(file);
exporter.exportWallet(decryptedWallet, outputStream);
EventManager.get().post(new WalletExportEvent(decryptedWallet));
if(file != null) {
OutputStream outputStream = new FileOutputStream(file);
exporter.exportWallet(decryptedWallet, outputStream);
EventManager.get().post(new WalletExportEvent(decryptedWallet));
} else {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
exporter.exportWallet(decryptedWallet, outputStream);
QRDisplayDialog qrDisplayDialog;
if(exporter instanceof CoboVaultMultisig) {
qrDisplayDialog = new QRDisplayDialog(RegistryType.BYTES.toString(), outputStream.toByteArray(), true);
} else {
qrDisplayDialog = new QRDisplayDialog(outputStream.toString(StandardCharsets.UTF_8));
}
qrDisplayDialog.showAndWait();
}
} catch(Exception e) {
String errorMessage = e.getMessage();
if(e.getCause() != null && e.getCause().getMessage() != null && !e.getCause().getMessage().isEmpty()) {
@@ -72,6 +72,9 @@ public class FileWalletKeystoreImportPane extends FileImportPane {
ComboBox<ScriptType> scriptTypeComboBox = new ComboBox<>(FXCollections.observableArrayList(ScriptType.getAddressableScriptTypes(PolicyType.SINGLE)));
scriptTypeComboBox.setValue(ScriptType.P2WPKH);
HelpLabel helpLabel = new HelpLabel();
helpLabel.setHelpText("P2WPKH is a Native Segwit type and is usually the best choice for new wallets.\nP2SH-P2WPKH is a Wrapped Segwit type and is a reasonable choice for the widest compatibility.\nP2PKH is a Legacy type and should be avoided for new wallets.\nFor existing wallets, be sure to choose the type that matches the wallet you are importing.");
Region region = new Region();
HBox.setHgrow(region, Priority.SOMETIMES);
@@ -97,7 +100,7 @@ public class FileWalletKeystoreImportPane extends FileImportPane {
HBox contentBox = new HBox();
contentBox.setAlignment(Pos.CENTER_RIGHT);
contentBox.setSpacing(20);
contentBox.getChildren().addAll(label, scriptTypeComboBox, region, importFileButton);
contentBox.getChildren().addAll(label, scriptTypeComboBox, helpLabel, region, importFileButton);
contentBox.setPadding(new Insets(10, 30, 10, 30));
contentBox.setPrefHeight(60);
@@ -5,6 +5,7 @@ import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.scene.control.Label;
import javafx.scene.control.Tooltip;
import javafx.util.Duration;
import org.controlsfx.glyphfont.Glyph;
public class HelpLabel extends Label {
@@ -14,6 +15,7 @@ public class HelpLabel extends Label {
super("", getHelpGlyph());
tooltip = new Tooltip();
tooltip.textProperty().bind(helpTextProperty());
tooltip.setShowDuration(Duration.seconds(15));
setTooltip(tooltip);
getStyleClass().add("help-label");
}
@@ -225,19 +225,15 @@ public class QRDisplayDialog extends Dialog<UR> {
private void setLegacyGraphic(ToggleButton legacy, boolean useLegacyEncoding) {
if(useLegacyEncoding) {
legacy.setGraphic(getGlyph(FontAwesome5.Glyph.CHECK_CIRCLE, "success"));
legacy.setGraphic(getGlyph(FontAwesome5.Glyph.CHECK_CIRCLE));
} else {
legacy.setGraphic(getGlyph(FontAwesome5.Glyph.QUESTION_CIRCLE, null));
legacy.setGraphic(getGlyph(FontAwesome5.Glyph.QUESTION_CIRCLE));
}
}
private Glyph getGlyph(FontAwesome5.Glyph glyphName, String styleClass) {
private Glyph getGlyph(FontAwesome5.Glyph glyphName) {
Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, glyphName);
glyph.setFontSize(12);
if(styleClass != null) {
glyph.getStyleClass().add(styleClass);
}
glyph.setFontSize(11);
return glyph;
}
}
@@ -1,5 +1,7 @@
package com.sparrowwallet.sparrow.control;
import com.github.sarxos.webcam.WebcamEvent;
import com.github.sarxos.webcam.WebcamListener;
import com.github.sarxos.webcam.WebcamResolution;
import com.sparrowwallet.drongo.ExtendedKey;
import com.sparrowwallet.drongo.KeyDerivation;
@@ -23,14 +25,17 @@ import com.sparrowwallet.hummingbird.ResultType;
import com.sparrowwallet.hummingbird.UR;
import com.sparrowwallet.hummingbird.URDecoder;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.Config;
import javafx.application.Platform;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.control.ButtonBar;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Dialog;
import javafx.scene.control.DialogPane;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.layout.StackPane;
import org.controlsfx.glyphfont.Glyph;
import org.controlsfx.tools.Borders;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -40,6 +45,7 @@ import java.nio.CharBuffer;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.IntStream;
@@ -58,14 +64,21 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
private static final Pattern PART_PATTERN = Pattern.compile("p(\\d+)of(\\d+) (.+)");
private final ObjectProperty<WebcamResolution> webcamResolutionProperty = new SimpleObjectProperty<>(WebcamResolution.VGA);
public QRScanDialog() {
this.decoder = new URDecoder();
this.legacyDecoder = new LegacyURDecoder();
this.webcamService = new WebcamService(WebcamResolution.VGA);
if(Config.get().isHdCapture()) {
webcamResolutionProperty.set(WebcamResolution.HD);
}
this.webcamService = new WebcamService(webcamResolutionProperty.get(), new QRScanListener());
WebcamView webcamView = new WebcamView(webcamService);
final DialogPane dialogPane = getDialogPane();
final DialogPane dialogPane = new QRScanDialogPane();
setDialogPane(dialogPane);
AppServices.setStageIcon(dialogPane.getScene().getWindow());
StackPane stackPane = new StackPane();
@@ -78,14 +91,27 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
Platform.runLater(() -> setResult(new Result(failedEvent.getSource().getException())));
});
webcamService.start();
webcamResolutionProperty.addListener((observable, oldValue, newResolution) -> {
if(newResolution != null) {
setHeight(newResolution == WebcamResolution.HD ? (getHeight() - 100) : (getHeight() + 100));
}
webcamService.cancel();
});
setOnCloseRequest(event -> {
Platform.runLater(webcamService::cancel);
boolean isHdCapture = (webcamResolutionProperty.get() == WebcamResolution.HD);
if(Config.get().isHdCapture() != isHdCapture) {
Config.get().setHdCapture(isHdCapture);
}
Platform.runLater(() -> webcamResolutionProperty.set(null));
});
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
dialogPane.getButtonTypes().addAll(cancelButtonType);
dialogPane.setPrefWidth(660);
dialogPane.setPrefHeight(550);
final ButtonType hdButtonType = new javafx.scene.control.ButtonType("Use HD Capture", ButtonBar.ButtonData.LEFT);
dialogPane.getButtonTypes().addAll(hdButtonType, cancelButtonType);
dialogPane.setPrefWidth(646);
dialogPane.setPrefHeight(webcamResolutionProperty.get() == WebcamResolution.HD ? 450 : 550);
setResultConverter(dialogButton -> dialogButton != cancelButtonType ? result : null);
}
@@ -425,6 +451,73 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
}
}
private class QRScanListener implements WebcamListener {
@Override
public void webcamOpen(WebcamEvent webcamEvent) {
}
@Override
public void webcamClosed(WebcamEvent webcamEvent) {
if(webcamResolutionProperty.get() != null) {
webcamService.setResolution(webcamResolutionProperty.get());
Platform.runLater(() -> {
if(!webcamService.isRunning()) {
webcamService.reset();
webcamService.start();
}
});
}
}
@Override
public void webcamDisposed(WebcamEvent webcamEvent) {
}
@Override
public void webcamImageObtained(WebcamEvent webcamEvent) {
}
}
private class QRScanDialogPane extends DialogPane {
@Override
protected Node createButton(ButtonType buttonType) {
if(buttonType.getButtonData() == ButtonBar.ButtonData.LEFT) {
ToggleButton hd = new ToggleButton(buttonType.getText());
hd.setSelected(webcamResolutionProperty.get() == WebcamResolution.HD);
hd.setGraphicTextGap(5);
setHdGraphic(hd, hd.isSelected());
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
ButtonBar.setButtonData(hd, buttonData);
hd.selectedProperty().addListener((observable, oldValue, newValue) -> {
webcamResolutionProperty.set(newValue ? WebcamResolution.HD : WebcamResolution.VGA);
setHdGraphic(hd, newValue);
});
return hd;
}
return super.createButton(buttonType);
}
private void setHdGraphic(ToggleButton hd, boolean isHd) {
if(isHd) {
hd.setGraphic(getGlyph(FontAwesome5.Glyph.CHECK_CIRCLE));
} else {
hd.setGraphic(getGlyph(FontAwesome5.Glyph.QUESTION_CIRCLE));
}
}
private Glyph getGlyph(FontAwesome5.Glyph glyphName) {
Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, glyphName);
glyph.setFontSize(11);
return glyph;
}
}
public static class Result {
public final Transaction transaction;
public final PSBT psbt;
@@ -30,14 +30,17 @@ import java.util.*;
import java.util.stream.Collectors;
public class TransactionDiagram extends GridPane {
private static final int MAX_UTXOS = 8;
private static final int MAX_PAYMENTS = 6;
private static final double DIAGRAM_HEIGHT = 230.0;
private static final int MAX_UTXOS = 7;
private static final int MAX_PAYMENTS = 5;
private static final double DIAGRAM_HEIGHT = 215.0;
private static final int TOOLTIP_SHOW_DELAY = 50;
private WalletTransaction walletTx;
public void update(WalletTransaction walletTx) {
setMinHeight(getDiagramHeight());
setMaxHeight(getDiagramHeight());
if(walletTx == null) {
getChildren().clear();
} else {
@@ -131,7 +134,7 @@ public class TransactionDiagram extends GridPane {
Line topYaxis = new Line();
topYaxis.setStartX(width * 0.5);
topYaxis.setStartY(DIAGRAM_HEIGHT * 0.5 - 20.0);
topYaxis.setStartY(getDiagramHeight() * 0.5 - 20.0);
topYaxis.setEndX(width * 0.5);
topYaxis.setEndY(0);
topYaxis.getStyleClass().add("inputs-type");
@@ -145,16 +148,16 @@ public class TransactionDiagram extends GridPane {
Line bottomYaxis = new Line();
bottomYaxis.setStartX(width * 0.5);
bottomYaxis.setStartY(DIAGRAM_HEIGHT);
bottomYaxis.setStartY(getDiagramHeight());
bottomYaxis.setEndX(width * 0.5);
bottomYaxis.setEndY(DIAGRAM_HEIGHT * 0.5 + 20.0);
bottomYaxis.setEndY(getDiagramHeight() * 0.5 + 20.0);
bottomYaxis.getStyleClass().add("inputs-type");
Line bottomBracket = new Line();
bottomBracket.setStartX(width * 0.5);
bottomBracket.setStartY(DIAGRAM_HEIGHT);
bottomBracket.setStartY(getDiagramHeight());
bottomBracket.setEndX(width);
bottomBracket.setEndY(DIAGRAM_HEIGHT);
bottomBracket.setEndY(getDiagramHeight());
bottomBracket.getStyleClass().add("inputs-type");
group.getChildren().addAll(widthLine, topYaxis, topBracket, bottomYaxis, bottomBracket);
@@ -240,7 +243,7 @@ public class TransactionDiagram extends GridPane {
yaxisLine.setStartX(0);
yaxisLine.setStartY(0);
yaxisLine.setEndX(0);
yaxisLine.setEndY(DIAGRAM_HEIGHT);
yaxisLine.setEndY(getDiagramHeight());
yaxisLine.getStyleClass().add("boundary");
group.getChildren().add(yaxisLine);
@@ -259,9 +262,9 @@ public class TransactionDiagram extends GridPane {
double scaleFactor = (double)i / (numUtxos + 1);
int nodeHeight = 17;
double additional = (0.5 - scaleFactor) * ((double)nodeHeight);
curve.setStartY(scale(DIAGRAM_HEIGHT, scaleFactor, additional));
curve.setStartY(scale(getDiagramHeight(), scaleFactor, additional));
curve.setEndX(width);
curve.setEndY(scale(DIAGRAM_HEIGHT, 0.5, 0));
curve.setEndY(scale(getDiagramHeight(), 0.5, 0));
curve.setControlX1(scale(width, 0.2, 0));
curve.setControlY1(curve.getStartY());
@@ -320,12 +323,12 @@ public class TransactionDiagram extends GridPane {
curve.getStyleClass().add("output-line");
curve.setStartX(0);
curve.setStartY(scale(DIAGRAM_HEIGHT, 0.5, 0));
curve.setStartY(scale(getDiagramHeight(), 0.5, 0));
curve.setEndX(width);
double scaleFactor = (double)i / (numOutputs + 1);
int nodeHeight = 20;
double additional = (0.5 - scaleFactor) * ((double)nodeHeight);
curve.setEndY(scale(DIAGRAM_HEIGHT, scaleFactor, additional));
curve.setEndY(scale(getDiagramHeight(), scaleFactor, additional));
curve.setControlX1(scale(width, 0.2, 0));
curve.controlY1Property().bind(curve.startYProperty());
@@ -404,6 +407,14 @@ public class TransactionDiagram extends GridPane {
return txPane;
}
public double getDiagramHeight() {
if(AppServices.isReducedWindowHeight(this)) {
return DIAGRAM_HEIGHT - 40;
}
return DIAGRAM_HEIGHT;
}
private Node createSpacer() {
final Region spacer = new Region();
VBox.setVgrow(spacer, Priority.ALWAYS);
@@ -43,7 +43,7 @@ public class WalletExportDialog extends Dialog<Wallet> {
if(wallet.getPolicyType() == PolicyType.SINGLE) {
exporters = List.of(new Electrum(), new SpecterDesktop());
} else if(wallet.getPolicyType() == PolicyType.MULTI) {
exporters = List.of(new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new SpecterDesktop());
exporters = List.of(new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new SpecterDesktop(), new BlueWalletMultisig());
} else {
throw new UnsupportedOperationException("Cannot export wallet with policy type " + wallet.getPolicyType());
}
@@ -39,13 +39,13 @@ public class WalletImportDialog extends Dialog<Wallet> {
AnchorPane.setRightAnchor(scrollPane, 0.0);
Accordion importAccordion = new Accordion();
List<KeystoreFileImport> keystoreImporters = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig());
List<KeystoreFileImport> keystoreImporters = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig(), new PassportSinglesig());
for(KeystoreFileImport importer : keystoreImporters) {
FileWalletKeystoreImportPane importPane = new FileWalletKeystoreImportPane(importer);
importAccordion.getPanes().add(importPane);
}
List<WalletImport> walletImporters = List.of(new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new SpecterDesktop());
List<WalletImport> walletImporters = List.of(new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new SpecterDesktop(), new BlueWalletMultisig());
for(WalletImport importer : walletImporters) {
FileWalletImportPane importPane = new FileWalletImportPane(importer);
importAccordion.getPanes().add(importPane);
@@ -1,6 +1,7 @@
package com.sparrowwallet.sparrow.control;
import com.github.sarxos.webcam.Webcam;
import com.github.sarxos.webcam.WebcamListener;
import com.github.sarxos.webcam.WebcamResolution;
import com.google.zxing.*;
import com.google.zxing.client.j2se.BufferedImageLuminanceSource;
@@ -13,15 +14,18 @@ import javafx.embed.swing.SwingFXUtils;
import javafx.scene.image.Image;
import java.awt.image.BufferedImage;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
public class WebcamService extends Service<Image> {
private final WebcamResolution resolution ;
private WebcamResolution resolution;
private final WebcamListener listener;
private final ObjectProperty<Result> resultProperty = new SimpleObjectProperty<>(null);
public WebcamService(WebcamResolution resolution) {
public WebcamService(WebcamResolution resolution, WebcamListener listener) {
this.resolution = resolution;
this.listener = listener;
}
@Override
@@ -33,6 +37,9 @@ public class WebcamService extends Service<Image> {
try {
cam.setCustomViewSizes(resolution.getSize());
cam.setViewSize(resolution.getSize());
if(!Arrays.asList(cam.getWebcamListeners()).contains(listener)) {
cam.addWebcamListener(listener);
}
cam.open();
while(!isCancelled()) {
@@ -78,4 +85,8 @@ public class WebcamService extends Service<Image> {
public int getCamHeight() {
return resolution.getSize().height;
}
public void setResolution(WebcamResolution resolution) {
this.resolution = resolution;
}
}
@@ -0,0 +1,15 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.sparrow.AppServices;
public class ConnectionStartEvent {
private final String status;
public ConnectionStartEvent(String serverAddress) {
this.status = AppServices.isTorRunning() ? "Tor running, connecting to " + serverAddress + "..." : "Connecting to " + serverAddress + "...";
}
public String getStatus() {
return status;
}
}
@@ -0,0 +1,7 @@
package com.sparrowwallet.sparrow.event;
public class TorBootStatusEvent extends TorStatusEvent {
public TorBootStatusEvent() {
super("Starting Tor...");
}
}
@@ -0,0 +1,16 @@
package com.sparrowwallet.sparrow.event;
public class TorFailedStatusEvent extends TorStatusEvent {
private final Throwable exception;
public TorFailedStatusEvent(Throwable exception) {
super("Tor failed to start: " + (exception.getCause() != null ?
(exception.getCause().getMessage().contains("Failed to bind") ? exception.getCause().getMessage() + " Is a Tor proxy already running?" : exception.getCause().getMessage() ) :
exception.getMessage()));
this.exception = exception;
}
public Throwable getException() {
return exception;
}
}
@@ -0,0 +1,7 @@
package com.sparrowwallet.sparrow.event;
public class TorReadyStatusEvent extends TorStatusEvent {
public TorReadyStatusEvent() {
super("Tor started");
}
}
@@ -12,13 +12,19 @@ import java.io.File;
* Note that all wallet detail controllers that share a WalletForm, and that class posts WalletNodesChangedEvent once it has cleared it's entry caches.
*/
public class WalletSettingsChangedEvent extends WalletChangedEvent {
private final Wallet pastWallet;
private final File walletFile;
public WalletSettingsChangedEvent(Wallet wallet, File walletFile) {
public WalletSettingsChangedEvent(Wallet wallet, Wallet pastWallet, File walletFile) {
super(wallet);
this.pastWallet = pastWallet;
this.walletFile = walletFile;
}
public Wallet getPastWallet() {
return pastWallet;
}
public File getWalletFile() {
return walletFile;
}
@@ -45,7 +45,10 @@ public class FontAwesome5 extends GlyphFont {
SATELLITE_DISH('\uf7c0'),
SD_CARD('\uf7c2'),
SEARCH('\uf002'),
SQUARE('\uf0c8'),
TIMES_CIRCLE('\uf057'),
TOGGLE_OFF('\uf204'),
TOGGLE_ON('\uf205'),
TOOLS('\uf7d9'),
UNDO('\uf0e2'),
WALLET('\uf555');
@@ -0,0 +1,50 @@
package com.sparrowwallet.sparrow.io;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletModel;
import java.io.InputStream;
public class BlueWalletMultisig extends ColdcardMultisig {
@Override
public String getName() {
return "Blue Wallet Vault Multisig";
}
@Override
public WalletModel getWalletModel() {
return WalletModel.BLUE_WALLET;
}
@Override
public Wallet importWallet(InputStream inputStream, String password) throws ImportException {
Wallet wallet = super.importWallet(inputStream, password);
for(Keystore keystore : wallet.getKeystores()) {
keystore.setLabel(keystore.getLabel().replace("Coldcard", "Blue Wallet"));
keystore.setWalletModel(WalletModel.BLUE_WALLET);
}
return wallet;
}
@Override
public String getWalletImportDescription() {
return "Import file or QR created by using the Wallet > Export Coordination Setup feature on your Blue Wallet Vault wallet.";
}
@Override
public String getWalletExportDescription() {
return "Export file that can be read by Blue Wallet using the Add Wallet > Vault > Import wallet feature.";
}
@Override
public boolean isWalletImportScannable() {
return true;
}
@Override
public boolean isWalletExportScannable() {
return true;
}
}
@@ -29,7 +29,7 @@ public class CoboVaultMultisig extends ColdcardMultisig {
@Override
public String getKeystoreImportDescription() {
return "Import file or QR created by using the Multisig Wallet > Show/Export XPUB feature on your Cobo Vault.";
return "Import file or QR created by using the Multisig Wallet > ... > Show/Export XPUB feature on your Cobo Vault.";
}
@Override
@@ -45,12 +45,12 @@ public class CoboVaultMultisig extends ColdcardMultisig {
@Override
public String getWalletImportDescription() {
return "Import file or QR created by using the Multisig Wallet > Create Multisig Wallet feature on your Cobo Vault.";
return "Import file or QR created by using the Multisig Wallet > ... > Create Multisig Wallet feature on your Cobo Vault.";
}
@Override
public String getWalletExportDescription() {
return "Export file that can be read by your Cobo Vault using the Multisig Wallet > Import Multisig Wallet feature.";
return "Export file or QR that can be read by your Cobo Vault using the Multisig Wallet > ... > Import Multisig Wallet feature.";
}
@Override
@@ -62,4 +62,9 @@ public class CoboVaultMultisig extends ColdcardMultisig {
public boolean isKeystoreImportScannable() {
return true;
}
@Override
public boolean isWalletExportScannable() {
return true;
}
}
@@ -21,7 +21,7 @@ public class CoboVaultSinglesig implements KeystoreFileImport, WalletImport {
@Override
public String getKeystoreImportDescription() {
return "Import file or QR created by using the Settings > Watch-Only Wallet > Generic Wallet > Export Wallet feature on your Cobo Vault.";
return "Import file or QR created by using the My Cobo Vault > ... > Export Wallet feature on your Cobo Vault.";
}
@Override
@@ -222,4 +222,9 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
public boolean isKeystoreImportScannable() {
return false;
}
@Override
public boolean isWalletExportScannable() {
return false;
}
}
@@ -57,7 +57,7 @@ public class ColdcardSinglesig implements KeystoreFileImport, WalletImport {
Map<String, JsonElement> map = gson.fromJson(new InputStreamReader(inputStream, StandardCharsets.UTF_8), stringStringMap);
if (map.get("xfp") == null) {
throw new ImportException("File was not a valid Coldcard wallet export");
throw new ImportException("File was not a valid " + getName() + " wallet export");
}
String masterFingerprint = map.get("xfp").getAsString();
@@ -107,7 +107,7 @@ public class ColdcardSinglesig implements KeystoreFileImport, WalletImport {
try {
wallet.checkWallet();
} catch(InvalidWalletException e) {
throw new ImportException("Imported Coldcard wallet was invalid: " + e.getMessage());
throw new ImportException("Imported " + getName() + " wallet was invalid: " + e.getMessage());
}
return wallet;
@@ -4,10 +4,7 @@ import com.google.gson.*;
import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.sparrow.Mode;
import com.sparrowwallet.sparrow.Theme;
import com.sparrowwallet.sparrow.net.CoreAuthType;
import com.sparrowwallet.sparrow.net.ExchangeSource;
import com.sparrowwallet.sparrow.net.FeeRatesSource;
import com.sparrowwallet.sparrow.net.ServerType;
import com.sparrowwallet.sparrow.net.*;
import com.sparrowwallet.sparrow.wallet.FeeRatesSelection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -39,11 +36,13 @@ public class Config {
private List<File> recentWalletFiles;
private Integer keyDerivationPeriod;
private File hwi;
private boolean hdCapture;
private ServerType serverType;
private String coreServer;
private CoreAuthType coreAuthType;
private File coreDataDir;
private String coreAuth;
private Boolean coreMultiWallet;
private String coreWallet;
private String electrumServer;
private File electrumServerCert;
@@ -141,6 +140,10 @@ public class Config {
flush();
}
public boolean isFetchRates() {
return getExchangeSource() != ExchangeSource.NONE;
}
public ExchangeSource getExchangeSource() {
return exchangeSource;
}
@@ -249,6 +252,15 @@ public class Config {
flush();
}
public boolean isHdCapture() {
return hdCapture;
}
public void setHdCapture(boolean hdCapture) {
this.hdCapture = hdCapture;
flush();
}
public ServerType getServerType() {
return serverType;
}
@@ -258,10 +270,27 @@ public class Config {
flush();
}
public boolean hasServerAddress() {
return getServerAddress() != null && !getServerAddress().isEmpty();
}
public String getServerAddress() {
return getServerType() == ServerType.BITCOIN_CORE ? getCoreServer() : getElectrumServer();
}
public boolean requiresTor() {
if(isUseProxy() || !hasServerAddress()) {
return false;
}
Protocol protocol = Protocol.getProtocol(getServerAddress());
if(protocol == null) {
return false;
}
return protocol.isOnionAddress(protocol.getServerHostAndPort(getServerAddress()));
}
public String getCoreServer() {
return coreServer;
}
@@ -298,12 +327,21 @@ 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 == null || coreWallet.isEmpty() ? null : coreWallet);
this.coreWallet = coreWallet;
flush();
}
@@ -173,6 +173,10 @@ public class Electrum implements KeystoreFileImport, WalletImport, WalletExport
keystore.setKeyDerivation(new KeyDerivation(masterFingerprint, derivationPath));
keystore.setExtendedPublicKey(xPub);
keystore.setLabel(ek.label != null ? ek.label : "Electrum");
if(keystore.getLabel().length() > Keystore.MAX_LABEL_LENGTH) {
keystore.setLabel(keystore.getLabel().substring(0, Keystore.MAX_LABEL_LENGTH));
}
wallet.getKeystores().add(keystore);
ExtendedKey.Header xpubHeader = ExtendedKey.Header.fromExtendedKey(ek.xpub);
@@ -355,6 +359,11 @@ public class Electrum implements KeystoreFileImport, WalletImport, WalletExport
return false;
}
@Override
public boolean isWalletExportScannable() {
return false;
}
@Override
public String getWalletExportDescription() {
return "Export this wallet as an Electrum wallet file.";
@@ -0,0 +1,38 @@
package com.sparrowwallet.sparrow.io;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.WalletModel;
import java.io.InputStream;
public class PassportMultisig extends ColdcardMultisig {
@Override
public String getName() {
return "Passport Multisig";
}
@Override
public WalletModel getWalletModel() {
return WalletModel.PASSPORT;
}
@Override
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
Keystore keystore = super.getKeystore(scriptType, inputStream, password);
keystore.setLabel("Passport");
keystore.setWalletModel(getWalletModel());
return keystore;
}
@Override
public String getKeystoreImportDescription() {
return "Import file or QR created from Settings > Pair Software Wallet > Sparrow > Multisig > microSD/QR on your Passport.";
}
@Override
public boolean isKeystoreImportScannable() {
return true;
}
}
@@ -0,0 +1,48 @@
package com.sparrowwallet.sparrow.io;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.WalletModel;
import java.io.InputStream;
public class PassportSinglesig extends ColdcardSinglesig {
@Override
public String getName() {
return "Passport";
}
@Override
public String getKeystoreImportDescription() {
return "Import file or QR created from Settings > Pair Software Wallet > Sparrow > Single Sig > microSD/QR on your Passport.";
}
@Override
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
Keystore keystore = super.getKeystore(scriptType, inputStream, password);
keystore.setLabel("Passport");
keystore.setWalletModel(getWalletModel());
return keystore;
}
@Override
public WalletModel getWalletModel() {
return WalletModel.PASSPORT;
}
@Override
public boolean isWalletImportScannable() {
return true;
}
@Override
public boolean isKeystoreImportScannable() {
return true;
}
@Override
public String getWalletImportDescription() {
return getKeystoreImportDescription();
}
}
@@ -3,6 +3,7 @@ package com.sparrowwallet.sparrow.io;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.sparrowwallet.drongo.OutputDescriptor;
import com.sparrowwallet.drongo.wallet.BlockTransactionHash;
import com.sparrowwallet.drongo.wallet.InvalidWalletException;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletModel;
@@ -19,7 +20,7 @@ public class SpecterDesktop implements WalletImport, WalletExport {
try {
SpecterWallet specterWallet = new SpecterWallet();
specterWallet.label = wallet.getName();
specterWallet.blockheight = wallet.getStoredBlockHeight();
specterWallet.blockheight = wallet.getTransactions().values().stream().mapToInt(BlockTransactionHash::getHeight).min().orElse(wallet.getStoredBlockHeight());
specterWallet.descriptor = OutputDescriptor.getOutputDescriptor(wallet).toString(true);
Gson gson = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
@@ -83,6 +84,11 @@ public class SpecterDesktop implements WalletImport, WalletExport {
return true;
}
@Override
public boolean isWalletExportScannable() {
return true;
}
@Override
public String getName() {
return "Specter Desktop";
@@ -8,4 +8,5 @@ public interface WalletExport extends Export {
void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException;
String getWalletExportDescription();
String getExportFileExtension();
boolean isWalletExportScannable();
}
@@ -16,9 +16,9 @@ public class HwAirgappedController extends KeystoreImportDetailController {
public void initializeView() {
List<KeystoreFileImport> importers = Collections.emptyList();
if(getMasterController().getWallet().getPolicyType().equals(PolicyType.SINGLE)) {
importers = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig(), new SpecterDIY());
importers = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig(), new PassportSinglesig(), new SpecterDIY());
} else if(getMasterController().getWallet().getPolicyType().equals(PolicyType.MULTI)) {
importers = List.of(new ColdcardMultisig(), new CoboVaultMultisig(), new SpecterDIY());
importers = List.of(new ColdcardMultisig(), new CoboVaultMultisig(), new PassportMultisig(), new SpecterDIY());
}
for(KeystoreImport importer : importers) {
@@ -25,23 +25,29 @@ import java.util.stream.Collectors;
public class Bwt {
private static final Logger log = LoggerFactory.getLogger(Bwt.class);
public static final String DEFAULT_CORE_WALLET = "sparrow";
private static final int IMPORT_BATCH_SIZE = 350;
private static boolean initialized;
private Long shutdownPtr;
private boolean terminating;
private boolean ready;
static {
try {
org.controlsfx.tools.Platform platform = org.controlsfx.tools.Platform.getCurrent();
if(platform == org.controlsfx.tools.Platform.OSX) {
NativeUtils.loadLibraryFromJar("/native/osx/x64/libbwt_jni.dylib");
} else if(platform == org.controlsfx.tools.Platform.WINDOWS) {
NativeUtils.loadLibraryFromJar("/native/windows/x64/bwt_jni.dll");
} else {
NativeUtils.loadLibraryFromJar("/native/linux/x64/libbwt_jni.so");
public synchronized static void initialize() {
if(!initialized) {
try {
org.controlsfx.tools.Platform platform = org.controlsfx.tools.Platform.getCurrent();
if(platform == org.controlsfx.tools.Platform.OSX) {
NativeUtils.loadLibraryFromJar("/native/osx/x64/libbwt_jni.dylib");
} else if(platform == org.controlsfx.tools.Platform.WINDOWS) {
NativeUtils.loadLibraryFromJar("/native/windows/x64/bwt_jni.dll");
} else {
NativeUtils.loadLibraryFromJar("/native/linux/x64/libbwt_jni.so");
}
initialized = true;
} catch(IOException e) {
log.error("Error loading bwt library", e);
}
} catch(IOException e) {
log.error("Error loading bwt library", e);
}
}
@@ -106,14 +112,15 @@ public class Bwt {
Config config = Config.get();
bwtConfig.bitcoindUrl = config.getCoreServer();
if(config.getCoreAuthType() == CoreAuthType.COOKIE) {
if(config.getCoreAuthType() == CoreAuthType.COOKIE && config.getCoreDataDir() != null) {
bwtConfig.bitcoindDir = config.getCoreDataDir().getAbsolutePath() + "/";
} else {
bwtConfig.bitcoindAuth = config.getCoreAuth();
}
if(config.getCoreWallet() != null && !config.getCoreWallet().isEmpty()) {
if(config.getCoreMultiWallet() != Boolean.FALSE) {
bwtConfig.bitcoindWallet = config.getCoreWallet();
}
bwtConfig.createWalletIfMissing = true;
Gson gson = new Gson();
String jsonConfig = gson.toJson(bwtConfig);
@@ -136,7 +143,6 @@ public class Bwt {
this.terminating = false;
this.ready = false;
this.shutdownPtr = null;
Platform.runLater(() -> EventManager.get().post(new BwtShutdownEvent()));
}
public boolean isRunning() {
@@ -178,6 +184,9 @@ public class Bwt {
@SerializedName("bitcoind_wallet")
public String bitcoindWallet;
@SerializedName("create_wallet_if_missing")
public Boolean createWalletIfMissing;
@SerializedName("descriptors")
public List<String> descriptors;
@@ -779,7 +779,7 @@ public class ElectrumServer {
private final Bwt bwt = new Bwt();
private final ReentrantLock bwtStartLock = new ReentrantLock();
private final Condition bwtStartCondition = bwtStartLock.newCondition();
private final StringProperty statusProperty = new SimpleStringProperty();
private Throwable bwtStartException;
public ConnectionService() {
this(true);
@@ -796,10 +796,14 @@ public class ElectrumServer {
ElectrumServer electrumServer = new ElectrumServer();
if(Config.get().getServerType() == ServerType.BITCOIN_CORE) {
Bwt.initialize();
if(!bwt.isRunning()) {
Bwt.ConnectionService bwtConnectionService = bwt.getConnectionService(subscribe ? AppServices.get().getOpenWallets().keySet() : null);
bwtStartException = null;
bwtConnectionService.setOnFailed(workerStateEvent -> {
log.error("Failed to start BWT", workerStateEvent.getSource().getException());
bwtStartException = workerStateEvent.getSource().getException();
try {
bwtStartLock.lock();
bwtStartCondition.signal();
@@ -813,8 +817,12 @@ public class ElectrumServer {
bwtStartLock.lock();
bwtStartCondition.await();
if(!bwt.isRunning()) {
throw new ServerException("Check if Bitcoin Core is running, and the authentication details are correct.");
if(!bwt.isReady()) {
if(bwtStartException != null && bwtStartException.getMessage().contains("Wallet file not specified")) {
throw new ServerException("Bitcoin Core requires Multi-Wallet to be enabled in the Server Preferences");
} else {
throw new ServerException("Check if Bitcoin Core is running, and the authentication details are correct.");
}
}
} catch(InterruptedException e) {
Thread.currentThread().interrupt();
@@ -921,13 +929,16 @@ public class ElectrumServer {
Bwt.DisconnectionService disconnectionService = bwt.getDisconnectionService();
disconnectionService.setOnSucceeded(workerStateEvent -> {
ElectrumServer.bwtElectrumServer = null;
if(subscribe) {
EventManager.get().post(new BwtShutdownEvent());
}
});
disconnectionService.setOnFailed(workerStateEvent -> {
log.error("Failed to stop BWT", workerStateEvent.getSource().getException());
});
Platform.runLater(disconnectionService::start);
} else {
Platform.runLater(() -> EventManager.get().post(new DisconnectionEvent()));
disconnectionService.start();
} else if(subscribe) {
EventManager.get().post(new DisconnectionEvent());
}
}
@@ -942,11 +953,6 @@ public class ElectrumServer {
log.error("Uncaught error in ConnectionService", e);
}
@Subscribe
public void torStatus(TorStatusEvent event) {
statusProperty.set(event.getStatus());
}
@Subscribe
public void bwtElectrumReadyStatus(BwtElectrumReadyStatusEvent event) {
if(this.isRunning()) {
@@ -975,10 +981,6 @@ public class ElectrumServer {
bwtStartLock.unlock();
}
}
public StringProperty statusProperty() {
return statusProperty;
}
}
public static class ReadRunnable implements Runnable {
@@ -1,9 +1,8 @@
package com.sparrowwallet.sparrow.net;
import com.google.common.net.HostAndPort;
import com.google.gson.Gson;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.event.ExchangeRatesUpdatedEvent;
import com.sparrowwallet.sparrow.io.Config;
import javafx.concurrent.ScheduledService;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
@@ -13,7 +12,6 @@ import org.slf4j.LoggerFactory;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.URL;
import java.nio.charset.StandardCharsets;
@@ -52,7 +50,7 @@ public enum ExchangeSource {
private CoinbaseRates getRates() {
String url = "https://api.coinbase.com/v2/exchange-rates?currency=BTC";
Proxy proxy = getProxy();
Proxy proxy = AppServices.getProxy();
try(InputStream is = (proxy == null ? new URL(url).openStream() : new URL(url).openConnection(proxy).getInputStream()); Reader reader = new InputStreamReader(is, StandardCharsets.UTF_8)) {
Gson gson = new Gson();
@@ -83,7 +81,7 @@ public enum ExchangeSource {
private CoinGeckoRates getRates() {
String url = "https://api.coingecko.com/api/v3/exchange_rates";
Proxy proxy = getProxy();
Proxy proxy = AppServices.getProxy();
try(InputStream is = (proxy == null ? new URL(url).openStream() : new URL(url).openConnection(proxy).getInputStream()); Reader reader = new InputStreamReader(is, StandardCharsets.UTF_8)) {
Gson gson = new Gson();
@@ -116,17 +114,6 @@ public enum ExchangeSource {
}
}
private static Proxy getProxy() {
Config config = Config.get();
if(config.isUseProxy()) {
HostAndPort proxy = HostAndPort.fromString(config.getProxyServer());
InetSocketAddress proxyAddress = new InetSocketAddress(proxy.getHost(), proxy.getPortOrDefault(ProxyTcpOverTlsTransport.DEFAULT_PROXY_PORT));
return new Proxy(Proxy.Type.SOCKS, proxyAddress);
}
return null;
}
@Override
public String toString() {
return name;
@@ -28,12 +28,13 @@ public enum Protocol {
@Override
public Transport getTransport(HostAndPort server, HostAndPort proxy) {
throw new UnsupportedOperationException("TCP protocol does not support proxying");
//Avoid using a TorSocket if a proxy is specified, even if a .onion address
return new TcpTransport(server, proxy);
}
@Override
public Transport getTransport(HostAndPort server, File serverCert, HostAndPort proxy) {
throw new UnsupportedOperationException("TCP protocol does not support proxying");
return getTransport(server, proxy);
}
},
SSL {
@@ -116,7 +117,7 @@ public enum Protocol {
}
public boolean isOnionAddress(HostAndPort server) {
return server.getHost().toLowerCase().endsWith(".onion");
return server.getHost().toLowerCase().endsWith(TorService.TOR_ADDRESS_SUFFIX);
}
public static Protocol getProtocol(String url) {
@@ -0,0 +1,67 @@
package com.sparrowwallet.sparrow.net;
import com.google.common.net.HostAndPort;
import javax.net.SocketFactory;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.Socket;
import static com.sparrowwallet.sparrow.net.ProxyTcpOverTlsTransport.DEFAULT_PROXY_PORT;
public class ProxySocketFactory extends SocketFactory {
private final Proxy proxy;
public ProxySocketFactory() {
this(Proxy.NO_PROXY);
}
public ProxySocketFactory(HostAndPort proxyHostAndPort) {
this(getSocksProxy(proxyHostAndPort.getHost(), proxyHostAndPort.getPortOrDefault(DEFAULT_PROXY_PORT)));
}
public ProxySocketFactory(Proxy proxy) {
this.proxy = proxy;
}
@Override
public Socket createSocket() {
return new Socket(proxy);
}
@Override
public Socket createSocket(String address, int port) throws IOException {
return createSocket(new InetSocketAddress(address, port), null);
}
@Override
public Socket createSocket(String address, int port, InetAddress localAddress, int localPort) throws IOException {
return createSocket(new InetSocketAddress(address, port), new InetSocketAddress(localAddress, localPort));
}
@Override
public Socket createSocket(InetAddress address, int port) throws IOException {
return createSocket(new InetSocketAddress(address, port), null);
}
@Override
public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
return createSocket(new InetSocketAddress(address, port), new InetSocketAddress(localAddress, localPort));
}
private Socket createSocket(InetSocketAddress address, InetSocketAddress bindAddress) throws IOException {
Socket socket = new Socket(proxy);
if(bindAddress != null) {
socket.bind(bindAddress);
}
socket.connect(address);
return socket;
}
private static Proxy getSocksProxy(String proxyAddress, int proxyPort) {
return new Proxy(Proxy.Type.SOCKS, new InetSocketAddress(proxyAddress, proxyPort));
}
}
@@ -47,8 +47,12 @@ public class TcpTransport implements Transport, Closeable {
private final Gson gson = new Gson();
public TcpTransport(HostAndPort server) {
this(server, null);
}
public TcpTransport(HostAndPort server, HostAndPort proxy) {
this.server = server;
this.socketFactory = SocketFactory.getDefault();
this.socketFactory = (proxy == null ? SocketFactory.getDefault() : new ProxySocketFactory(proxy));
}
@Override
@@ -0,0 +1,61 @@
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.LinkedHashMap;
/**
* Service to start internal Tor (including a Tor proxy running on localhost:9050)
*
* This is a ScheduledService to take advantage of the retry on failure behaviour
*/
public class TorService extends ScheduledService<NativeTor> {
private static final Logger log = LoggerFactory.getLogger(TorService.class);
public static final int PROXY_PORT = 9050;
public static final String TOR_DIR_PREFIX = "tor";
public static final String TOR_ADDRESS_SUFFIX = ".onion";
@Override
protected Task<NativeTor> createTask() {
return new Task<>() {
protected NativeTor call() throws IOException {
if(Tor.getDefault() == null) {
Path path = Files.createTempDirectory(TOR_DIR_PREFIX);
File torInstallDir = path.toFile();
torInstallDir.deleteOnExit();
try {
LinkedHashMap<String, String> torrcOptionsMap = new LinkedHashMap<>();
torrcOptionsMap.put("SocksPort", Integer.toString(PROXY_PORT));
torrcOptionsMap.put("DisableNetwork", "0");
Torrc override = new Torrc(torrcOptionsMap);
return new NativeTor(torInstallDir, Collections.emptyList(), override);
} catch(TorCtlException e) {
log.error("Failed to start Tor", e);
if(e.getCause() instanceof TorControlError) {
throw new IOException("Failed to start Tor", e.getCause());
} else {
throw new IOException("Failed to start Tor", e);
}
}
}
return null;
}
};
}
}
@@ -1,18 +1,11 @@
package com.sparrowwallet.sparrow.net;
import com.google.common.net.HostAndPort;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.StatusEvent;
import com.sparrowwallet.sparrow.event.TorStatusEvent;
import javafx.application.Platform;
import com.sparrowwallet.sparrow.AppServices;
import org.berndpruenster.netlayer.tor.*;
import java.io.*;
import java.net.Socket;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.LinkedHashMap;
public class TorTcpTransport extends TcpTransport {
public static final String TOR_DIR_PREFIX = "tor";
@@ -21,35 +14,16 @@ public class TorTcpTransport extends TcpTransport {
super(server);
}
public TorTcpTransport(HostAndPort server, HostAndPort proxy) {
super(server, proxy);
}
@Override
protected Socket createSocket() throws IOException {
if(Tor.getDefault() == null) {
Platform.runLater(() -> {
String status = "Starting Tor...";
EventManager.get().post(new TorStatusEvent(status));
});
Path path = Files.createTempDirectory(TOR_DIR_PREFIX);
File torInstallDir = path.toFile();
torInstallDir.deleteOnExit();
try {
LinkedHashMap<String, String> torrcOptionsMap = new LinkedHashMap<>();
torrcOptionsMap.put("DisableNetwork", "0");
Torrc override = new Torrc(torrcOptionsMap);
NativeTor nativeTor = new NativeTor(torInstallDir, Collections.emptyList(), override);
Tor.setDefault(nativeTor);
} catch(TorCtlException e) {
e.printStackTrace();
throw new IOException(e);
}
if(!AppServices.isTorRunning()) {
throw new IllegalStateException("Can't create Tor socket, Tor is not running");
}
Platform.runLater(() -> {
String status = "Tor running, connecting to " + server.toString() + "...";
EventManager.get().post(new TorStatusEvent(status));
});
return new TorSocket(server.getHost(), server.getPort(), "sparrow");
}
}
@@ -5,6 +5,7 @@ import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.address.InvalidAddressException;
import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.MainApp;
import com.sparrowwallet.sparrow.event.VersionUpdatedEvent;
import javafx.concurrent.ScheduledService;
@@ -15,6 +16,7 @@ import org.slf4j.LoggerFactory;
import javax.net.ssl.HttpsURLConnection;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Proxy;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.SignatureException;
@@ -44,7 +46,8 @@ public class VersionCheckService extends ScheduledService<VersionUpdatedEvent> {
private VersionCheck getVersionCheck() throws IOException {
URL url = new URL(VERSION_CHECK_URL);
HttpsURLConnection conn = (HttpsURLConnection)url.openConnection();
Proxy proxy = AppServices.getProxy();
HttpsURLConnection conn = (HttpsURLConnection)(proxy == null ? url.openConnection() : url.openConnection(proxy));
try(InputStreamReader reader = new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8)) {
Gson gson = new Gson();
@@ -14,7 +14,6 @@ import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.net.*;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.concurrent.WorkerStateEvent;
import javafx.fxml.FXML;
import javafx.scene.control.*;
import javafx.scene.text.Font;
@@ -22,6 +21,8 @@ import javafx.stage.DirectoryChooser;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import javafx.util.Duration;
import net.freehaven.tor.control.TorControlError;
import org.berndpruenster.netlayer.tor.Tor;
import org.controlsfx.glyphfont.Glyph;
import org.controlsfx.validation.ValidationResult;
import org.controlsfx.validation.ValidationSupport;
@@ -121,6 +122,8 @@ public class ServerPreferencesController extends PreferencesDetailController {
private final ValidationSupport validationSupport = new ValidationSupport();
private TorService torService;
private ElectrumServer.ConnectionService connectionService;
@Override
@@ -205,10 +208,8 @@ public class ServerPreferencesController extends PreferencesDetailController {
});
coreMultiWallet.selectedProperty().addListener((observable, oldValue, newValue) -> {
coreWallet.setText(" ");
coreWallet.setText("");
config.setCoreMultiWallet(newValue);
coreWallet.setDisable(!newValue);
coreWallet.setPromptText(newValue ? "" : "Default");
});
electrumUseSsl.selectedProperty().addListener((observable, oldValue, newValue) -> {
@@ -244,17 +245,9 @@ public class ServerPreferencesController extends PreferencesDetailController {
proxyHost.setText(proxyHost.getText().trim());
proxyHost.setDisable(!newValue);
proxyPort.setDisable(!newValue);
if(newValue) {
electrumUseSsl.setSelected(true);
electrumUseSsl.setDisable(true);
} else {
electrumUseSsl.setDisable(false);
}
});
boolean isConnected = AppServices.isConnecting() || AppServices.isConnected();
setFieldsEditable(!isConnected);
if(AppServices.isConnecting()) {
testResults.appendText("Connecting to server, please wait...");
@@ -266,7 +259,12 @@ public class ServerPreferencesController extends PreferencesDetailController {
testConnection.setOnAction(event -> {
testConnection.setGraphic(getGlyph(FontAwesome5.Glyph.ELLIPSIS_H, null));
testResults.setText("Connecting to " + config.getServerAddress() + "...");
startElectrumConnection();
if(Config.get().requiresTor() && Tor.getDefault() == null) {
startTor();
} else {
startElectrumConnection();
}
});
editConnection.managedProperty().bind(editConnection.visibleProperty());
@@ -307,12 +305,15 @@ public class ServerPreferencesController extends PreferencesDetailController {
}
}
coreMultiWallet.setSelected(true);
coreMultiWallet.setSelected(config.getCoreWallet() != null);
if(config.getCoreWallet() != null) {
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);
@@ -340,11 +341,6 @@ public class ServerPreferencesController extends PreferencesDetailController {
proxyHost.setDisable(!config.isUseProxy());
proxyPort.setDisable(!config.isUseProxy());
if(config.isUseProxy()) {
electrumUseSsl.setSelected(true);
electrumUseSsl.setDisable(true);
}
String proxyServer = config.getProxyServer();
if(proxyServer != null) {
HostAndPort server = HostAndPort.fromString(proxyServer);
@@ -353,6 +349,34 @@ public class ServerPreferencesController extends PreferencesDetailController {
proxyPort.setText(Integer.toString(server.getPort()));
}
}
setFieldsEditable(!isConnected);
}
private void startTor() {
if(torService != null && torService.isRunning()) {
return;
}
torService = new TorService();
torService.setPeriod(Duration.hours(1000));
torService.setRestartOnFailure(false);
torService.setOnRunning(workerStateEvent -> {
testResults.setText(testResults.getText() + "\nStarting Tor...");
});
torService.setOnSucceeded(workerStateEvent -> {
Tor.setDefault(torService.getValue());
torService.cancel();
testResults.setText(testResults.getText() + "\nTor started");
startElectrumConnection();
});
torService.setOnFailed(workerStateEvent -> {
testResults.setText(testResults.getText() + "\nTor failed to start");
showConnectionFailure(workerStateEvent.getSource().getException());
});
torService.start();
}
private void startElectrumConnection() {
@@ -362,10 +386,8 @@ public class ServerPreferencesController extends PreferencesDetailController {
connectionService = new ElectrumServer.ConnectionService(false);
connectionService.setPeriod(Duration.hours(1));
connectionService.setRestartOnFailure(false);
EventManager.get().register(connectionService);
connectionService.statusProperty().addListener((observable, oldValue, newValue) -> {
testResults.setText(testResults.getText() + "\n" + newValue);
});
connectionService.setOnSucceeded(successEvent -> {
EventManager.get().unregister(connectionService);
@@ -377,8 +399,7 @@ public class ServerPreferencesController extends PreferencesDetailController {
});
connectionService.setOnFailed(workerStateEvent -> {
EventManager.get().unregister(connectionService);
showConnectionFailure(workerStateEvent);
connectionService.cancel();
showConnectionFailure(workerStateEvent.getSource().getException());
});
connectionService.start();
}
@@ -419,13 +440,15 @@ public class ServerPreferencesController extends PreferencesDetailController {
}
}
private void showConnectionFailure(WorkerStateEvent failEvent) {
Throwable e = failEvent.getSource().getException();
log.error("Connection error", e);
String reason = e.getCause() != null ? e.getCause().getMessage() : e.getMessage();
if(e.getCause() != null && e.getCause() instanceof SSLHandshakeException) {
private void showConnectionFailure(Throwable exception) {
log.error("Connection error", exception);
String reason = exception.getCause() != null ? exception.getCause().getMessage() : exception.getMessage();
if(exception.getCause() != null && exception.getCause() instanceof SSLHandshakeException) {
reason = "SSL Handshake Error\n" + reason;
}
if(exception.getCause() != null && exception.getCause() instanceof TorControlError && exception.getCause().getMessage().contains("Failed to bind")) {
reason += "\nIs a Tor proxy already running on port " + TorService.PROXY_PORT + "?";
}
testResults.setText("Could not connect:\n\n" + reason);
testConnection.setGraphic(getGlyph(FontAwesome5.Glyph.EXCLAMATION_CIRCLE, "failure"));
@@ -452,10 +475,6 @@ public class ServerPreferencesController extends PreferencesDetailController {
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Core pass required", coreAuthToggleGroup.getSelectedToggle().getUserData() == CoreAuthType.USERPASS && newValue.isEmpty())
));
validationSupport.registerValidator(coreWallet, Validator.combine(
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Core wallet required", coreMultiWallet.isSelected() && newValue.isEmpty())
));
validationSupport.registerValidator(electrumHost, Validator.combine(
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid Electrum host", getHost(newValue) == null)
));
@@ -633,7 +633,7 @@ public class HeadersController extends TransactionFormController implements Init
toggleButton.setSelected(false);
//TODO: Remove once Cobo Vault has upgraded to UR2.0
boolean addLegacyEncodingOption = headersForm.getSigningWallet().getKeystores().stream().anyMatch(keystore -> keystore.getWalletModel().equals(WalletModel.COBO_VAULT));
boolean addLegacyEncodingOption = headersForm.getSigningWallet().getKeystores().stream().anyMatch(keystore -> keystore.getWalletModel().equals(WalletModel.COBO_VAULT) || keystore.getWalletModel().equals(WalletModel.SPARROW));
try {
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(RegistryType.CRYPTO_PSBT.toString(), headersForm.getPsbt().serialize(), addLegacyEncodingOption);
@@ -64,6 +64,7 @@ public class TransactionController implements Initializable {
public void initializeView() {
initializeTxTree();
transactionMasterDetail.setDividerPosition(0.82);
transactionMasterDetail.setShowDetailNode(Config.get().isShowTransactionHex());
txhex.setTransaction(getTransaction());
highlightTxHex();
@@ -75,6 +76,12 @@ public class TransactionController implements Initializable {
} else if(TransactionView.OUTPUT.equals(initialView) && initialIndex >= PageForm.PAGE_SIZE) {
fetchOutputBlockTransactions(initialIndex, initialIndex + 1);
}
transactionMasterDetail.sceneProperty().addListener((observable, oldScene, newScene) -> {
if(oldScene == null && newScene != null) {
transactionMasterDetail.setDividerPosition(AppServices.isReducedWindowHeight(transactionMasterDetail) ? 0.9 : 0.82);
}
});
}
private void initializeTxTree() {
@@ -337,6 +337,15 @@ public class SendController extends WalletFormController implements Initializabl
transactionDiagram.update(walletTransaction);
createButton.setDisable(walletTransaction == null || isInsufficientFeeRate());
});
transactionDiagram.sceneProperty().addListener((observable, oldScene, newScene) -> {
if(oldScene == null && newScene != null) {
transactionDiagram.update(null);
newScene.getWindow().heightProperty().addListener((observable1, oldValue, newValue) -> {
transactionDiagram.update(walletTransactionProperty.get());
});
}
});
}
private void initializeTabHeader(int count) {
@@ -38,6 +38,8 @@ public class SettingsWalletForm extends WalletForm {
@Override
public void saveAndRefresh() throws IOException {
Wallet pastWallet = wallet.copy();
boolean refreshAll = isRefreshNecessary(wallet, walletCopy);
if(refreshAll) {
walletCopy.clearNodes();
@@ -47,7 +49,7 @@ public class SettingsWalletForm extends WalletForm {
save();
if(refreshAll) {
EventManager.get().post(new WalletSettingsChangedEvent(wallet, getWalletFile()));
EventManager.get().post(new WalletSettingsChangedEvent(wallet, pastWallet, getWalletFile()));
}
}
@@ -2,14 +2,18 @@ package com.sparrowwallet.sparrow.wallet;
import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.wallet.BlockTransaction;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletNode;
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.net.ElectrumServer;
import com.sparrowwallet.sparrow.io.Storage;
import com.sparrowwallet.sparrow.net.ServerType;
import javafx.application.Platform;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -23,6 +27,7 @@ public class WalletForm {
private final Storage storage;
protected Wallet wallet;
private Wallet savedPastWallet;
private WalletTransactionsEntry walletTransactionsEntry;
private WalletUtxosEntry walletUtxosEntry;
@@ -32,7 +37,7 @@ public class WalletForm {
public WalletForm(Storage storage, Wallet currentWallet) {
this.storage = storage;
this.wallet = currentWallet;
refreshHistory(AppServices.getCurrentBlockHeight());
refreshHistory(AppServices.getCurrentBlockHeight(), null);
}
public Wallet getWallet() {
@@ -60,27 +65,28 @@ public class WalletForm {
}
public void saveAndRefresh() throws IOException {
Wallet pastWallet = wallet.copy();
wallet.clearHistory();
save();
refreshHistory(AppServices.getCurrentBlockHeight());
refreshHistory(AppServices.getCurrentBlockHeight(), pastWallet);
}
public void saveBackup() throws IOException {
storage.backupWallet();
}
public void refreshHistory(Integer blockHeight) {
refreshHistory(blockHeight, null);
public void refreshHistory(Integer blockHeight, Wallet pastWallet) {
refreshHistory(blockHeight, pastWallet, null);
}
public void refreshHistory(Integer blockHeight, WalletNode node) {
public void refreshHistory(Integer blockHeight, Wallet pastWallet, WalletNode node) {
Wallet previousWallet = wallet.copy();
if(wallet.isValid() && AppServices.isConnected()) {
log.debug(node == null ? wallet.getName() + " refreshing full wallet history" : wallet.getName() + " requesting node wallet history for " + node.getDerivationPath());
ElectrumServer.TransactionHistoryService historyService = new ElectrumServer.TransactionHistoryService(wallet, getWalletTransactionNodes(node));
historyService.setOnSucceeded(workerStateEvent -> {
EventManager.get().post(new WalletHistoryStatusEvent(wallet, false));
updateWallet(previousWallet, blockHeight);
updateWallet(blockHeight, pastWallet, previousWallet);
});
historyService.setOnFailed(workerStateEvent -> {
log.error("Error retrieving wallet history", workerStateEvent.getSource().getException());
@@ -91,15 +97,31 @@ public class WalletForm {
}
}
private void updateWallet(Wallet previousWallet, Integer blockHeight) {
private void updateWallet(Integer blockHeight, Wallet pastWallet, Wallet previousWallet) {
if(blockHeight != null) {
wallet.setStoredBlockHeight(blockHeight);
}
notifyIfChanged(previousWallet, blockHeight);
if(pastWallet != null) {
copyLabels(pastWallet);
}
notifyIfChanged(blockHeight, previousWallet);
}
private void notifyIfChanged(Wallet previousWallet, Integer blockHeight) {
private void copyLabels(Wallet pastWallet) {
wallet.getNode(KeyPurpose.RECEIVE).copyLabels(pastWallet.getNode(KeyPurpose.RECEIVE));
wallet.getNode(KeyPurpose.CHANGE).copyLabels(pastWallet.getNode(KeyPurpose.CHANGE));
for(Map.Entry<Sha256Hash, BlockTransaction> txEntry : wallet.getTransactions().entrySet()) {
BlockTransaction pastBlockTransaction = pastWallet.getTransactions().get(txEntry.getKey());
if(pastBlockTransaction != null && txEntry.getValue() != null && txEntry.getValue().getLabel() == null && pastBlockTransaction.getLabel() != null) {
txEntry.getValue().setLabel(pastBlockTransaction.getLabel());
}
}
}
private void notifyIfChanged(Integer blockHeight, Wallet previousWallet) {
List<WalletNode> historyChangedNodes = new ArrayList<>();
historyChangedNodes.addAll(getHistoryChangedNodes(previousWallet.getNode(KeyPurpose.RECEIVE).getChildren(), wallet.getNode(KeyPurpose.RECEIVE).getChildren()));
historyChangedNodes.addAll(getHistoryChangedNodes(previousWallet.getNode(KeyPurpose.CHANGE).getChildren(), wallet.getNode(KeyPurpose.CHANGE).getChildren()));
@@ -226,7 +248,14 @@ public class WalletForm {
walletUtxosEntry = null;
accountEntries.clear();
EventManager.get().post(new WalletNodesChangedEvent(wallet));
refreshHistory(AppServices.getCurrentBlockHeight());
//It is necessary to save the past wallet because the actual copying of the past labels only occurs on a later ConnectionEvent with bwt
//The savedPastWallet variable can be removed once bwt supports dynamic loading of wallets without needing to disconnect/reconnect
if(Config.get().getServerType() == ServerType.BITCOIN_CORE) {
savedPastWallet = event.getPastWallet();
}
refreshHistory(AppServices.getCurrentBlockHeight(), event.getPastWallet());
}
}
@@ -234,13 +263,14 @@ public class WalletForm {
public void newBlock(NewBlockEvent event) {
//Check if wallet is valid to avoid saving wallets in initial setup
if(wallet.isValid()) {
updateWallet(wallet.copy(), event.getHeight());
updateWallet(event.getHeight(), null, wallet.copy());
}
}
@Subscribe
public void connected(ConnectionEvent event) {
refreshHistory(event.getBlockHeight());
refreshHistory(event.getBlockHeight(), savedPastWallet);
savedPastWallet = null;
}
@Subscribe
@@ -249,7 +279,7 @@ public class WalletForm {
WalletNode walletNode = event.getWalletNode(wallet);
if(walletNode != null) {
log.debug(wallet.getName() + " history event for node " + walletNode + " (" + event.getScriptHash() + ")");
refreshHistory(AppServices.getCurrentBlockHeight(), walletNode);
refreshHistory(AppServices.getCurrentBlockHeight(), null, walletNode);
}
}
}
+1
View File
@@ -26,4 +26,5 @@ open module com.sparrowwallet.sparrow {
requires jcommander;
requires slf4j.api;
requires bwt.jni;
requires jtorctl;
}
@@ -8,7 +8,7 @@
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.image.Image?>
<StackPane prefHeight="420.0" prefWidth="600.0" stylesheets="@about.css, @general.css" fx:controller="com.sparrowwallet.sparrow.AboutController" xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml">
<StackPane prefHeight="460.0" prefWidth="600.0" stylesheets="@about.css, @general.css" fx:controller="com.sparrowwallet.sparrow.AboutController" xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml">
<VBox spacing="20">
<HBox styleClass="title-area">
<HBox alignment="CENTER_LEFT">
@@ -22,7 +22,7 @@
<MenuItem text="File..." onAction="#openTransactionFromFile" accelerator="Shortcut+F"/>
<MenuItem fx:id="openTransactionIdItem" text="From ID..." onAction="#openTransactionFromId" accelerator="Shortcut+I"/>
<MenuItem text="From Text..." onAction="#openTransactionFromText" accelerator="Shortcut+T"/>
<MenuItem text="From QR..." onAction="#openTransactionFromQR" accelerator="Shortcut+R"/>
<MenuItem text="From QR..." onAction="#openTransactionFromQR" accelerator="Shortcut+U"/>
<!-- <MenuItem text="Examples" onAction="#openExamples"/> -->
</items>
</Menu>
@@ -82,6 +82,8 @@
<CheckMenuItem fx:id="openWalletsInNewWindows" mnemonicParsing="false" text="Open Wallets in New Windows" onAction="#openWalletsInNewWindows"/>
<CheckMenuItem fx:id="hideEmptyUsedAddresses" mnemonicParsing="false" text="Hide Empty Used Addresses" onAction="#hideEmptyUsedAddresses"/>
<CheckMenuItem fx:id="showTxHex" mnemonicParsing="false" text="Show Transaction Hex" onAction="#showTxHex"/>
<SeparatorMenuItem />
<MenuItem fx:id="refreshWallet" mnemonicParsing="false" text="Refresh Wallet" accelerator="Shortcut+R" onAction="#refreshWallet"/>
</items>
</Menu>
<Menu fx:id="toolsMenu" mnemonicParsing="false" text="Tools">
@@ -89,7 +89,7 @@
<PasswordField fx:id="corePass"/>
</Field>
<Field text="Multi-Wallet:">
<UnlabeledToggleSwitch fx:id="coreMultiWallet"/> <HelpLabel helpText="Enable this if using multiple Bitcoin Core wallets" />
<UnlabeledToggleSwitch fx:id="coreMultiWallet"/> <HelpLabel helpText="Creates a new Bitcoin Core wallet with the following name (recommended to avoid conflicts)" />
</Field>
<Field text="Wallet Name:" styleClass="label-button">
<TextField fx:id="coreWallet"/>
@@ -65,10 +65,6 @@
-fx-alignment: center-left;
}
#transactionDiagram {
-fx-min-height: 230px;
}
#transactionDiagram .boundary {
-fx-stroke: transparent;
}
@@ -127,7 +127,7 @@
</AnchorPane>
</GridPane>
<AnchorPane>
<TransactionDiagram fx:id="transactionDiagram" maxWidth="700" maxHeight="230" AnchorPane.leftAnchor="100" />
<TransactionDiagram fx:id="transactionDiagram" maxWidth="700" AnchorPane.leftAnchor="100" />
</AnchorPane>
</VBox>
</center>
@@ -13,6 +13,7 @@
<?import org.fxmisc.flowless.VirtualizedScrollPane?>
<?import org.controlsfx.glyphfont.Glyph?>
<?import com.sparrowwallet.sparrow.control.HelpLabel?>
<BorderPane stylesheets="@settings.css, @wallet.css, @../script.css, @../descriptor.css, @../general.css" styleClass="wallet-pane" xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.sparrowwallet.sparrow.wallet.SettingsController">
<center>
<GridPane hgap="10.0" vgap="10.0">
@@ -40,6 +41,7 @@
</FXCollections>
</items>
</ComboBox>
<HelpLabel helpText="Single signature wallets use a single keystore (hardware wallet or seed).\nMultisignature wallets require multiple keystores (N), of which a certain number (M) need to sign." />
</Field>
<Field text="Script Type:">
<ComboBox fx:id="scriptType">
@@ -55,6 +57,7 @@
</FXCollections>
</items>
</ComboBox>
<HelpLabel helpText="P2WPKH and P2WSH are Native Segwit types and are usually the best choice.\nP2SH-P2WPKH and P2SH-P2WSH are Wrapped Segwit types and a good choice for the widest compatibility.\nP2PKH and P2SH are Legacy types and should be avoided, unless configuring an old wallet." />
</Field>
</Fieldset>
</Form>
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.