mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2026-07-30 19:46:16 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9872fc9221 | ||
|
|
84d2db427c | ||
|
|
cfbf9187e6 | ||
|
|
5478ac05b8 | ||
|
|
836bd6bc40 | ||
|
|
1bab07fc5f | ||
|
|
4b3de044c3 | ||
|
|
f2c2fc6071 | ||
|
|
4dd52eb9dd | ||
|
|
e45fdc72a0 | ||
|
|
d67dd515c4 | ||
|
|
ff962aa5f0 | ||
|
|
c0e44b2c55 | ||
|
|
f0c239d625 | ||
|
|
58a31f435e | ||
|
|
c49439d541 | ||
|
|
0070cbfb1e | ||
|
|
7ed75fc83d | ||
|
|
13d701b0a7 | ||
|
|
07ed9c857b | ||
|
|
32ab53ba03 | ||
|
|
68344e981c | ||
|
|
10c1574a4a | ||
|
|
3a853e2ab1 | ||
|
|
0f639dc2c2 | ||
|
|
a801773545 | ||
|
|
ffbf4bb0db | ||
|
|
11e46155a1 | ||
|
|
f369b3ad00 | ||
|
|
6068666415 | ||
|
|
6a58e8a799 | ||
|
|
3bd2f69157 | ||
|
|
03ec0c9da1 | ||
|
|
47fc0e11eb | ||
|
|
27f4b3b3ae | ||
|
|
bb8c9a19cf | ||
|
|
16da6ae224 | ||
|
|
152b55f7f0 | ||
|
|
62d83151d7 | ||
|
|
9060a4b284 | ||
|
|
759de8e047 | ||
|
|
1e3dd61b57 | ||
|
|
d13bc8c313 | ||
|
|
9b6ef1f3ca | ||
|
|
173078dd8d | ||
|
|
ae2db6e204 | ||
|
|
5abea8c594 | ||
|
|
cb94d91e98 | ||
|
|
454e4b4c4b | ||
|
|
48a63c25d2 | ||
|
|
0fdc90523e | ||
|
|
3203308d82 | ||
|
|
83a7f5c562 | ||
|
|
55e0648fc8 | ||
|
|
b517b575da | ||
|
|
890b4098d7 | ||
|
|
11a0e3765b | ||
|
|
3ffb09c5e6 | ||
|
|
622b681109 | ||
|
|
c27a576b3d | ||
|
|
7bc8bdc13e | ||
|
|
0097a1ecbc | ||
|
|
525d57ec73 | ||
|
|
e3d7bb57ee | ||
|
|
095518e858 | ||
|
|
ac470f64c1 | ||
|
|
30215b0dac | ||
|
|
563252c823 | ||
|
|
3a7ffe95e8 | ||
|
|
b588a4ee02 | ||
|
|
ab0611fabc | ||
|
|
a121bb5f26 | ||
|
|
2fc01e0345 | ||
|
|
650c3657a5 | ||
|
|
d836eb515e | ||
|
|
717bc9fec2 | ||
|
|
1ad6da86b1 | ||
|
|
54680a5692 | ||
|
|
6ef5e79b39 | ||
|
|
1dbf51b406 | ||
|
|
8a3aa0d06e | ||
|
|
ca99efa215 | ||
|
|
cc202ce4ca | ||
|
|
ad44fe2335 | ||
|
|
452242f208 | ||
|
|
ff83af2e0d | ||
|
|
aacecc8517 | ||
|
|
a9a3eef157 | ||
|
|
7fc37c21fd | ||
|
|
ff828b483b | ||
|
|
64a9cce837 | ||
|
|
caa7ef429c | ||
|
|
38818d8222 | ||
|
|
fd7d2232a1 | ||
|
|
b5eb59344c | ||
|
|
3093e4906c | ||
|
|
2faddc1133 | ||
|
|
a2ead56593 | ||
|
|
03adc1f9f0 | ||
|
|
7bc707bcaa | ||
|
|
7ae74e3000 | ||
|
|
94da3b37b6 | ||
|
|
c6689228d0 | ||
|
|
d847da9d61 | ||
|
|
6637ea09bf | ||
|
|
99959470e2 | ||
|
|
826162ba9f | ||
|
|
546ccd66b3 | ||
|
|
628b15a3b5 | ||
|
|
72281233e1 | ||
|
|
c68722e420 | ||
|
|
901a4520fd | ||
|
|
3e083b907d | ||
|
|
3c4b25ecee | ||
|
|
c23bbb9e02 | ||
|
|
94897e9744 | ||
|
|
d722c4972a | ||
|
|
e93df8273b | ||
|
|
a1c65cff75 | ||
|
|
18bc7bf302 | ||
|
|
17bb442c78 | ||
|
|
3134a9ad42 | ||
|
|
eda96af607 | ||
|
|
038069f6e6 |
@@ -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
|
||||
|
||||
@@ -28,11 +28,11 @@ 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
|
||||
|
||||
Sparrow has a number of command line options, for example to change it's home folder or use testnet:
|
||||
Sparrow has a number of command line options, for example to change its home folder or use testnet:
|
||||
|
||||
```
|
||||
./sparrow -h
|
||||
@@ -43,12 +43,15 @@ Usage: sparrow [options]
|
||||
Path to Sparrow home folder
|
||||
--help, -h
|
||||
Show usage
|
||||
--level, -l
|
||||
Set log level
|
||||
Possible Values: [ERROR, WARN, INFO, DEBUG, TRACE]
|
||||
--network, -n
|
||||
Network to use
|
||||
Possible Values: [mainnet, testnet, regtest]
|
||||
Possible Values: [mainnet, testnet, regtest, signet]
|
||||
```
|
||||
|
||||
As a fallback, the network (mainnet, testnet or regtest) can also be set using an environment variable `SPARROW_NETWORK`. For example:
|
||||
As a fallback, the network (mainnet, testnet, regtest or signet) can also be set using an environment variable `SPARROW_NETWORK`. For example:
|
||||
|
||||
`export SPARROW_NETWORK=testnet`
|
||||
|
||||
@@ -56,7 +59,7 @@ A final fallback which can be useful when running the Sparrow binary is to creat
|
||||
|
||||
Note that if you are connecting to an Electrum server when using testnet, that server will need to running on testnet configuration as well.
|
||||
|
||||
When not explicitly configured using the command line argument above, Sparrow stores it's mainnet config file, log file and wallets in a home folder location appropriate to the operating system:
|
||||
When not explicitly configured using the command line argument above, Sparrow stores its mainnet config file, log file and wallets in a home folder location appropriate to the operating system:
|
||||
|
||||
Platform | Location
|
||||
-------- | --------
|
||||
@@ -64,7 +67,7 @@ OSX | ~/.sparrow
|
||||
Linux | ~/.sparrow
|
||||
Windows | %APPDATA%/Sparrow
|
||||
|
||||
Testnet and regtest configurations (along with their wallets) are stored in subfolders to allow easy switching between networks.
|
||||
Testnet, regtest and signet configurations (along with their wallets) are stored in subfolders to allow easy switching between networks.
|
||||
|
||||
## Reporting Issues
|
||||
|
||||
|
||||
+7
-2
@@ -5,7 +5,7 @@ plugins {
|
||||
id 'org.beryx.jlink' version '2.22.0'
|
||||
}
|
||||
|
||||
def sparrowVersion = '0.9.9'
|
||||
def sparrowVersion = '1.3.0'
|
||||
def os = org.gradle.internal.os.OperatingSystem.current()
|
||||
def osName = os.getFamilyName()
|
||||
if(os.macOsX) {
|
||||
@@ -69,6 +69,8 @@ dependencies {
|
||||
exclude group: 'org.openjfx', module: 'javafx-web'
|
||||
exclude group: 'org.openjfx', module: 'javafx-media'
|
||||
}
|
||||
implementation('dev.bwt:bwt-jni:0.1.7')
|
||||
implementation('net.sourceforge.javacsv:javacsv:2.0')
|
||||
testImplementation('junit:junit:4.12')
|
||||
}
|
||||
|
||||
@@ -144,10 +146,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: 0567409742...e974c3f422
@@ -21,7 +21,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.9.9</string>
|
||||
<string>1.3.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 |
@@ -22,4 +22,8 @@ public class AboutController {
|
||||
public void close(ActionEvent event) {
|
||||
stage.close();
|
||||
}
|
||||
|
||||
public void openDonate(ActionEvent event) {
|
||||
AppServices.get().getApplication().getHostServices().showDocument("https://sparrowwallet.com/donate");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ import com.sparrowwallet.sparrow.control.*;
|
||||
import com.sparrowwallet.sparrow.event.*;
|
||||
import com.sparrowwallet.sparrow.io.*;
|
||||
import com.sparrowwallet.sparrow.net.ElectrumServer;
|
||||
import com.sparrowwallet.sparrow.net.ServerType;
|
||||
import com.sparrowwallet.sparrow.preferences.PreferenceGroup;
|
||||
import com.sparrowwallet.sparrow.preferences.PreferencesDialog;
|
||||
import com.sparrowwallet.sparrow.transaction.TransactionController;
|
||||
import com.sparrowwallet.sparrow.transaction.TransactionData;
|
||||
@@ -32,6 +34,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;
|
||||
@@ -98,6 +101,9 @@ public class AppController implements Initializable {
|
||||
@FXML
|
||||
private CheckMenuItem showTxHex;
|
||||
|
||||
@FXML
|
||||
private MenuItem refreshWallet;
|
||||
|
||||
@FXML
|
||||
private StackPane rootStack;
|
||||
|
||||
@@ -110,8 +116,12 @@ public class AppController implements Initializable {
|
||||
@FXML
|
||||
private UnlabeledToggleSwitch serverToggle;
|
||||
|
||||
private PauseTransition wait;
|
||||
|
||||
private Timeline statusTimeline;
|
||||
|
||||
private final Set<Wallet> loadingWallets = new LinkedHashSet<>();
|
||||
|
||||
@Override
|
||||
public void initialize(URL location, ResourceBundle resources) {
|
||||
EventManager.get().register(this);
|
||||
@@ -170,7 +180,7 @@ public class AppController implements Initializable {
|
||||
boolean walletAdded = c.getAddedSubList().stream().anyMatch(tab -> ((TabData)tab.getUserData()).getType() == TabData.TabType.WALLET);
|
||||
boolean walletRemoved = c.getRemoved().stream().anyMatch(tab -> ((TabData)tab.getUserData()).getType() == TabData.TabType.WALLET);
|
||||
if(walletAdded || walletRemoved) {
|
||||
EventManager.get().post(new OpenWalletsEvent(tabs.getScene().getWindow(), getOpenWallets()));
|
||||
EventManager.get().post(new OpenWalletsEvent(tabs.getScene().getWindow(), getOpenWalletTabData()));
|
||||
}
|
||||
|
||||
List<WalletTabData> closedWalletTabs = c.getRemoved().stream().map(tab -> (TabData)tab.getUserData())
|
||||
@@ -194,7 +204,7 @@ public class AppController implements Initializable {
|
||||
|
||||
tabs.getScene().getWindow().setOnCloseRequest(event -> {
|
||||
EventManager.get().unregister(this);
|
||||
EventManager.get().post(new OpenWalletsEvent(tabs.getScene().getWindow(), Collections.emptyMap()));
|
||||
EventManager.get().post(new OpenWalletsEvent(tabs.getScene().getWindow(), Collections.emptyList()));
|
||||
});
|
||||
|
||||
BitcoinUnit unit = Config.get().getBitcoinUnit();
|
||||
@@ -220,12 +230,18 @@ 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())));
|
||||
|
||||
serverToggle.setSelected(isOnline());
|
||||
setServerType(Config.get().getServerType());
|
||||
serverToggle.setSelected(isConnected());
|
||||
serverToggle.setDisable(Config.get().getServerType() == null);
|
||||
onlineProperty().bindBidirectional(serverToggle.selectedProperty());
|
||||
onlineProperty().addListener((observable, oldValue, newValue) -> {
|
||||
Platform.runLater(() -> setServerToggleTooltip(getCurrentBlockHeight()));
|
||||
});
|
||||
serverToggle.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
|
||||
Config.get().setMode(serverToggle.isSelected() ? Mode.ONLINE : Mode.OFFLINE);
|
||||
});
|
||||
|
||||
openTransactionIdItem.disableProperty().bind(onlineProperty().not());
|
||||
}
|
||||
@@ -247,6 +263,28 @@ public class AppController implements Initializable {
|
||||
}
|
||||
}
|
||||
|
||||
public void showIntroduction(ActionEvent event) {
|
||||
WelcomeDialog welcomeDialog = new WelcomeDialog();
|
||||
Optional<Mode> optionalMode = welcomeDialog.showAndWait();
|
||||
if(optionalMode.isPresent() && optionalMode.get().equals(Mode.ONLINE)) {
|
||||
PreferencesDialog preferencesDialog = new PreferencesDialog(PreferenceGroup.SERVER);
|
||||
preferencesDialog.showAndWait();
|
||||
}
|
||||
}
|
||||
|
||||
public void showDocumentation(ActionEvent event) throws IOException {
|
||||
AppServices.get().getApplication().getHostServices().showDocument("https://sparrowwallet.com/docs");
|
||||
}
|
||||
|
||||
public void showLogFile(ActionEvent event) throws IOException {
|
||||
File logFile = new File(Storage.getSparrowHome(), "sparrow.log");
|
||||
if(logFile.exists()) {
|
||||
AppServices.get().getApplication().getHostServices().showDocument(logFile.toPath().toUri().toString());
|
||||
} else {
|
||||
AppServices.showErrorDialog("Log file unavailable", "Cannot find log file at " + logFile.getCanonicalPath());
|
||||
}
|
||||
}
|
||||
|
||||
public void showAbout(ActionEvent event) {
|
||||
Stage aboutStage = getAboutStage();
|
||||
aboutStage.show();
|
||||
@@ -435,6 +473,10 @@ public class AppController implements Initializable {
|
||||
|
||||
File file = fileChooser.showSaveDialog(window);
|
||||
if(file != null) {
|
||||
if(!saveTx && !file.getName().toLowerCase().endsWith(".psbt")) {
|
||||
file = new File(file.getAbsolutePath() + ".psbt");
|
||||
}
|
||||
|
||||
try {
|
||||
try(PrintWriter writer = new PrintWriter(file, StandardCharsets.UTF_8)) {
|
||||
if(saveTx) {
|
||||
@@ -451,21 +493,30 @@ public class AppController implements Initializable {
|
||||
}
|
||||
}
|
||||
|
||||
public Map<Wallet, Storage> getOpenWallets() {
|
||||
Map<Wallet, Storage> openWallets = new LinkedHashMap<>();
|
||||
|
||||
public List<WalletTabData> getOpenWalletTabData() {
|
||||
List<WalletTabData> openWalletTabData = new ArrayList<>();
|
||||
|
||||
for(Tab tab : tabs.getTabs()) {
|
||||
TabData tabData = (TabData)tab.getUserData();
|
||||
if(tabData.getType() == TabData.TabType.WALLET) {
|
||||
WalletTabData walletTabData = (WalletTabData) tabData;
|
||||
openWallets.put(walletTabData.getWallet(), walletTabData.getStorage());
|
||||
openWalletTabData.add((WalletTabData)tabData);
|
||||
}
|
||||
}
|
||||
|
||||
return openWalletTabData;
|
||||
}
|
||||
|
||||
public Map<Wallet, Storage> getOpenWallets() {
|
||||
Map<Wallet, Storage> openWallets = new LinkedHashMap<>();
|
||||
|
||||
for(WalletTabData walletTabData : getOpenWalletTabData()){
|
||||
openWallets.put(walletTabData.getWallet(), walletTabData.getStorage());
|
||||
}
|
||||
|
||||
return openWallets;
|
||||
}
|
||||
|
||||
|
||||
public void selectTab(Wallet wallet) {
|
||||
for(Tab tab : tabs.getTabs()) {
|
||||
TabData tabData = (TabData) tab.getUserData();
|
||||
@@ -521,17 +572,29 @@ public class AppController implements Initializable {
|
||||
}
|
||||
|
||||
private void setServerToggleTooltip(Integer currentBlockHeight) {
|
||||
serverToggle.setTooltip(new Tooltip(AppServices.isOnline() ? "Connected to " + Config.get().getElectrumServer() + (currentBlockHeight != null ? " at height " + currentBlockHeight : "") : "Disconnected"));
|
||||
Tooltip tooltip = new Tooltip(getServerToggleTooltipText(currentBlockHeight));
|
||||
tooltip.setShowDuration(Duration.seconds(15));
|
||||
serverToggle.setTooltip(tooltip);
|
||||
}
|
||||
|
||||
private String getServerToggleTooltipText(Integer currentBlockHeight) {
|
||||
if(AppServices.isConnected()) {
|
||||
return "Connected to " + Config.get().getServerAddress() + (currentBlockHeight != null ? " at height " + currentBlockHeight : "") +
|
||||
(Config.get().getServerType() == ServerType.PUBLIC_ELECTRUM_SERVER ? "\nWarning! You are connected to a public server and sharing your transaction data with it.\nFor better privacy, consider using your own Bitcoin Core node or private Electrum server." : "");
|
||||
}
|
||||
|
||||
return "Disconnected";
|
||||
}
|
||||
|
||||
public void newWallet(ActionEvent event) {
|
||||
WalletNameDialog dlg = new WalletNameDialog();
|
||||
Optional<String> walletName = dlg.showAndWait();
|
||||
if(walletName.isPresent()) {
|
||||
File walletFile = Storage.getWalletFile(walletName.get());
|
||||
Optional<WalletNameDialog.NameAndBirthDate> optNameAndBirthDate = dlg.showAndWait();
|
||||
if(optNameAndBirthDate.isPresent()) {
|
||||
WalletNameDialog.NameAndBirthDate nameAndBirthDate = optNameAndBirthDate.get();
|
||||
File walletFile = Storage.getWalletFile(nameAndBirthDate.getName());
|
||||
Storage storage = new Storage(walletFile);
|
||||
Wallet wallet = new Wallet(walletName.get(), PolicyType.SINGLE, ScriptType.P2WPKH);
|
||||
addWalletTabOrWindow(storage, wallet, false);
|
||||
Wallet wallet = new Wallet(nameAndBirthDate.getName(), PolicyType.SINGLE, ScriptType.P2WPKH, nameAndBirthDate.getBirthDate());
|
||||
addWalletTabOrWindow(storage, wallet, null, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -556,13 +619,13 @@ public class AppController implements Initializable {
|
||||
Storage storage = new Storage(file);
|
||||
FileType fileType = IOUtils.getFileType(file);
|
||||
if(FileType.JSON.equals(fileType)) {
|
||||
Wallet wallet = storage.loadWallet();
|
||||
checkWalletNetwork(wallet);
|
||||
restorePublicKeysFromSeed(wallet, null);
|
||||
if(!wallet.isValid()) {
|
||||
Storage.WalletBackupAndKey walletBackupAndKey = storage.loadWallet();
|
||||
checkWalletNetwork(walletBackupAndKey.wallet);
|
||||
restorePublicKeysFromSeed(walletBackupAndKey.wallet, null);
|
||||
if(!walletBackupAndKey.wallet.isValid()) {
|
||||
throw new IllegalStateException("Wallet file is not valid.");
|
||||
}
|
||||
addWalletTabOrWindow(storage, wallet, forceSameWindow);
|
||||
addWalletTabOrWindow(storage, walletBackupAndKey.wallet, walletBackupAndKey.backupWallet, forceSameWindow);
|
||||
} else if(FileType.BINARY.equals(fileType)) {
|
||||
WalletPasswordDialog dlg = new WalletPasswordDialog(file.getName(), WalletPasswordDialog.PasswordRequirement.LOAD);
|
||||
Optional<SecureString> optionalPassword = dlg.showAndWait();
|
||||
@@ -574,22 +637,25 @@ public class AppController implements Initializable {
|
||||
Storage.LoadWalletService loadWalletService = new Storage.LoadWalletService(storage, password);
|
||||
loadWalletService.setOnSucceeded(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(storage.getWalletFile(), TimedEvent.Action.END, "Done"));
|
||||
Storage.WalletAndKey walletAndKey = loadWalletService.getValue();
|
||||
Storage.WalletBackupAndKey walletBackupAndKey = loadWalletService.getValue();
|
||||
try {
|
||||
checkWalletNetwork(walletAndKey.wallet);
|
||||
restorePublicKeysFromSeed(walletAndKey.wallet, walletAndKey.key);
|
||||
addWalletTabOrWindow(storage, walletAndKey.wallet, forceSameWindow);
|
||||
checkWalletNetwork(walletBackupAndKey.wallet);
|
||||
restorePublicKeysFromSeed(walletBackupAndKey.wallet, walletBackupAndKey.key);
|
||||
addWalletTabOrWindow(storage, walletBackupAndKey.wallet, walletBackupAndKey.backupWallet, forceSameWindow);
|
||||
} catch(Exception e) {
|
||||
showErrorDialog("Error Opening Wallet", e.getMessage());
|
||||
} finally {
|
||||
walletAndKey.key.clear();
|
||||
walletBackupAndKey.key.clear();
|
||||
}
|
||||
});
|
||||
loadWalletService.setOnFailed(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(storage.getWalletFile(), TimedEvent.Action.END, "Failed"));
|
||||
Throwable exception = loadWalletService.getException();
|
||||
if(exception instanceof InvalidPasswordException) {
|
||||
showErrorDialog("Invalid Password", "The wallet password was invalid.");
|
||||
Optional<ButtonType> optResponse = showErrorDialog("Invalid Password", "The wallet password was invalid. Try again?", ButtonType.CANCEL, ButtonType.OK);
|
||||
if(optResponse.isPresent() && optResponse.get().equals(ButtonType.OK)) {
|
||||
Platform.runLater(() -> openWalletFile(file, forceSameWindow));
|
||||
}
|
||||
} else {
|
||||
if(!attemptImportWallet(file, password)) {
|
||||
log.error("Error Opening Wallet", exception);
|
||||
@@ -669,7 +735,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) {
|
||||
@@ -695,8 +765,17 @@ public class AppController implements Initializable {
|
||||
}
|
||||
|
||||
private void addImportedWallet(Wallet wallet) {
|
||||
File walletFile = Storage.getExistingWallet(wallet.getName());
|
||||
WalletNameDialog nameDlg = new WalletNameDialog(wallet.getName());
|
||||
Optional<WalletNameDialog.NameAndBirthDate> optNameAndBirthDate = nameDlg.showAndWait();
|
||||
if(optNameAndBirthDate.isPresent()) {
|
||||
WalletNameDialog.NameAndBirthDate nameAndBirthDate = optNameAndBirthDate.get();
|
||||
wallet.setName(nameAndBirthDate.getName());
|
||||
wallet.setBirthDate(nameAndBirthDate.getBirthDate());
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
File walletFile = Storage.getExistingWallet(wallet.getName());
|
||||
if(walletFile != null) {
|
||||
Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
|
||||
alert.setTitle("Existing wallet found");
|
||||
@@ -728,7 +807,7 @@ public class AppController implements Initializable {
|
||||
if(password.isPresent()) {
|
||||
if(password.get().length() == 0) {
|
||||
storage.setEncryptionPubKey(Storage.NO_PASSWORD_KEY);
|
||||
addWalletTabOrWindow(storage, wallet, false);
|
||||
addWalletTabOrWindow(storage, wallet, null, false);
|
||||
} else {
|
||||
Storage.KeyDerivationService keyDerivationService = new Storage.KeyDerivationService(storage, password.get());
|
||||
keyDerivationService.setOnSucceeded(workerStateEvent -> {
|
||||
@@ -741,7 +820,7 @@ public class AppController implements Initializable {
|
||||
key = new Key(encryptionFullKey.getPrivKeyBytes(), storage.getKeyDeriver().getSalt(), EncryptionType.Deriver.ARGON2);
|
||||
wallet.encrypt(key);
|
||||
storage.setEncryptionPubKey(encryptionPubKey);
|
||||
addWalletTabOrWindow(storage, wallet, false);
|
||||
addWalletTabOrWindow(storage, wallet, null, false);
|
||||
} finally {
|
||||
encryptionFullKey.clear();
|
||||
if(key != null) {
|
||||
@@ -798,7 +877,20 @@ public class AppController implements Initializable {
|
||||
messageSignDialog.showAndWait();
|
||||
}
|
||||
|
||||
public void addWalletTabOrWindow(Storage storage, Wallet wallet, boolean forceSameWindow) {
|
||||
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();
|
||||
walletTabData.getStorage().backupTempWallet();
|
||||
wallet.clearHistory();
|
||||
EventManager.get().post(new WalletSettingsChangedEvent(wallet, pastWallet, walletTabData.getStorage().getWalletFile()));
|
||||
}
|
||||
}
|
||||
|
||||
public void addWalletTabOrWindow(Storage storage, Wallet wallet, Wallet backupWallet, boolean forceSameWindow) {
|
||||
Window existingWalletWindow = AppServices.get().getWindowForWallet(storage);
|
||||
if(existingWalletWindow instanceof Stage) {
|
||||
Stage existingWalletStage = (Stage)existingWalletWindow;
|
||||
@@ -813,13 +905,13 @@ public class AppController implements Initializable {
|
||||
AppController appController = AppServices.newAppWindow(stage);
|
||||
stage.toFront();
|
||||
stage.setX(AppServices.get().getWalletWindowMaxX() + 30);
|
||||
appController.addWalletTab(storage, wallet);
|
||||
appController.addWalletTab(storage, wallet, backupWallet);
|
||||
} else {
|
||||
addWalletTab(storage, wallet);
|
||||
addWalletTab(storage, wallet, backupWallet);
|
||||
}
|
||||
}
|
||||
|
||||
public void addWalletTab(Storage storage, Wallet wallet) {
|
||||
public void addWalletTab(Storage storage, Wallet wallet, Wallet backupWallet) {
|
||||
try {
|
||||
String name = storage.getWalletFile().getName();
|
||||
if(name.endsWith(".json")) {
|
||||
@@ -835,8 +927,10 @@ public class AppController implements Initializable {
|
||||
tab.setContent(walletLoader.load());
|
||||
WalletController controller = walletLoader.getController();
|
||||
|
||||
EventManager.get().post(new WalletOpeningEvent(storage, wallet));
|
||||
|
||||
//Note that only one WalletForm is created per wallet tab, and registered to listen for events. All wallet controllers (except SettingsController) share this instance.
|
||||
WalletForm walletForm = new WalletForm(storage, wallet);
|
||||
WalletForm walletForm = new WalletForm(storage, wallet, backupWallet);
|
||||
EventManager.get().register(walletForm);
|
||||
controller.setWalletForm(walletForm);
|
||||
|
||||
@@ -1019,6 +1113,22 @@ public class AppController implements Initializable {
|
||||
return contextMenu;
|
||||
}
|
||||
|
||||
public void setServerType(ServerType serverType) {
|
||||
if(serverType == ServerType.PUBLIC_ELECTRUM_SERVER && !serverToggle.getStyleClass().contains("public-server")) {
|
||||
serverToggle.getStyleClass().add("public-server");
|
||||
} else {
|
||||
serverToggle.getStyleClass().remove("public-server");
|
||||
}
|
||||
|
||||
if(serverType == ServerType.BITCOIN_CORE && !serverToggle.getStyleClass().contains("core-server")) {
|
||||
serverToggle.getStyleClass().add("core-server");
|
||||
} else {
|
||||
serverToggle.getStyleClass().remove("core-server");
|
||||
}
|
||||
|
||||
serverToggle.setDisable(false);
|
||||
}
|
||||
|
||||
public void setTheme(ActionEvent event) {
|
||||
Theme selectedTheme = (Theme)theme.getSelectedToggle().getUserData();
|
||||
if(Config.get().getTheme() != selectedTheme) {
|
||||
@@ -1040,6 +1150,11 @@ public class AppController implements Initializable {
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void serverTypeChanged(ServerTypeChangedEvent event) {
|
||||
setServerType(event.getServerType());
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void tabSelected(TabSelectedEvent event) {
|
||||
if(tabs.getTabs().contains(event.getTab())) {
|
||||
@@ -1152,7 +1267,10 @@ public class AppController implements Initializable {
|
||||
public void statusUpdated(StatusEvent event) {
|
||||
statusBar.setText(event.getStatus());
|
||||
|
||||
PauseTransition wait = new PauseTransition(Duration.seconds(20));
|
||||
if(wait != null && wait.getStatus() == Animation.Status.RUNNING) {
|
||||
wait.stop();
|
||||
}
|
||||
wait = new PauseTransition(Duration.seconds(event.getShowDuration()));
|
||||
wait.setOnFinished((e) -> {
|
||||
if(statusBar.getText().equals(event.getStatus())) {
|
||||
statusBar.setText("");
|
||||
@@ -1225,6 +1343,130 @@ 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 failed: " + 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 walletTabsClosed(WalletTabsClosedEvent event) {
|
||||
if(event.getClosedWalletTabData().stream().map(WalletTabData::getWallet).anyMatch(loadingWallets::remove) && loadingWallets.isEmpty()) {
|
||||
if(statusBar.getText().equals("Loading transactions...")) {
|
||||
statusBar.setText("");
|
||||
}
|
||||
if(statusTimeline == null || statusTimeline.getStatus() != Animation.Status.RUNNING) {
|
||||
statusBar.setProgress(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletHistoryStarted(WalletHistoryStartedEvent event) {
|
||||
if(AppServices.isConnected() && getOpenWallets().containsKey(event.getWallet())) {
|
||||
statusUpdated(new StatusEvent("Loading transactions...", 120));
|
||||
if(statusTimeline == null || statusTimeline.getStatus() != Animation.Status.RUNNING) {
|
||||
statusBar.setProgress(-1);
|
||||
loadingWallets.add(event.getWallet());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletHistoryFinished(WalletHistoryFinishedEvent event) {
|
||||
if(getOpenWallets().containsKey(event.getWallet())) {
|
||||
if(statusBar.getText().equals("Loading transactions...")) {
|
||||
statusBar.setText("");
|
||||
}
|
||||
if(statusTimeline == null || statusTimeline.getStatus() != Animation.Status.RUNNING) {
|
||||
statusBar.setProgress(0);
|
||||
}
|
||||
loadingWallets.remove(event.getWallet());
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void bwtBootStatus(BwtBootStatusEvent event) {
|
||||
serverToggle.setDisable(true);
|
||||
if(AppServices.isConnecting()) {
|
||||
statusUpdated(new StatusEvent(event.getStatus(), 60));
|
||||
if(statusTimeline == null || statusTimeline.getStatus() != Animation.Status.RUNNING) {
|
||||
statusBar.setProgress(0.01);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void bwtSyncStatus(BwtSyncStatusEvent event) {
|
||||
serverToggle.setDisable(false);
|
||||
if((AppServices.isConnecting() || AppServices.isConnected()) && !event.isCompleted()) {
|
||||
statusUpdated(new StatusEvent("Syncing... (" + event.getProgress() + "% complete, synced to " + event.getTipAsString() + ")"));
|
||||
if(event.getProgress() > 0 && (statusTimeline == null || statusTimeline.getStatus() != Animation.Status.RUNNING)) {
|
||||
statusBar.setProgress((double)event.getProgress() / 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void bwtScanStatus(BwtScanStatusEvent event) {
|
||||
serverToggle.setDisable(true);
|
||||
if((AppServices.isConnecting() || AppServices.isConnected()) && !event.isCompleted()) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void bwtReadyStatus(BwtReadyStatusEvent event) {
|
||||
serverToggle.setDisable(false);
|
||||
if(statusTimeline == null || statusTimeline.getStatus() != Animation.Status.RUNNING) {
|
||||
statusBar.setProgress(0);
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void torBootStatus(TorBootStatusEvent event) {
|
||||
serverToggle.setDisable(true);
|
||||
statusUpdated(new StatusEvent(event.getStatus(), 120));
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void torFailedStatus(TorFailedStatusEvent event) {
|
||||
serverToggle.setDisable(false);
|
||||
statusUpdated(new StatusEvent(event.getStatus()));
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void torReadyStatus(TorReadyStatusEvent event) {
|
||||
serverToggle.setDisable(false);
|
||||
statusUpdated(new StatusEvent(event.getStatus()));
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void newBlock(NewBlockEvent event) {
|
||||
setServerToggleTooltip(event.getHeight());
|
||||
@@ -1278,7 +1520,7 @@ public class AppController implements Initializable {
|
||||
|
||||
@Subscribe
|
||||
public void requestOpenWallets(RequestOpenWalletsEvent event) {
|
||||
EventManager.get().post(new OpenWalletsEvent(tabs.getScene().getWindow(), getOpenWallets()));
|
||||
EventManager.get().post(new OpenWalletsEvent(tabs.getScene().getWindow(), getOpenWalletTabData()));
|
||||
}
|
||||
|
||||
@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;
|
||||
@@ -12,10 +13,7 @@ import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.io.Device;
|
||||
import com.sparrowwallet.sparrow.io.Hwi;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
import com.sparrowwallet.sparrow.net.ElectrumServer;
|
||||
import com.sparrowwallet.sparrow.net.ExchangeSource;
|
||||
import com.sparrowwallet.sparrow.net.MempoolRateSize;
|
||||
import com.sparrowwallet.sparrow.net.VersionCheckService;
|
||||
import com.sparrowwallet.sparrow.net.*;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
@@ -24,19 +22,24 @@ 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;
|
||||
import javafx.scene.control.ButtonType;
|
||||
import javafx.scene.image.Image;
|
||||
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;
|
||||
@@ -46,9 +49,10 @@ 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 PUBLIC_SERVER_RETRY_PERIOD_SECS = 3;
|
||||
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");
|
||||
@@ -57,7 +61,7 @@ public class AppServices {
|
||||
|
||||
private final MainApp application;
|
||||
|
||||
private final Map<Window, Map<Wallet, Storage>> walletWindows = new LinkedHashMap<>();
|
||||
private final Map<Window, List<WalletTabData>> walletWindows = new LinkedHashMap<>();
|
||||
|
||||
private static final BooleanProperty onlineProperty = new SimpleBooleanProperty(false);
|
||||
|
||||
@@ -69,6 +73,8 @@ public class AppServices {
|
||||
|
||||
private VersionCheckService versionCheckService;
|
||||
|
||||
private TorService torService;
|
||||
|
||||
private static Integer currentBlockHeight;
|
||||
|
||||
private static Map<Integer, Double> targetBlockFeeRates;
|
||||
@@ -86,16 +92,11 @@ public class AppServices {
|
||||
private final ChangeListener<Boolean> onlineServicesListener = new ChangeListener<>() {
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean online) {
|
||||
Config.get().setMode(online ? Mode.ONLINE : Mode.OFFLINE);
|
||||
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();
|
||||
@@ -105,20 +106,6 @@ public class AppServices {
|
||||
}
|
||||
};
|
||||
|
||||
private void restartService(ScheduledService<?> service) {
|
||||
if(service.isRunning()) {
|
||||
service.cancel();
|
||||
}
|
||||
|
||||
if(service.getState() == Worker.State.CANCELLED) {
|
||||
service.reset();
|
||||
}
|
||||
|
||||
if(!service.isRunning()) {
|
||||
service.start();
|
||||
}
|
||||
}
|
||||
|
||||
public AppServices(MainApp application) {
|
||||
this.application = application;
|
||||
EventManager.get().register(this);
|
||||
@@ -127,38 +114,83 @@ public class AppServices {
|
||||
public void start() {
|
||||
Config config = Config.get();
|
||||
connectionService = createConnectionService();
|
||||
if(config.getMode() == Mode.ONLINE && config.getElectrumServer() != null && !config.getElectrumServer().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();
|
||||
}
|
||||
|
||||
ratesService = createRatesService(config.getExchangeSource(), config.getFiatCurrency());
|
||||
versionCheckService = createVersionCheckService();
|
||||
if(config.getMode() == Mode.ONLINE && config.isCheckNewVersions()) {
|
||||
versionCheckService.start();
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
||||
if(service.getState() == Worker.State.CANCELLED || service.getState() == Worker.State.FAILED) {
|
||||
service.reset();
|
||||
}
|
||||
|
||||
if(!service.isRunning()) {
|
||||
service.start();
|
||||
}
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
if(connectionService != null) {
|
||||
connectionService.cancel();
|
||||
}
|
||||
|
||||
if(ratesService != null) {
|
||||
ratesService.cancel();
|
||||
}
|
||||
|
||||
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.setPeriod(Duration.seconds(SERVER_PING_PERIOD_SECS));
|
||||
connectionService.setRestartOnFailure(true);
|
||||
|
||||
onlineProperty.removeListener(onlineServicesListener);
|
||||
onlineProperty.setValue(true);
|
||||
onlineProperty.addListener(onlineServicesListener);
|
||||
@@ -171,10 +203,20 @@ public class AppServices {
|
||||
//Close connection here to create a new transport next time we try
|
||||
connectionService.resetConnection();
|
||||
|
||||
if(failEvent.getSource().getException() instanceof ServerConfigException) {
|
||||
connectionService.setRestartOnFailure(false);
|
||||
}
|
||||
|
||||
onlineProperty.removeListener(onlineServicesListener);
|
||||
onlineProperty.setValue(false);
|
||||
onlineProperty.addListener(onlineServicesListener);
|
||||
|
||||
if(Config.get().getServerType() == ServerType.PUBLIC_ELECTRUM_SERVER) {
|
||||
List<String> otherServers = Arrays.stream(PublicElectrumServer.values()).map(PublicElectrumServer::getUrl).filter(url -> !url.equals(Config.get().getPublicElectrumServer())).collect(Collectors.toList());
|
||||
Config.get().setPublicElectrumServer(otherServers.get(new Random().nextInt(otherServers.size())));
|
||||
connectionService.setPeriod(Duration.seconds(PUBLIC_SERVER_RETRY_PERIOD_SECS));
|
||||
}
|
||||
|
||||
log.debug("Connection failed", failEvent.getSource().getException());
|
||||
EventManager.get().post(new ConnectionFailedEvent(failEvent.getSource().getException()));
|
||||
});
|
||||
@@ -183,8 +225,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 -> {
|
||||
@@ -212,7 +256,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();
|
||||
|
||||
@@ -229,6 +273,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);
|
||||
}
|
||||
@@ -248,7 +331,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")));
|
||||
|
||||
@@ -261,21 +344,32 @@ public class AppServices {
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isReducedWindowHeight(Node node) {
|
||||
return (node.getScene() != null && node.getScene().getWindow().getHeight() < 768);
|
||||
}
|
||||
|
||||
public MainApp getApplication() {
|
||||
return application;
|
||||
}
|
||||
|
||||
public Map<Wallet, Storage> getOpenWallets(Window window) {
|
||||
return walletWindows.get(window);
|
||||
public Map<Wallet, Storage> getOpenWallets() {
|
||||
Map<Wallet, Storage> openWallets = new LinkedHashMap<>();
|
||||
for(List<WalletTabData> walletTabDataList : walletWindows.values()) {
|
||||
for(WalletTabData walletTabData : walletTabDataList) {
|
||||
openWallets.put(walletTabData.getWallet(), walletTabData.getStorage());
|
||||
}
|
||||
}
|
||||
|
||||
return openWallets;
|
||||
}
|
||||
|
||||
public Window getWindowForWallet(Storage storage) {
|
||||
Optional<Window> optWindow = walletWindows.entrySet().stream().filter(entry -> entry.getValue().values().stream().anyMatch(storage1 -> storage1.getWalletFile().equals(storage.getWalletFile()))).map(Map.Entry::getKey).findFirst();
|
||||
Optional<Window> optWindow = walletWindows.entrySet().stream().filter(entry -> entry.getValue().stream().anyMatch(walletTabData -> walletTabData.getStorage().getWalletFile().equals(storage.getWalletFile()))).map(Map.Entry::getKey).findFirst();
|
||||
return optWindow.orElse(null);
|
||||
}
|
||||
|
||||
public Window getWindowForPSBT(PSBT psbt) {
|
||||
Optional<Window> optWindow = walletWindows.entrySet().stream().filter(entry -> entry.getValue().keySet().stream().anyMatch(wallet -> wallet.canSign(psbt))).map(Map.Entry::getKey).findFirst();
|
||||
Optional<Window> optWindow = walletWindows.entrySet().stream().filter(entry -> entry.getValue().stream().anyMatch(walletTabData -> walletTabData.getWallet().canSign(psbt))).map(Map.Entry::getKey).findFirst();
|
||||
return optWindow.orElse(null);
|
||||
}
|
||||
|
||||
@@ -283,8 +377,12 @@ public class AppServices {
|
||||
return walletWindows.keySet().stream().mapToDouble(Window::getX).max().orElse(0d);
|
||||
}
|
||||
|
||||
public static boolean isOnline() {
|
||||
return onlineProperty.get();
|
||||
public static boolean isConnecting() {
|
||||
return get().connectionService != null && get().connectionService.isConnecting();
|
||||
}
|
||||
|
||||
public static boolean isConnected() {
|
||||
return onlineProperty.get() && get().connectionService != null && get().connectionService.isConnected();
|
||||
}
|
||||
|
||||
public static BooleanProperty onlineProperty() {
|
||||
@@ -335,14 +433,21 @@ public class AppServices {
|
||||
payjoinURIs.put(bitcoinURI.getAddress(), bitcoinURI);
|
||||
}
|
||||
|
||||
public static void showErrorDialog(String title, String content) {
|
||||
Alert alert = new Alert(Alert.AlertType.ERROR);
|
||||
public static Optional<ButtonType> showWarningDialog(String title, String content, ButtonType... buttons) {
|
||||
return showAlertDialog(title, content, Alert.AlertType.WARNING, buttons);
|
||||
}
|
||||
|
||||
public static Optional<ButtonType> showErrorDialog(String title, String content, ButtonType... buttons) {
|
||||
return showAlertDialog(title, content, Alert.AlertType.ERROR, buttons);
|
||||
}
|
||||
|
||||
public static Optional<ButtonType> showAlertDialog(String title, String content, Alert.AlertType alertType, ButtonType... buttons) {
|
||||
Alert alert = new Alert(alertType, content, buttons);
|
||||
setStageIcon(alert.getDialogPane().getScene().getWindow());
|
||||
alert.getDialogPane().getScene().getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
alert.setTitle(title);
|
||||
alert.setHeaderText(title);
|
||||
alert.setContentText(content);
|
||||
alert.showAndWait();
|
||||
return alert.showAndWait();
|
||||
}
|
||||
|
||||
public static void setStageIcon(Window window) {
|
||||
@@ -364,16 +469,6 @@ public class AppServices {
|
||||
targetBlockFeeRates = event.getTargetBlockFeeRates();
|
||||
addMempoolRateSizes(event.getMempoolRateSizes());
|
||||
minimumRelayFeeRate = event.getMinimumRelayFeeRate();
|
||||
String banner = event.getServerBanner();
|
||||
String status = "Connected to " + Config.get().getElectrumServer() + " 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
|
||||
@@ -406,11 +501,13 @@ public class AppServices {
|
||||
|
||||
@Subscribe
|
||||
public void fiatCurrencySelected(FiatCurrencySelectedEvent event) {
|
||||
ratesService.cancel();
|
||||
if(ratesService != null) {
|
||||
ratesService.cancel();
|
||||
|
||||
if(Config.get().getMode() != Mode.OFFLINE && event.getExchangeSource() != ExchangeSource.NONE) {
|
||||
ratesService = createRatesService(event.getExchangeSource(), event.getCurrency());
|
||||
ratesService.start();
|
||||
if(Config.get().getMode() != Mode.OFFLINE && event.getExchangeSource() != ExchangeSource.NONE) {
|
||||
ratesService = createRatesService(event.getExchangeSource(), event.getCurrency());
|
||||
ratesService.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -431,25 +528,25 @@ public class AppServices {
|
||||
|
||||
@Subscribe
|
||||
public void openWallets(OpenWalletsEvent event) {
|
||||
if(event.getWalletsMap().isEmpty()) {
|
||||
if(event.getWalletTabDataList().isEmpty()) {
|
||||
walletWindows.remove(event.getWindow());
|
||||
} else {
|
||||
walletWindows.put(event.getWindow(), event.getWalletsMap());
|
||||
walletWindows.put(event.getWindow(), event.getWalletTabDataList());
|
||||
}
|
||||
|
||||
List<Map.Entry<Wallet, Storage>> allWallets = walletWindows.values().stream().flatMap(map -> map.entrySet().stream()).collect(Collectors.toList());
|
||||
List<WalletTabData> allWallets = walletWindows.values().stream().flatMap(Collection::stream).collect(Collectors.toList());
|
||||
|
||||
Platform.runLater(() -> {
|
||||
if(!Window.getWindows().isEmpty()) {
|
||||
List<File> walletFiles = allWallets.stream().map(entry -> entry.getValue().getWalletFile()).collect(Collectors.toList());
|
||||
List<File> walletFiles = allWallets.stream().map(walletTabData -> walletTabData.getStorage().getWalletFile()).collect(Collectors.toList());
|
||||
Config.get().setRecentWalletFiles(walletFiles);
|
||||
}
|
||||
});
|
||||
|
||||
boolean usbWallet = false;
|
||||
for(Map.Entry<Wallet, Storage> entry : allWallets) {
|
||||
Wallet wallet = entry.getKey();
|
||||
Storage storage = entry.getValue();
|
||||
for(WalletTabData walletTabData : allWallets) {
|
||||
Wallet wallet = walletTabData.getWallet();
|
||||
Storage storage = walletTabData.getStorage();
|
||||
|
||||
if(!storage.getWalletFile().exists() || wallet.containsSource(KeystoreSource.HW_USB)) {
|
||||
usbWallet = true;
|
||||
@@ -485,4 +582,28 @@ public class AppServices {
|
||||
public void requestDisconnect(RequestDisconnectEvent event) {
|
||||
onlineProperty.set(false);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletSettingsChanged(WalletSettingsChangedEvent event) {
|
||||
restartBwt(event.getWallet());
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletOpening(WalletOpeningEvent event) {
|
||||
restartBwt(event.getWallet());
|
||||
}
|
||||
|
||||
private void restartBwt(Wallet wallet) {
|
||||
if(Config.get().getServerType() == ServerType.BITCOIN_CORE && isConnected() && wallet.isValid()) {
|
||||
connectionService.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void bwtShutdown(BwtShutdownEvent event) {
|
||||
if(onlineProperty().get() && !connectionService.isRunning()) {
|
||||
connectionService.reset();
|
||||
connectionService.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.sparrowwallet.sparrow;
|
||||
|
||||
import com.beust.jcommander.Parameter;
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import org.slf4j.event.Level;
|
||||
|
||||
public class Args {
|
||||
@Parameter(names = { "--dir", "-d" }, description = "Path to Sparrow home folder")
|
||||
@@ -10,6 +11,9 @@ public class Args {
|
||||
@Parameter(names = { "--network", "-n" }, description = "Network to use")
|
||||
public Network network;
|
||||
|
||||
@Parameter(names = { "--level", "-l" }, description = "Set log level")
|
||||
public Level level;
|
||||
|
||||
@Parameter(names = { "--help", "-h" }, description = "Show usage", help = true)
|
||||
public boolean help;
|
||||
}
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
package com.sparrowwallet.sparrow;
|
||||
|
||||
import com.beust.jcommander.JCommander;
|
||||
import com.sparrowwallet.drongo.Drongo;
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.sparrow.control.WelcomeDialog;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5Brands;
|
||||
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.PublicElectrumServer;
|
||||
import com.sparrowwallet.sparrow.net.ServerType;
|
||||
import com.sparrowwallet.sparrow.preferences.PreferenceGroup;
|
||||
import com.sparrowwallet.sparrow.preferences.PreferencesDialog;
|
||||
import javafx.application.Application;
|
||||
import javafx.application.Platform;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.scene.Parent;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.text.Font;
|
||||
import javafx.stage.Stage;
|
||||
import org.controlsfx.glyphfont.GlyphFontRegistry;
|
||||
@@ -28,10 +27,8 @@ import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class MainApp extends Application {
|
||||
private static final Logger log = LoggerFactory.getLogger(MainApp.class);
|
||||
|
||||
public static final String APP_NAME = "Sparrow";
|
||||
public static final String APP_VERSION = "0.9.9";
|
||||
public static final String APP_VERSION = "1.3.0";
|
||||
public static final String APP_HOME_PROPERTY = "sparrow.home";
|
||||
public static final String NETWORK_ENV_PROPERTY = "SPARROW_NETWORK";
|
||||
|
||||
@@ -51,24 +48,38 @@ public class MainApp extends Application {
|
||||
GlyphFontRegistry.register(new FontAwesome5Brands());
|
||||
Font.loadFont(AppServices.class.getResourceAsStream("/font/RobotoMono-Regular.ttf"), 13);
|
||||
|
||||
AppServices.initialize(this);
|
||||
|
||||
boolean createNewWallet = false;
|
||||
Mode mode = Config.get().getMode();
|
||||
if(mode == null) {
|
||||
WelcomeDialog welcomeDialog = new WelcomeDialog(getHostServices());
|
||||
WelcomeDialog welcomeDialog = new WelcomeDialog();
|
||||
Optional<Mode> optionalMode = welcomeDialog.showAndWait();
|
||||
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);
|
||||
Optional<Boolean> optNewWallet = preferencesDialog.showAndWait();
|
||||
createNewWallet = optNewWallet.isPresent() && optNewWallet.get();
|
||||
} else if(Network.get() == Network.MAINNET) {
|
||||
Config.get().setServerType(ServerType.PUBLIC_ELECTRUM_SERVER);
|
||||
Config.get().setPublicElectrumServer(PublicElectrumServer.values()[new Random().nextInt(PublicElectrumServer.values().length)].getUrl());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AppServices.initialize(this);
|
||||
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("");
|
||||
}
|
||||
|
||||
System.setProperty(Wallet.ALLOW_DERIVATIONS_MATCHING_OTHER_SCRIPT_TYPES_PROPERTY, Boolean.toString(!Config.get().isValidateDerivationPaths()));
|
||||
|
||||
AppController appController = AppServices.newAppWindow(stage);
|
||||
|
||||
if(createNewWallet) {
|
||||
@@ -96,6 +107,7 @@ public class MainApp extends Application {
|
||||
|
||||
@Override
|
||||
public void stop() throws Exception {
|
||||
AppServices.get().stop();
|
||||
mainStage.close();
|
||||
}
|
||||
|
||||
@@ -108,9 +120,13 @@ public class MainApp extends Application {
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
if(args.level != null) {
|
||||
Drongo.setRootLogLevel(args.level);
|
||||
}
|
||||
|
||||
if(args.dir != null) {
|
||||
log.info("Using configured Sparrow home folder of " + args.dir);
|
||||
System.setProperty(APP_HOME_PROPERTY, args.dir);
|
||||
getLogger().info("Using configured Sparrow home folder of " + args.dir);
|
||||
}
|
||||
|
||||
if(args.network != null) {
|
||||
@@ -121,7 +137,7 @@ public class MainApp extends Application {
|
||||
try {
|
||||
Network.set(Network.valueOf(envNetwork.toUpperCase()));
|
||||
} catch(Exception e) {
|
||||
log.warn("Invalid " + NETWORK_ENV_PROPERTY + " property: " + envNetwork);
|
||||
getLogger().warn("Invalid " + NETWORK_ENV_PROPERTY + " property: " + envNetwork);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -131,10 +147,19 @@ public class MainApp extends Application {
|
||||
Network.set(Network.TESTNET);
|
||||
}
|
||||
|
||||
File signetFlag = new File(Storage.getSparrowHome(), "network-" + Network.SIGNET.getName());
|
||||
if(signetFlag.exists()) {
|
||||
Network.set(Network.SIGNET);
|
||||
}
|
||||
|
||||
if(Network.get() != Network.MAINNET) {
|
||||
log.info("Using " + Network.get() + " configuration");
|
||||
getLogger().info("Using " + Network.get() + " configuration");
|
||||
}
|
||||
|
||||
com.sun.javafx.application.LauncherImpl.launchApplication(MainApp.class, MainAppPreloader.class, argv);
|
||||
}
|
||||
|
||||
private static Logger getLogger() {
|
||||
return LoggerFactory.getLogger(MainApp.class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.sparrowwallet.sparrow;
|
||||
|
||||
import com.sparrowwallet.drongo.LogHandler;
|
||||
import com.sparrowwallet.sparrow.event.TorStatusEvent;
|
||||
import org.slf4j.event.Level;
|
||||
|
||||
public class TorLogHandler implements LogHandler {
|
||||
@Override
|
||||
public void handleLog(String threadName, Level level, String message, String loggerName, long timestamp, StackTraceElement[] callerData) {
|
||||
EventManager.get().post(new TorStatusEvent(message));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package com.sparrowwallet.sparrow;
|
||||
|
||||
import com.sparrowwallet.sparrow.control.UnlabeledToggleSwitch;
|
||||
import javafx.animation.PauseTransition;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.event.Event;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.util.Duration;
|
||||
import org.controlsfx.control.StatusBar;
|
||||
|
||||
public class WelcomeController {
|
||||
@FXML
|
||||
private VBox welcomeBox;
|
||||
|
||||
@FXML
|
||||
private VBox step1;
|
||||
|
||||
@FXML
|
||||
private VBox step2;
|
||||
|
||||
@FXML
|
||||
private VBox step3;
|
||||
|
||||
@FXML
|
||||
private VBox step4;
|
||||
|
||||
@FXML
|
||||
private StatusBar serverStatus;
|
||||
|
||||
@FXML
|
||||
private UnlabeledToggleSwitch serverToggle;
|
||||
|
||||
public void initializeView() {
|
||||
step1.managedProperty().bind(step1.visibleProperty());
|
||||
step2.managedProperty().bind(step2.visibleProperty());
|
||||
step3.managedProperty().bind(step3.visibleProperty());
|
||||
step4.managedProperty().bind(step4.visibleProperty());
|
||||
|
||||
step2.setVisible(false);
|
||||
step3.setVisible(false);
|
||||
step4.setVisible(false);
|
||||
|
||||
welcomeBox.getStyleClass().add("offline");
|
||||
serverStatus.setText("Offline");
|
||||
serverToggle.selectedProperty().addListener((observable, oldValue, newValue) -> {
|
||||
serverStatus.setText(newValue ? "Connected (demonstration only)" : "Offline");
|
||||
});
|
||||
}
|
||||
|
||||
public boolean next() {
|
||||
if(step1.isVisible()) {
|
||||
step1.setVisible(false);
|
||||
step2.setVisible(true);
|
||||
welcomeBox.getStyleClass().clear();
|
||||
welcomeBox.getStyleClass().add("public-electrum");
|
||||
PauseTransition wait = new PauseTransition(Duration.millis(200));
|
||||
wait.setOnFinished((e) -> {
|
||||
serverToggle.setSelected(true);
|
||||
serverStatus.setText("Connected to a Public Server (demonstration only)");
|
||||
});
|
||||
wait.play();
|
||||
return true;
|
||||
}
|
||||
|
||||
if(step2.isVisible()) {
|
||||
step2.setVisible(false);
|
||||
step3.setVisible(true);
|
||||
welcomeBox.getStyleClass().clear();
|
||||
welcomeBox.getStyleClass().add("bitcoin-core");
|
||||
serverToggle.setSelected(true);
|
||||
serverStatus.setText("Connected to Bitcoin Core (demonstration only)");
|
||||
return true;
|
||||
}
|
||||
|
||||
if(step3.isVisible()) {
|
||||
step3.setVisible(false);
|
||||
step4.setVisible(true);
|
||||
welcomeBox.getStyleClass().clear();
|
||||
welcomeBox.getStyleClass().add("private-electrum");
|
||||
serverToggle.setSelected(true);
|
||||
serverStatus.setText("Connected to a Private Electrum Server (demonstration only)");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean back() {
|
||||
if(step2.isVisible()) {
|
||||
step2.setVisible(false);
|
||||
step1.setVisible(true);
|
||||
welcomeBox.getStyleClass().clear();
|
||||
welcomeBox.getStyleClass().add("offline");
|
||||
PauseTransition wait = new PauseTransition(Duration.millis(200));
|
||||
wait.setOnFinished((e) -> {
|
||||
serverToggle.setSelected(false);
|
||||
serverStatus.setText("Offline");
|
||||
});
|
||||
wait.play();
|
||||
return false;
|
||||
}
|
||||
|
||||
if(step3.isVisible()) {
|
||||
step3.setVisible(false);
|
||||
step2.setVisible(true);
|
||||
welcomeBox.getStyleClass().clear();
|
||||
welcomeBox.getStyleClass().add("public-electrum");
|
||||
serverToggle.setSelected(true);
|
||||
serverStatus.setText("Connected to a Public Server (demonstration only)");
|
||||
return true;
|
||||
}
|
||||
|
||||
if(step4.isVisible()) {
|
||||
step4.setVisible(false);
|
||||
step3.setVisible(true);
|
||||
welcomeBox.getStyleClass().clear();
|
||||
welcomeBox.getStyleClass().add("bitcoin-core");
|
||||
serverToggle.setSelected(true);
|
||||
serverStatus.setText("Connected to Bitcoin Core (demonstration only)");
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package com.sparrowwallet.sparrow;
|
||||
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class WelcomeDialog extends Dialog<Mode> {
|
||||
public WelcomeDialog() {
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
|
||||
try {
|
||||
FXMLLoader welcomeLoader = new FXMLLoader(AppServices.class.getResource("welcome.fxml"));
|
||||
dialogPane.setContent(welcomeLoader.load());
|
||||
WelcomeController welcomeController = welcomeLoader.getController();
|
||||
welcomeController.initializeView();
|
||||
|
||||
dialogPane.setPrefWidth(600);
|
||||
dialogPane.setPrefHeight(520);
|
||||
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("welcome.css").toExternalForm());
|
||||
|
||||
final ButtonType nextButtonType = new javafx.scene.control.ButtonType("Next", ButtonBar.ButtonData.OK_DONE);
|
||||
final ButtonType backButtonType = new javafx.scene.control.ButtonType("Back", ButtonBar.ButtonData.LEFT);
|
||||
final ButtonType onlineButtonType = new javafx.scene.control.ButtonType("Configure Server", ButtonBar.ButtonData.APPLY);
|
||||
final ButtonType offlineButtonType = new javafx.scene.control.ButtonType(AppServices.isConnected() ? "Done" : "Later or Offline Mode", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||
dialogPane.getButtonTypes().addAll(nextButtonType, backButtonType, onlineButtonType, offlineButtonType);
|
||||
|
||||
Button nextButton = (Button)dialogPane.lookupButton(nextButtonType);
|
||||
Button backButton = (Button)dialogPane.lookupButton(backButtonType);
|
||||
Button onlineButton = (Button)dialogPane.lookupButton(onlineButtonType);
|
||||
Button offlineButton = (Button)dialogPane.lookupButton(offlineButtonType);
|
||||
|
||||
nextButton.managedProperty().bind(nextButton.visibleProperty());
|
||||
backButton.managedProperty().bind(backButton.visibleProperty());
|
||||
onlineButton.managedProperty().bind(onlineButton.visibleProperty());
|
||||
offlineButton.managedProperty().bind(offlineButton.visibleProperty());
|
||||
|
||||
backButton.setDisable(true);
|
||||
onlineButton.visibleProperty().bind(nextButton.visibleProperty().not());
|
||||
offlineButton.visibleProperty().bind(nextButton.visibleProperty().not());
|
||||
|
||||
nextButton.addEventFilter(ActionEvent.ACTION, event -> {
|
||||
if(!welcomeController.next()) {
|
||||
nextButton.setVisible(false);
|
||||
onlineButton.setDefaultButton(true);
|
||||
}
|
||||
backButton.setDisable(false);
|
||||
event.consume();
|
||||
});
|
||||
|
||||
backButton.addEventFilter(ActionEvent.ACTION, event -> {
|
||||
nextButton.setVisible(true);
|
||||
if(!welcomeController.back()) {
|
||||
backButton.setDisable(true);
|
||||
}
|
||||
event.consume();
|
||||
});
|
||||
|
||||
setResultConverter(dialogButton -> dialogButton == onlineButtonType ? Mode.ONLINE : Mode.OFFLINE);
|
||||
} catch(IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.sparrow.wallet.SendController;
|
||||
import javafx.beans.NamedArg;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.chart.Axis;
|
||||
@@ -27,9 +28,11 @@ public class BlockTargetFeeRatesChart extends LineChart<String, Number> {
|
||||
|
||||
for(Iterator<Integer> targetBlocksIter = targetBlocksFeeRates.keySet().iterator(); targetBlocksIter.hasNext(); ) {
|
||||
Integer targetBlocks = targetBlocksIter.next();
|
||||
String category = targetBlocks + (targetBlocksIter.hasNext() ? "" : "+");
|
||||
XYChart.Data<String, Number> data = new XYChart.Data<>(category, targetBlocksFeeRates.get(targetBlocks));
|
||||
feeRateSeries.getData().add(data);
|
||||
if(SendController.TARGET_BLOCKS_RANGE.contains(targetBlocks)) {
|
||||
String category = targetBlocks + (targetBlocksIter.hasNext() ? "" : "+");
|
||||
XYChart.Data<String, Number> data = new XYChart.Data<>(category, targetBlocksFeeRates.get(targetBlocks));
|
||||
feeRateSeries.getData().add(data);
|
||||
}
|
||||
}
|
||||
|
||||
if(selectedTargetBlocks != null) {
|
||||
|
||||
@@ -2,12 +2,27 @@ package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.WalletDataChangedEvent;
|
||||
import com.sparrowwallet.sparrow.event.WalletHistoryStatusEvent;
|
||||
import com.sparrowwallet.sparrow.event.WalletSettingsChangedEvent;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
import com.sparrowwallet.sparrow.net.ServerType;
|
||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||
import javafx.application.Platform;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Hyperlink;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.TreeTableView;
|
||||
import javafx.scene.layout.StackPane;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Optional;
|
||||
|
||||
public class CoinTreeTable extends TreeTableView<Entry> {
|
||||
private BitcoinUnit bitcoinUnit;
|
||||
@@ -51,10 +66,46 @@ public class CoinTreeTable extends TreeTableView<Entry> {
|
||||
setPlaceholder(new Label("Loading transactions..."));
|
||||
}
|
||||
} else {
|
||||
setPlaceholder(new Label("No transactions"));
|
||||
setPlaceholder(getDefaultPlaceholder(event.getWallet()));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected Node getDefaultPlaceholder(Wallet wallet) {
|
||||
StackPane stackPane = new StackPane();
|
||||
stackPane.getChildren().add(AppServices.isConnecting() ? new Label("Loading transactions...") : new Label("No transactions"));
|
||||
|
||||
if(Config.get().getServerType() == ServerType.BITCOIN_CORE && !AppServices.isConnecting()) {
|
||||
Hyperlink hyperlink = new Hyperlink();
|
||||
hyperlink.setTranslateY(30);
|
||||
hyperlink.setOnAction(event -> {
|
||||
WalletBirthDateDialog dlg = new WalletBirthDateDialog(wallet.getBirthDate());
|
||||
Optional<Date> optDate = dlg.showAndWait();
|
||||
if(optDate.isPresent()) {
|
||||
Storage storage = AppServices.get().getOpenWallets().get(wallet);
|
||||
Wallet pastWallet = wallet.copy();
|
||||
storage.backupTempWallet();
|
||||
wallet.setBirthDate(optDate.get());
|
||||
//Trigger background save of birthdate
|
||||
EventManager.get().post(new WalletDataChangedEvent(wallet));
|
||||
//Trigger full wallet rescan
|
||||
wallet.clearHistory();
|
||||
EventManager.get().post(new WalletSettingsChangedEvent(wallet, pastWallet, storage.getWalletFile()));
|
||||
}
|
||||
});
|
||||
if(wallet.getBirthDate() == null) {
|
||||
hyperlink.setText("Scan for previous transactions?");
|
||||
} else {
|
||||
DateFormat dateFormat = new SimpleDateFormat(DateStringConverter.FORMAT_PATTERN);
|
||||
hyperlink.setText("Scan for transactions earlier than " + dateFormat.format(wallet.getBirthDate()) + "?");
|
||||
}
|
||||
|
||||
stackPane.getChildren().add(hyperlink);
|
||||
}
|
||||
|
||||
stackPane.setAlignment(Pos.CENTER);
|
||||
return stackPane;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import javafx.util.StringConverter;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
public class DateStringConverter extends StringConverter<LocalDate> {
|
||||
public static final String FORMAT_PATTERN = "yyyy/MM/dd";
|
||||
public static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern(FORMAT_PATTERN);
|
||||
|
||||
@Override
|
||||
public String toString(LocalDate date) {
|
||||
if (date != null) {
|
||||
return DATE_FORMATTER.format(date);
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public LocalDate fromString(String string) {
|
||||
if (string != null && !string.isEmpty()) {
|
||||
return LocalDate.parse(string, DATE_FORMATTER);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,22 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.OutputDescriptor;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.AddressDisplayedEvent;
|
||||
import com.sparrowwallet.sparrow.io.Device;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class DeviceAddressDialog extends DeviceDialog<String> {
|
||||
private final Wallet wallet;
|
||||
private final KeyDerivation keyDerivation;
|
||||
private final OutputDescriptor outputDescriptor;
|
||||
|
||||
public DeviceAddressDialog(List<String> operationFingerprints, Wallet wallet, KeyDerivation keyDerivation) {
|
||||
super(operationFingerprints);
|
||||
public DeviceAddressDialog(Wallet wallet, OutputDescriptor outputDescriptor) {
|
||||
super(outputDescriptor.getExtendedPublicKeys().stream().map(extKey -> outputDescriptor.getKeyDerivation(extKey).getMasterFingerprint()).collect(Collectors.toList()));
|
||||
this.wallet = wallet;
|
||||
this.keyDerivation = keyDerivation;
|
||||
this.outputDescriptor = outputDescriptor;
|
||||
|
||||
EventManager.get().register(this);
|
||||
setOnCloseRequest(event -> {
|
||||
@@ -26,7 +26,7 @@ public class DeviceAddressDialog extends DeviceDialog<String> {
|
||||
|
||||
@Override
|
||||
protected DevicePane getDevicePane(Device device) {
|
||||
return new DevicePane(wallet, keyDerivation, device);
|
||||
return new DevicePane(wallet, outputDescriptor, device);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
|
||||
@@ -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() {
|
||||
@@ -99,6 +101,7 @@ public abstract class DeviceDialog<R> extends Dialog<R> {
|
||||
Platform.runLater(() -> EventManager.get().post(new UsbDeviceEvent(devices)));
|
||||
});
|
||||
enumerateService.setOnFailed(workerStateEvent -> {
|
||||
deviceAccordion.getPanes().clear();
|
||||
scanBox.setVisible(true);
|
||||
scanLabel.setText(workerStateEvent.getSource().getException().getMessage());
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.ExtendedKey;
|
||||
import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.OutputDescriptor;
|
||||
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
@@ -15,7 +16,6 @@ import com.sparrowwallet.sparrow.event.MessageSignedEvent;
|
||||
import com.sparrowwallet.sparrow.event.PSBTSignedEvent;
|
||||
import com.sparrowwallet.sparrow.io.Device;
|
||||
import com.sparrowwallet.sparrow.io.Hwi;
|
||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
@@ -25,7 +25,6 @@ import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.*;
|
||||
import org.controlsfx.control.textfield.CustomPasswordField;
|
||||
import org.controlsfx.control.textfield.CustomTextField;
|
||||
import org.controlsfx.control.textfield.TextFields;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.controlsfx.validation.ValidationResult;
|
||||
@@ -36,6 +35,7 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class DevicePane extends TitledDescriptionPane {
|
||||
private static final Logger log = LoggerFactory.getLogger(DevicePane.class);
|
||||
@@ -43,6 +43,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
private final DeviceOperation deviceOperation;
|
||||
private final Wallet wallet;
|
||||
private final PSBT psbt;
|
||||
private final OutputDescriptor outputDescriptor;
|
||||
private final KeyDerivation keyDerivation;
|
||||
private final String message;
|
||||
private final Device device;
|
||||
@@ -63,6 +64,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
this.deviceOperation = DeviceOperation.IMPORT;
|
||||
this.wallet = wallet;
|
||||
this.psbt = null;
|
||||
this.outputDescriptor = null;
|
||||
this.keyDerivation = null;
|
||||
this.message = null;
|
||||
this.device = device;
|
||||
@@ -73,18 +75,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);
|
||||
}
|
||||
@@ -94,6 +85,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
this.deviceOperation = DeviceOperation.SIGN;
|
||||
this.wallet = null;
|
||||
this.psbt = psbt;
|
||||
this.outputDescriptor = null;
|
||||
this.keyDerivation = null;
|
||||
this.message = null;
|
||||
this.device = device;
|
||||
@@ -104,23 +96,18 @@ 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);
|
||||
}
|
||||
|
||||
public DevicePane(Wallet wallet, KeyDerivation keyDerivation, Device device) {
|
||||
public DevicePane(Wallet wallet, OutputDescriptor outputDescriptor, Device device) {
|
||||
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
|
||||
this.deviceOperation = DeviceOperation.DISPLAY_ADDRESS;
|
||||
this.wallet = wallet;
|
||||
this.psbt = null;
|
||||
this.keyDerivation = keyDerivation;
|
||||
this.outputDescriptor = outputDescriptor;
|
||||
this.keyDerivation = null;
|
||||
this.message = null;
|
||||
this.device = device;
|
||||
|
||||
@@ -130,13 +117,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);
|
||||
}
|
||||
@@ -146,6 +127,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
this.deviceOperation = DeviceOperation.SIGN_MESSAGE;
|
||||
this.wallet = wallet;
|
||||
this.psbt = null;
|
||||
this.outputDescriptor = null;
|
||||
this.keyDerivation = keyDerivation;
|
||||
this.message = message;
|
||||
this.device = device;
|
||||
@@ -156,15 +138,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
|
||||
@@ -247,7 +238,8 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
displayAddressButton.managedProperty().bind(displayAddressButton.visibleProperty());
|
||||
displayAddressButton.setVisible(false);
|
||||
|
||||
if(device.getFingerprint() != null && !device.getFingerprint().equals(keyDerivation.getMasterFingerprint())) {
|
||||
List<String> fingerprints = outputDescriptor.getExtendedPublicKeys().stream().map(extKey -> outputDescriptor.getKeyDerivation(extKey).getMasterFingerprint()).collect(Collectors.toList());
|
||||
if(device.getFingerprint() != null && !fingerprints.contains(device.getFingerprint())) {
|
||||
displayAddressButton.setDisable(true);
|
||||
}
|
||||
}
|
||||
@@ -316,7 +308,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);
|
||||
|
||||
@@ -384,7 +376,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
}
|
||||
});
|
||||
sendPinService.setOnFailed(workerStateEvent -> {
|
||||
setError(sendPinService.getException().getMessage(), null);
|
||||
setError("Error", sendPinService.getException().getMessage());
|
||||
enterPinButton.setDisable(false);
|
||||
});
|
||||
setDescription("Unlocking...");
|
||||
@@ -450,14 +442,18 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
getXpubService.setOnSucceeded(workerStateEvent -> {
|
||||
String xpub = getXpubService.getValue();
|
||||
|
||||
Keystore keystore = new Keystore();
|
||||
keystore.setLabel(device.getModel().toDisplayString());
|
||||
keystore.setSource(KeystoreSource.HW_USB);
|
||||
keystore.setWalletModel(device.getModel());
|
||||
keystore.setKeyDerivation(new KeyDerivation(device.getFingerprint(), derivationPath));
|
||||
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(xpub));
|
||||
try {
|
||||
Keystore keystore = new Keystore();
|
||||
keystore.setLabel(device.getModel().toDisplayString());
|
||||
keystore.setSource(KeystoreSource.HW_USB);
|
||||
keystore.setWalletModel(device.getModel());
|
||||
keystore.setKeyDerivation(new KeyDerivation(device.getFingerprint(), derivationPath));
|
||||
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(xpub));
|
||||
|
||||
EventManager.get().post(new KeystoreImportEvent(keystore));
|
||||
EventManager.get().post(new KeystoreImportEvent(keystore));
|
||||
} catch(Exception e) {
|
||||
setError("Could not retrieve xpub", e.getMessage());
|
||||
}
|
||||
});
|
||||
getXpubService.setOnFailed(workerStateEvent -> {
|
||||
setError("Could not retrieve xpub", getXpubService.getException().getMessage());
|
||||
@@ -485,7 +481,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
}
|
||||
|
||||
private void displayAddress() {
|
||||
Hwi.DisplayAddressService displayAddressService = new Hwi.DisplayAddressService(device, passphrase.get(), wallet.getScriptType(), keyDerivation.getDerivationPath());
|
||||
Hwi.DisplayAddressService displayAddressService = new Hwi.DisplayAddressService(device, passphrase.get(), wallet.getScriptType(), outputDescriptor);
|
||||
displayAddressService.setOnSucceeded(successEvent -> {
|
||||
String address = displayAddressService.getValue();
|
||||
EventManager.get().post(new AddressDisplayedEvent(address));
|
||||
|
||||
@@ -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;
|
||||
@@ -24,8 +25,8 @@ import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm");
|
||||
public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
public static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm");
|
||||
|
||||
public EntryCell() {
|
||||
super();
|
||||
@@ -173,9 +174,16 @@ 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();
|
||||
double vSize = tx.getVirtualSize();
|
||||
int inputSize = tx.getInputs().get(0).getLength() + (tx.getInputs().get(0).hasWitness() ? tx.getInputs().get(0).getWitness().getLength() / Transaction.WITNESS_SCALE_FACTOR : 0);
|
||||
List<BlockTransactionHashIndex> walletUtxos = new ArrayList<>(transactionEntry.getWallet().getWalletUtxos().keySet());
|
||||
Collections.shuffle(walletUtxos);
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.Bounds;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.util.Duration;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
|
||||
public class HelpLabel extends Label {
|
||||
@@ -14,8 +19,10 @@ public class HelpLabel extends Label {
|
||||
super("", getHelpGlyph());
|
||||
tooltip = new Tooltip();
|
||||
tooltip.textProperty().bind(helpTextProperty());
|
||||
setTooltip(tooltip);
|
||||
tooltip.setShowDuration(Duration.seconds(15));
|
||||
getStyleClass().add("help-label");
|
||||
|
||||
Platform.runLater(() -> setTooltip(tooltip));
|
||||
}
|
||||
|
||||
private static Glyph getHelpGlyph() {
|
||||
|
||||
@@ -24,6 +24,7 @@ import org.controlsfx.control.textfield.AutoCompletionBinding;
|
||||
import org.controlsfx.control.textfield.CustomTextField;
|
||||
import org.controlsfx.control.textfield.TextFields;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.controlsfx.tools.Borders;
|
||||
import org.controlsfx.validation.ValidationResult;
|
||||
import org.controlsfx.validation.ValidationSupport;
|
||||
import org.controlsfx.validation.Validator;
|
||||
@@ -48,6 +49,7 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
|
||||
private Label invalidLabel;
|
||||
private Button calculateButton;
|
||||
private Button backButton;
|
||||
private Button nextButton;
|
||||
private Button confirmButton;
|
||||
private List<String> generatedMnemonicCode;
|
||||
|
||||
@@ -197,9 +199,7 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
|
||||
confirmButton.managedProperty().bind(confirmButton.visibleProperty());
|
||||
confirmButton.setVisible(false);
|
||||
confirmButton.setDefaultButton(true);
|
||||
confirmButton.setTooltip(new Tooltip("Write down the words above as a backup - you will need to re-enter them to confirm your backup is correct"));
|
||||
buttonPane.getChildren().add(confirmButton);
|
||||
AnchorPane.setRightAnchor(confirmButton, 0.0);
|
||||
confirmButton.setTooltip(new Tooltip("Re-enter the generated word list to confirm your backup is correct"));
|
||||
|
||||
calculateButton = new Button("Calculate Seed");
|
||||
calculateButton.setDisable(true);
|
||||
@@ -218,6 +218,15 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
|
||||
backButton.setTooltip(new Tooltip("Go back to the generated word list"));
|
||||
backButton.setVisible(false);
|
||||
|
||||
nextButton = new Button("Confirm Backup...");
|
||||
nextButton.setOnAction(event -> {
|
||||
confirmRecord();
|
||||
});
|
||||
nextButton.managedProperty().bind(nextButton.visibleProperty());
|
||||
nextButton.setTooltip(new Tooltip("Confirm you have recorded the generated word list"));
|
||||
nextButton.setVisible(false);
|
||||
nextButton.setDefaultButton(true);
|
||||
|
||||
wordEntriesProperty.addListener((ListChangeListener<String>) c -> {
|
||||
boolean empty = true;
|
||||
boolean validWords = true;
|
||||
@@ -249,7 +258,7 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
|
||||
|
||||
HBox rightBox = new HBox();
|
||||
rightBox.setSpacing(10);
|
||||
rightBox.getChildren().addAll(backButton, calculateButton);
|
||||
rightBox.getChildren().addAll(backButton, nextButton, confirmButton, calculateButton);
|
||||
|
||||
buttonPane.getChildren().add(rightBox);
|
||||
AnchorPane.setRightAnchor(rightBox, 0.0);
|
||||
@@ -257,7 +266,9 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
|
||||
vBox.getChildren().add(buttonPane);
|
||||
}
|
||||
|
||||
return vBox;
|
||||
StackPane stackPane = new StackPane();
|
||||
stackPane.getChildren().add(vBox);
|
||||
return stackPane;
|
||||
}
|
||||
|
||||
private void generateNew() {
|
||||
@@ -275,7 +286,7 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
|
||||
showHideLink.setVisible(false);
|
||||
|
||||
calculateButton.setVisible(false);
|
||||
confirmButton.setVisible(true);
|
||||
nextButton.setVisible(true);
|
||||
backButton.setVisible(false);
|
||||
|
||||
if(generatedMnemonicCode.size() != wordsPane.getChildren().size()) {
|
||||
@@ -287,6 +298,36 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
|
||||
wordEntry.getEditor().setText(generatedMnemonicCode.get(i));
|
||||
wordEntry.getEditor().setEditable(false);
|
||||
}
|
||||
|
||||
StackPane wordsStackPane = (StackPane)getContent();
|
||||
if(wordsStackPane.getChildren().size() > 1) {
|
||||
wordsStackPane.getChildren().remove(1);
|
||||
confirmButton.setVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void confirmRecord() {
|
||||
setDescription("Confirm words have been recorded");
|
||||
showHideLink.setVisible(false);
|
||||
|
||||
StackPane wordsPane = (StackPane)getContent();
|
||||
StackPane confirmPane = new StackPane();
|
||||
confirmPane.setMaxWidth(350);
|
||||
confirmPane.setMaxHeight(100);
|
||||
Region region = new Region();
|
||||
region.setMinWidth(confirmPane.getMaxWidth());
|
||||
region.setMinHeight(confirmPane.getMaxHeight());
|
||||
confirmPane.getStyleClass().add("box-overlay");
|
||||
Node wrappedRegion = Borders.wrap(region).lineBorder().innerPadding(0).outerPadding(0).buildAll();
|
||||
Label label = new Label("Have these " + wordEntriesProperty.get().size() + " words been written down?\nIn the next step, you will need to re-enter them.");
|
||||
confirmPane.getChildren().addAll(wrappedRegion, label);
|
||||
wordsPane.getChildren().add(confirmPane);
|
||||
|
||||
setExpanded(true);
|
||||
backButton.setVisible(true);
|
||||
nextButton.setVisible(false);
|
||||
confirmButton.setVisible(true);
|
||||
generateButton.setVisible(false);
|
||||
}
|
||||
|
||||
private void confirmBackup() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -61,7 +61,7 @@ public class TitledDescriptionPane extends TitledPane {
|
||||
|
||||
descriptionLabel = new Label(description);
|
||||
descriptionLabel.getStyleClass().add("description-label");
|
||||
showHideLink = new Hyperlink("Show Details...");
|
||||
showHideLink = new Hyperlink("Details...");
|
||||
showHideLink.managedProperty().bind(showHideLink.visibleProperty());
|
||||
showHideLink.setOnAction(event -> {
|
||||
setExpanded(!this.isExpanded());
|
||||
|
||||
@@ -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());
|
||||
@@ -394,7 +397,7 @@ public class TransactionDiagram extends GridPane {
|
||||
|
||||
String txDesc = "Transaction";
|
||||
Label txLabel = new Label(txDesc);
|
||||
Tooltip tooltip = new Tooltip(walletTx.getTransaction().getLength() + " bytes\n" + walletTx.getTransaction().getVirtualSize() + " vBytes");
|
||||
Tooltip tooltip = new Tooltip(walletTx.getTransaction().getLength() + " bytes\n" + String.format("%.2f", walletTx.getTransaction().getVirtualSize()) + " vBytes");
|
||||
tooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
|
||||
tooltip.getStyleClass().add("transaction-tooltip");
|
||||
txLabel.setTooltip(tooltip);
|
||||
@@ -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);
|
||||
|
||||
@@ -5,7 +5,6 @@ import com.sparrowwallet.sparrow.wallet.Entry;
|
||||
import com.sparrowwallet.sparrow.wallet.TransactionEntry;
|
||||
import com.sparrowwallet.sparrow.wallet.WalletTransactionsEntry;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.TreeTableColumn;
|
||||
import javafx.scene.control.TreeTableView;
|
||||
|
||||
@@ -51,7 +50,7 @@ public class TransactionsTreeTable extends CoinTreeTable {
|
||||
balanceCol.setSortable(true);
|
||||
getColumns().add(balanceCol);
|
||||
|
||||
setPlaceholder(new Label("No transactions"));
|
||||
setPlaceholder(getDefaultPlaceholder(rootEntry.getWallet()));
|
||||
setEditable(true);
|
||||
setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY);
|
||||
dateCol.setSortType(TreeTableColumn.SortType.DESCENDING);
|
||||
|
||||
@@ -3,7 +3,6 @@ package com.sparrowwallet.sparrow.control;
|
||||
import com.sparrowwallet.drongo.wallet.WalletNode;
|
||||
import com.sparrowwallet.sparrow.wallet.*;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.SelectionMode;
|
||||
import javafx.scene.control.TreeTableColumn;
|
||||
import javafx.scene.control.TreeTableView;
|
||||
@@ -69,7 +68,7 @@ public class UtxosTreeTable extends CoinTreeTable {
|
||||
getColumns().add(amountCol);
|
||||
setTreeColumn(amountCol);
|
||||
|
||||
setPlaceholder(new Label("No unspent outputs"));
|
||||
setPlaceholder(getDefaultPlaceholder(rootEntry.getWallet()));
|
||||
setEditable(true);
|
||||
setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY);
|
||||
amountCol.setSortType(TreeTableColumn.SortType.DESCENDING);
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.HBox;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.controlsfx.validation.ValidationResult;
|
||||
import org.controlsfx.validation.ValidationSupport;
|
||||
import org.controlsfx.validation.Validator;
|
||||
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.ZoneId;
|
||||
import java.util.Date;
|
||||
|
||||
public class WalletBirthDateDialog extends Dialog<Date> {
|
||||
private final DatePicker birthDatePicker;
|
||||
|
||||
public WalletBirthDateDialog(Date birthDate) {
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
|
||||
setTitle("Wallet Birth Date");
|
||||
dialogPane.setHeaderText("Select an approximate date earlier than the first wallet transaction:");
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
dialogPane.getButtonTypes().addAll(ButtonType.CANCEL);
|
||||
dialogPane.setPrefWidth(420);
|
||||
dialogPane.setPrefHeight(200);
|
||||
|
||||
Glyph wallet = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.HISTORY);
|
||||
wallet.setFontSize(50);
|
||||
dialogPane.setGraphic(wallet);
|
||||
|
||||
HBox datePickerBox = new HBox(10);
|
||||
Label label = new Label("Start scanning from:");
|
||||
label.setPadding(new Insets(5, 0, 0, 8));
|
||||
datePickerBox.getChildren().add(label);
|
||||
|
||||
birthDatePicker = birthDate == null ? new DatePicker() : new DatePicker(birthDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDate());
|
||||
birthDatePicker.setEditable(false);
|
||||
birthDatePicker.setConverter(new DateStringConverter());
|
||||
|
||||
datePickerBox.getChildren().add(birthDatePicker);
|
||||
|
||||
dialogPane.setContent(datePickerBox);
|
||||
|
||||
ValidationSupport validationSupport = new ValidationSupport();
|
||||
Platform.runLater( () -> {
|
||||
validationSupport.registerValidator(birthDatePicker, Validator.combine(
|
||||
(Control c, LocalDate newValue) -> ValidationResult.fromErrorIf( c, "Birth date not specified", newValue == null)
|
||||
));
|
||||
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
||||
});
|
||||
|
||||
final ButtonType okButtonType = new javafx.scene.control.ButtonType("Rescan Wallet", ButtonBar.ButtonData.OK_DONE);
|
||||
dialogPane.getButtonTypes().addAll(okButtonType);
|
||||
Button okButton = (Button) dialogPane.lookupButton(okButtonType);
|
||||
BooleanBinding isInvalid = Bindings.createBooleanBinding(() -> birthDatePicker.getValue() == null, birthDatePicker.valueProperty());
|
||||
okButton.disableProperty().bind(isInvalid);
|
||||
|
||||
setResultConverter(dialogButton -> dialogButton == okButtonType ? Date.from(birthDatePicker.getValue().atStartOfDay().atZone(ZoneId.systemDefault()).toInstant()) : null);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -2,11 +2,15 @@ package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
import com.sparrowwallet.sparrow.net.ServerType;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.controlsfx.control.textfield.CustomTextField;
|
||||
import org.controlsfx.control.textfield.TextFields;
|
||||
@@ -16,28 +20,74 @@ import org.controlsfx.validation.ValidationSupport;
|
||||
import org.controlsfx.validation.Validator;
|
||||
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
|
||||
|
||||
public class WalletNameDialog extends Dialog<String> {
|
||||
import java.time.LocalDate;
|
||||
import java.time.ZoneId;
|
||||
import java.util.Date;
|
||||
|
||||
public class WalletNameDialog extends Dialog<WalletNameDialog.NameAndBirthDate> {
|
||||
private final CustomTextField name;
|
||||
private final CheckBox existingCheck;
|
||||
private final DatePicker existingPicker;
|
||||
|
||||
public WalletNameDialog() {
|
||||
this.name = (CustomTextField)TextFields.createClearableTextField();
|
||||
this("");
|
||||
}
|
||||
|
||||
public WalletNameDialog(String initialName) {
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
boolean requestBirthDate = (Config.get().getServerType() == null || Config.get().getServerType() == ServerType.BITCOIN_CORE);
|
||||
|
||||
setTitle("Wallet Name");
|
||||
dialogPane.setHeaderText("Enter a name for this wallet:");
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
dialogPane.getButtonTypes().addAll(ButtonType.CANCEL);
|
||||
dialogPane.setPrefWidth(380);
|
||||
dialogPane.setPrefHeight(200);
|
||||
dialogPane.setPrefWidth(460);
|
||||
dialogPane.setPrefHeight(requestBirthDate ? 250 : 200);
|
||||
|
||||
Glyph wallet = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.WALLET);
|
||||
wallet.setFontSize(50);
|
||||
dialogPane.setGraphic(wallet);
|
||||
|
||||
final VBox content = new VBox(10);
|
||||
final VBox content = new VBox(20);
|
||||
name = (CustomTextField)TextFields.createClearableTextField();
|
||||
name.setText(initialName);
|
||||
content.getChildren().add(name);
|
||||
|
||||
HBox existingBox = new HBox(10);
|
||||
existingCheck = new CheckBox("Has existing transactions");
|
||||
existingCheck.setPadding(new Insets(5, 0, 0, 0));
|
||||
existingBox.getChildren().add(existingCheck);
|
||||
|
||||
existingPicker = new DatePicker();
|
||||
existingPicker.setConverter(new DateStringConverter());
|
||||
existingPicker.setEditable(false);
|
||||
existingPicker.setPrefWidth(130);
|
||||
existingPicker.managedProperty().bind(existingPicker.visibleProperty());
|
||||
existingPicker.setVisible(false);
|
||||
existingBox.getChildren().add(existingPicker);
|
||||
|
||||
HelpLabel helpLabel = new HelpLabel();
|
||||
helpLabel.setHelpText("Select an approximate date earlier than the first wallet transaction.");
|
||||
helpLabel.setTranslateY(5);
|
||||
helpLabel.managedProperty().bind(helpLabel.visibleProperty());
|
||||
helpLabel.visibleProperty().bind(existingPicker.visibleProperty());
|
||||
existingBox.getChildren().add(helpLabel);
|
||||
|
||||
existingCheck.selectedProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if(newValue) {
|
||||
existingCheck.setText("Has existing transactions starting from");
|
||||
existingPicker.setVisible(true);
|
||||
} else {
|
||||
existingCheck.setText("Has existing transactions");
|
||||
existingPicker.setVisible(false);
|
||||
}
|
||||
});
|
||||
|
||||
if(requestBirthDate) {
|
||||
content.getChildren().add(existingBox);
|
||||
}
|
||||
|
||||
dialogPane.setContent(content);
|
||||
|
||||
ValidationSupport validationSupport = new ValidationSupport();
|
||||
@@ -46,18 +96,39 @@ public class WalletNameDialog extends Dialog<String> {
|
||||
Validator.createEmptyValidator("Wallet name is required"),
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Wallet name is not unique", Storage.walletExists(newValue))
|
||||
));
|
||||
validationSupport.registerValidator(existingPicker, Validator.combine(
|
||||
(Control c, LocalDate newValue) -> ValidationResult.fromErrorIf( c, "Birth date not specified", existingCheck.isSelected() && newValue == null)
|
||||
));
|
||||
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
||||
});
|
||||
|
||||
final ButtonType okButtonType = new javafx.scene.control.ButtonType("New Wallet", ButtonBar.ButtonData.OK_DONE);
|
||||
final ButtonType okButtonType = new javafx.scene.control.ButtonType("Create Wallet", ButtonBar.ButtonData.OK_DONE);
|
||||
dialogPane.getButtonTypes().addAll(okButtonType);
|
||||
Button okButton = (Button) dialogPane.lookupButton(okButtonType);
|
||||
BooleanBinding isInvalid = Bindings.createBooleanBinding(() ->
|
||||
name.getText().length() == 0 || Storage.walletExists(name.getText()), name.textProperty());
|
||||
name.getText().length() == 0 || Storage.walletExists(name.getText()) || (existingCheck.isSelected() && existingPicker.getValue() == null), name.textProperty(), existingCheck.selectedProperty(), existingPicker.valueProperty());
|
||||
okButton.disableProperty().bind(isInvalid);
|
||||
|
||||
name.setPromptText("Wallet Name");
|
||||
Platform.runLater(name::requestFocus);
|
||||
setResultConverter(dialogButton -> dialogButton == okButtonType ? name.getText() : null);
|
||||
setResultConverter(dialogButton -> dialogButton == okButtonType ? new NameAndBirthDate(name.getText(), existingPicker.getValue()) : null);
|
||||
}
|
||||
|
||||
public static class NameAndBirthDate {
|
||||
private final String name;
|
||||
private final Date birthDate;
|
||||
|
||||
public NameAndBirthDate(String name, LocalDate birthLocalDate) {
|
||||
this.name = name;
|
||||
this.birthDate = (birthLocalDate == null ? null : Date.from(birthLocalDate.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant()));
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public Date getBirthDate() {
|
||||
return birthDate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,17 +22,24 @@ public class WalletPasswordDialog extends Dialog<SecureString> {
|
||||
private final CustomPasswordField passwordConfirm;
|
||||
private final CheckBox backupExisting;
|
||||
private final CheckBox changePassword;
|
||||
private final CheckBox deleteBackups;
|
||||
private boolean addingPassword;
|
||||
|
||||
public WalletPasswordDialog(PasswordRequirement requirement) {
|
||||
this(null, requirement);
|
||||
}
|
||||
|
||||
public WalletPasswordDialog(String walletName, PasswordRequirement requirement) {
|
||||
this(null, requirement, false);
|
||||
}
|
||||
|
||||
public WalletPasswordDialog(String walletName, PasswordRequirement requirement, boolean suggestChangePassword) {
|
||||
this.requirement = requirement;
|
||||
this.password = (CustomPasswordField)TextFields.createClearablePasswordField();
|
||||
this.passwordConfirm = (CustomPasswordField)TextFields.createClearablePasswordField();
|
||||
this.backupExisting = new CheckBox("Backup existing wallet first");
|
||||
this.changePassword = new CheckBox("Change password");
|
||||
this.deleteBackups = new CheckBox("Delete any backups");
|
||||
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
setTitle("Wallet Password" + (walletName != null ? " - " + walletName : ""));
|
||||
@@ -52,13 +59,21 @@ public class WalletPasswordDialog extends Dialog<SecureString> {
|
||||
content.getChildren().add(password);
|
||||
content.getChildren().add(passwordConfirm);
|
||||
|
||||
if(requirement == PasswordRequirement.UPDATE_EMPTY || requirement == PasswordRequirement.UPDATE_SET) {
|
||||
content.getChildren().add(backupExisting);
|
||||
backupExisting.setSelected(true);
|
||||
}
|
||||
|
||||
if(requirement == PasswordRequirement.UPDATE_SET) {
|
||||
content.getChildren().add(changePassword);
|
||||
changePassword.selectedProperty().addListener((observable, oldValue, newValue) -> {
|
||||
backupExisting.setVisible(!newValue);
|
||||
});
|
||||
changePassword.setSelected(suggestChangePassword);
|
||||
}
|
||||
|
||||
if(requirement == PasswordRequirement.UPDATE_EMPTY || requirement == PasswordRequirement.UPDATE_SET) {
|
||||
backupExisting.managedProperty().bind(backupExisting.visibleProperty());
|
||||
deleteBackups.managedProperty().bind(deleteBackups.visibleProperty());
|
||||
deleteBackups.visibleProperty().bind(backupExisting.visibleProperty().not());
|
||||
content.getChildren().addAll(backupExisting, deleteBackups);
|
||||
backupExisting.setSelected(true);
|
||||
deleteBackups.setSelected(true);
|
||||
}
|
||||
|
||||
dialogPane.setContent(content);
|
||||
@@ -87,10 +102,14 @@ public class WalletPasswordDialog extends Dialog<SecureString> {
|
||||
okButton.setText("No Password");
|
||||
passwordConfirm.setVisible(false);
|
||||
passwordConfirm.setManaged(false);
|
||||
backupExisting.setVisible(true);
|
||||
addingPassword = false;
|
||||
} else {
|
||||
okButton.setText("Set Password");
|
||||
passwordConfirm.setVisible(true);
|
||||
passwordConfirm.setManaged(true);
|
||||
backupExisting.setVisible(false);
|
||||
addingPassword = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -103,13 +122,17 @@ public class WalletPasswordDialog extends Dialog<SecureString> {
|
||||
}
|
||||
|
||||
public boolean isBackupExisting() {
|
||||
return backupExisting.isSelected();
|
||||
return !(addingPassword || isChangePassword()) && backupExisting.isSelected();
|
||||
}
|
||||
|
||||
public boolean isChangePassword() {
|
||||
return changePassword.isSelected();
|
||||
}
|
||||
|
||||
public boolean isDeleteBackups() {
|
||||
return (addingPassword || isChangePassword()) && deleteBackups.isSelected();
|
||||
}
|
||||
|
||||
public enum PasswordRequirement {
|
||||
LOAD("Please enter the wallet password:", "Unlock"),
|
||||
UPDATE_NEW("Add a password to the wallet?\nLeave empty for none:", "No Password"),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.Mode;
|
||||
import javafx.application.HostServices;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.controlsfx.control.HyperlinkLabel;
|
||||
import org.controlsfx.control.StatusBar;
|
||||
import org.controlsfx.control.ToggleSwitch;
|
||||
|
||||
public class WelcomeDialog extends Dialog<Mode> {
|
||||
private static final String[] ELECTRUM_SERVERS = new String[]{
|
||||
"ElectrumX (Recommended)", "https://github.com/spesmilo/electrumx",
|
||||
"electrs", "https://github.com/romanz/electrs",
|
||||
"esplora-electrs", "https://github.com/Blockstream/electrs"};
|
||||
|
||||
private final HostServices hostServices;
|
||||
|
||||
public WelcomeDialog(HostServices services) {
|
||||
this.hostServices = services;
|
||||
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
|
||||
setTitle("Welcome to Sparrow");
|
||||
dialogPane.setHeaderText("Welcome to Sparrow!");
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
dialogPane.setPrefWidth(600);
|
||||
dialogPane.setPrefHeight(480);
|
||||
|
||||
Image image = new Image("image/sparrow-small.png", 50, 50, false, false);
|
||||
if (!image.isError()) {
|
||||
ImageView imageView = new ImageView();
|
||||
imageView.setSmooth(false);
|
||||
imageView.setImage(image);
|
||||
dialogPane.setGraphic(imageView);
|
||||
}
|
||||
|
||||
final ButtonType onlineButtonType = new javafx.scene.control.ButtonType("Configure Server", ButtonBar.ButtonData.OK_DONE);
|
||||
final ButtonType offlineButtonType = new javafx.scene.control.ButtonType("Later or Offline Mode", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||
dialogPane.getButtonTypes().addAll(onlineButtonType, offlineButtonType);
|
||||
|
||||
final VBox content = new VBox(20);
|
||||
content.setPadding(new Insets(20, 20, 20, 20));
|
||||
content.getChildren().add(createParagraph("Sparrow can operate in both an online and offline mode. In the online mode it connects to your Electrum server to display transaction history. In the offline mode it is useful as a transaction editor and as an airgapped multisig coordinator."));
|
||||
content.getChildren().add(createParagraph("For privacy and security reasons it is not recommended to use a public Electrum server. Install an Electrum server that connects to your full node to index the blockchain and provide full privacy. Examples include:"));
|
||||
|
||||
VBox linkBox = new VBox();
|
||||
for(int i = 0; i < ELECTRUM_SERVERS.length; i+=2) {
|
||||
linkBox.getChildren().add(createBulletedLink(ELECTRUM_SERVERS[i], ELECTRUM_SERVERS[i+1]));
|
||||
}
|
||||
content.getChildren().add(linkBox);
|
||||
|
||||
content.getChildren().add(createParagraph("You can change your mode at any time using the toggle in the status bar:"));
|
||||
content.getChildren().add(createStatusBar(onlineButtonType, offlineButtonType));
|
||||
|
||||
dialogPane.setContent(content);
|
||||
|
||||
setResultConverter(dialogButton -> dialogButton == onlineButtonType ? Mode.ONLINE : Mode.OFFLINE);
|
||||
}
|
||||
|
||||
private Label createParagraph(String text) {
|
||||
Label label = new Label(text);
|
||||
label.setWrapText(true);
|
||||
|
||||
return label;
|
||||
}
|
||||
|
||||
private HyperlinkLabel createBulletedLink(String name, String url) {
|
||||
String[] nameParts = name.split(" ");
|
||||
HyperlinkLabel label = new HyperlinkLabel(" \u2022 [" + nameParts[0] + "] " + (nameParts.length > 1 ? nameParts[1] : ""));
|
||||
label.setOnAction(event -> {
|
||||
hostServices.showDocument(url);
|
||||
});
|
||||
|
||||
return label;
|
||||
}
|
||||
|
||||
private StatusBar createStatusBar(ButtonType onlineButtonType, ButtonType offlineButtonType) {
|
||||
StatusBar statusBar = new StatusBar();
|
||||
statusBar.setText("Online Mode");
|
||||
statusBar.getRightItems().add(createToggle(statusBar, onlineButtonType, offlineButtonType));
|
||||
|
||||
return statusBar;
|
||||
}
|
||||
|
||||
private ToggleSwitch createToggle(StatusBar statusBar, ButtonType onlineButtonType, ButtonType offlineButtonType) {
|
||||
ToggleSwitch toggleSwitch = new UnlabeledToggleSwitch();
|
||||
toggleSwitch.setStyle("-fx-padding: 1px 0 0 0");
|
||||
|
||||
toggleSwitch.selectedProperty().addListener((observable, oldValue, newValue) -> {
|
||||
Button onlineButton = (Button) getDialogPane().lookupButton(onlineButtonType);
|
||||
onlineButton.setDefaultButton(newValue);
|
||||
Button offlineButton = (Button) getDialogPane().lookupButton(offlineButtonType);
|
||||
offlineButton.setDefaultButton(!newValue);
|
||||
statusBar.setText(newValue ? "Online Mode" : "Offline Mode");
|
||||
});
|
||||
|
||||
toggleSwitch.setSelected(true);
|
||||
return toggleSwitch;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
public class BwtBootStatusEvent extends BwtStatusEvent {
|
||||
public BwtBootStatusEvent(String status) {
|
||||
super(status);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
public class BwtElectrumReadyStatusEvent extends BwtStatusEvent {
|
||||
private final String electrumAddr;
|
||||
|
||||
public BwtElectrumReadyStatusEvent(String status, String electrumAddr) {
|
||||
super(status);
|
||||
this.electrumAddr = electrumAddr;
|
||||
}
|
||||
|
||||
public String getElectrumAddr() {
|
||||
return electrumAddr;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
public class BwtReadyStatusEvent extends BwtStatusEvent {
|
||||
public BwtReadyStatusEvent(String status) {
|
||||
super(status);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
public class BwtScanStatusEvent extends BwtStatusEvent {
|
||||
private final int progress;
|
||||
private final Duration remainingDuration;
|
||||
|
||||
public BwtScanStatusEvent(String status, int progress, Duration remainingDuration) {
|
||||
super(status);
|
||||
this.progress = progress;
|
||||
this.remainingDuration = remainingDuration;
|
||||
}
|
||||
|
||||
public int getProgress() {
|
||||
return progress;
|
||||
}
|
||||
|
||||
public boolean isCompleted() {
|
||||
return progress == 100;
|
||||
}
|
||||
|
||||
public Duration getRemaining() {
|
||||
return remainingDuration;
|
||||
}
|
||||
|
||||
public String getRemainingAsString() {
|
||||
if(remainingDuration != null) {
|
||||
if(progress < 30) {
|
||||
return Math.round((double)remainingDuration.toSeconds() / 60) + "m";
|
||||
} else {
|
||||
return remainingDuration.toMinutesPart() + "m " + remainingDuration.toSecondsPart() + "s";
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
/**
|
||||
* Empty class used to notify the bwt has shut down.
|
||||
* Note this extends from DisconnectionEvent, which is the more general event fired on any type of disconnection.
|
||||
*/
|
||||
public class BwtShutdownEvent extends DisconnectionEvent {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
public class BwtStatusEvent {
|
||||
private final String status;
|
||||
|
||||
public BwtStatusEvent(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
|
||||
public class BwtSyncStatusEvent extends BwtStatusEvent {
|
||||
private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy/MM/dd HH:mm");
|
||||
|
||||
private final int progress;
|
||||
private final Date tip;
|
||||
|
||||
public BwtSyncStatusEvent(String status, int progress, Date tip) {
|
||||
super(status);
|
||||
this.progress = progress;
|
||||
this.tip = tip;
|
||||
}
|
||||
|
||||
public int getProgress() {
|
||||
return progress;
|
||||
}
|
||||
|
||||
public boolean isCompleted() {
|
||||
return progress == 100;
|
||||
}
|
||||
|
||||
public Date getTip() {
|
||||
return tip;
|
||||
}
|
||||
|
||||
public String getTipAsString() {
|
||||
return tip == null ? "" : DATE_FORMAT.format(tip);
|
||||
}
|
||||
}
|
||||
@@ -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,8 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
/**
|
||||
* Empty class used to signal that the server has been disconnected from.
|
||||
*/
|
||||
public class DisconnectionEvent {
|
||||
|
||||
}
|
||||
@@ -1,14 +1,21 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.wallet.FeeRatesSelection;
|
||||
|
||||
public class FeeRatesSelectionChangedEvent {
|
||||
private final Wallet wallet;
|
||||
private final FeeRatesSelection feeRatesSelection;
|
||||
|
||||
public FeeRatesSelectionChangedEvent(FeeRatesSelection feeRatesSelection) {
|
||||
public FeeRatesSelectionChangedEvent(Wallet wallet, FeeRatesSelection feeRatesSelection) {
|
||||
this.wallet = wallet;
|
||||
this.feeRatesSelection = feeRatesSelection;
|
||||
}
|
||||
|
||||
public Wallet getWallet() {
|
||||
return wallet;
|
||||
}
|
||||
|
||||
public FeeRatesSelection getFeeRateSelection() {
|
||||
return feeRatesSelection;
|
||||
}
|
||||
|
||||
@@ -1,35 +1,47 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.WalletTabData;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
import javafx.stage.Window;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class OpenWalletsEvent {
|
||||
private final Window window;
|
||||
private final Map<Wallet, Storage> walletsMap;
|
||||
private final List<WalletTabData> walletTabDataList;
|
||||
|
||||
public OpenWalletsEvent(Window window, Map<Wallet, Storage> walletsMap) {
|
||||
public OpenWalletsEvent(Window window, List<WalletTabData> walletTabDataList) {
|
||||
this.window = window;
|
||||
this.walletsMap = walletsMap;
|
||||
this.walletTabDataList = walletTabDataList;
|
||||
}
|
||||
|
||||
public Window getWindow() {
|
||||
return window;
|
||||
}
|
||||
|
||||
public List<Wallet> getWallets() {
|
||||
return new ArrayList<>(walletsMap.keySet());
|
||||
}
|
||||
|
||||
public Storage getStorage(Wallet wallet) {
|
||||
return walletsMap.get(wallet);
|
||||
public List<WalletTabData> getWalletTabDataList() {
|
||||
return walletTabDataList;
|
||||
}
|
||||
|
||||
public Map<Wallet, Storage> getWalletsMap() {
|
||||
return walletsMap;
|
||||
Map<Wallet, Storage> openWallets = new LinkedHashMap<>();
|
||||
|
||||
for(WalletTabData walletTabData : walletTabDataList){
|
||||
openWallets.put(walletTabData.getWallet(), walletTabData.getStorage());
|
||||
}
|
||||
|
||||
return openWallets;
|
||||
}
|
||||
|
||||
public List<Wallet> getWallets() {
|
||||
return new ArrayList<>(getWalletsMap().keySet());
|
||||
}
|
||||
|
||||
public Storage getStorage(Wallet wallet) {
|
||||
return getWalletsMap().get(wallet);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.sparrow.net.ServerType;
|
||||
|
||||
public class ServerTypeChangedEvent {
|
||||
private final ServerType serverType;
|
||||
|
||||
public ServerTypeChangedEvent(ServerType serverType) {
|
||||
this.serverType = serverType;
|
||||
}
|
||||
|
||||
public ServerType getServerType() {
|
||||
return serverType;
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,6 @@ public class SettingsChangedEvent {
|
||||
}
|
||||
|
||||
public enum Type {
|
||||
POLICY, SCRIPT_TYPE, MUTLISIG_THRESHOLD, MULTISIG_TOTAL, KEYSTORE_LABEL, KEYSTORE_FINGERPRINT, KEYSTORE_DERIVATION, KEYSTORE_XPUB, GAP_LIMIT;
|
||||
POLICY, SCRIPT_TYPE, MUTLISIG_THRESHOLD, MULTISIG_TOTAL, KEYSTORE_LABEL, KEYSTORE_FINGERPRINT, KEYSTORE_DERIVATION, KEYSTORE_XPUB, GAP_LIMIT, BIRTH_DATE;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,25 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
public class StatusEvent {
|
||||
public static final int DEFAULT_SHOW_DURATION_SECS = 20;
|
||||
|
||||
private final String status;
|
||||
private final int showDuration;
|
||||
|
||||
public StatusEvent(String status) {
|
||||
this(status, DEFAULT_SHOW_DURATION_SECS);
|
||||
}
|
||||
|
||||
public StatusEvent(String status, int showDuration) {
|
||||
this.status = status;
|
||||
this.showDuration = showDuration;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public int getShowDuration() {
|
||||
return showDuration;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,9 @@ package com.sparrowwallet.sparrow.event;
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletNode;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -12,13 +14,19 @@ import java.util.stream.Collectors;
|
||||
*
|
||||
*/
|
||||
public class WalletHistoryChangedEvent extends WalletChangedEvent {
|
||||
private final Storage storage;
|
||||
private final List<WalletNode> historyChangedNodes;
|
||||
|
||||
public WalletHistoryChangedEvent(Wallet wallet, List<WalletNode> historyChangedNodes) {
|
||||
public WalletHistoryChangedEvent(Wallet wallet, Storage storage, List<WalletNode> historyChangedNodes) {
|
||||
super(wallet);
|
||||
this.storage = storage;
|
||||
this.historyChangedNodes = historyChangedNodes;
|
||||
}
|
||||
|
||||
public File getWalletFile() {
|
||||
return storage.getWalletFile();
|
||||
}
|
||||
|
||||
public List<WalletNode> getHistoryChangedNodes() {
|
||||
return historyChangedNodes;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
public class WalletHistoryFinishedEvent extends WalletHistoryStatusEvent {
|
||||
public WalletHistoryFinishedEvent(Wallet wallet) {
|
||||
super(wallet, false);
|
||||
}
|
||||
|
||||
public WalletHistoryFinishedEvent(Wallet wallet, String errorMessage) {
|
||||
super(wallet, errorMessage);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
public class WalletHistoryStartedEvent extends WalletHistoryStatusEvent {
|
||||
public WalletHistoryStartedEvent(Wallet wallet) {
|
||||
super(wallet, true);
|
||||
}
|
||||
}
|
||||
@@ -4,27 +4,27 @@ import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
public class WalletHistoryStatusEvent {
|
||||
private final Wallet wallet;
|
||||
private final boolean loaded;
|
||||
private final boolean loading;
|
||||
private final String statusMessage;
|
||||
private final String errorMessage;
|
||||
|
||||
public WalletHistoryStatusEvent(Wallet wallet, boolean loaded) {
|
||||
public WalletHistoryStatusEvent(Wallet wallet, boolean loading) {
|
||||
this.wallet = wallet;
|
||||
this.loaded = loaded;
|
||||
this.loading = loading;
|
||||
this.statusMessage = null;
|
||||
this.errorMessage = null;
|
||||
}
|
||||
|
||||
public WalletHistoryStatusEvent(Wallet wallet,boolean loaded, String statusMessage) {
|
||||
public WalletHistoryStatusEvent(Wallet wallet, boolean loading, String statusMessage) {
|
||||
this.wallet = wallet;
|
||||
this.loaded = false;
|
||||
this.loading = loading;
|
||||
this.statusMessage = statusMessage;
|
||||
this.errorMessage = null;
|
||||
}
|
||||
|
||||
public WalletHistoryStatusEvent(Wallet wallet,String errorMessage) {
|
||||
public WalletHistoryStatusEvent(Wallet wallet, String errorMessage) {
|
||||
this.wallet = wallet;
|
||||
this.loaded = false;
|
||||
this.loading = false;
|
||||
this.statusMessage = null;
|
||||
this.errorMessage = errorMessage;
|
||||
}
|
||||
@@ -34,11 +34,7 @@ public class WalletHistoryStatusEvent {
|
||||
}
|
||||
|
||||
public boolean isLoading() {
|
||||
return !loaded;
|
||||
}
|
||||
|
||||
public boolean isLoaded() {
|
||||
return loaded;
|
||||
return loading;
|
||||
}
|
||||
|
||||
public String getStatusMessage() {
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
|
||||
public class WalletOpeningEvent {
|
||||
private final Storage storage;
|
||||
private final Wallet wallet;
|
||||
|
||||
public WalletOpeningEvent(Storage storage, Wallet wallet) {
|
||||
this.storage = storage;
|
||||
this.wallet = wallet;
|
||||
}
|
||||
|
||||
public Storage getStorage() {
|
||||
return storage;
|
||||
}
|
||||
|
||||
public Wallet getWallet() {
|
||||
return wallet;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ public class FontAwesome5 extends GlyphFont {
|
||||
* The individual glyphs offered by the FontAwesome5 font.
|
||||
*/
|
||||
public static enum Glyph implements INamedCharacter {
|
||||
ARROW_CIRCLE_DOWN('\uf0ab'),
|
||||
ANGLE_DOUBLE_RIGHT('\uf101'),
|
||||
ARROW_DOWN('\uf063'),
|
||||
ARROW_UP('\uf062'),
|
||||
@@ -29,14 +30,17 @@ public class FontAwesome5 extends GlyphFont {
|
||||
EXTERNAL_LINK_ALT('\uf35d'),
|
||||
ELLIPSIS_H('\uf141'),
|
||||
EYE('\uf06e'),
|
||||
FILE_CSV('\uf6dd'),
|
||||
HAND_HOLDING('\uf4bd'),
|
||||
HAND_HOLDING_MEDICAL('\ue05c'),
|
||||
HISTORY('\uf1da'),
|
||||
KEY('\uf084'),
|
||||
LAPTOP('\uf109'),
|
||||
LOCK('\uf023'),
|
||||
LOCK_OPEN('\uf3c1'),
|
||||
PEN_FANCY('\uf5ac'),
|
||||
PLUS('\uf067'),
|
||||
PLUS_CIRCLE('\uf055'),
|
||||
QRCODE('\uf029'),
|
||||
QUESTION_CIRCLE('\uf059'),
|
||||
RANDOM('\uf074'),
|
||||
@@ -44,7 +48,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import com.sparrowwallet.drongo.wallet.*;
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class CoboVaultSinglesig implements KeystoreFileImport, WalletImport {
|
||||
@Override
|
||||
@@ -20,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
|
||||
@@ -32,7 +33,7 @@ public class CoboVaultSinglesig implements KeystoreFileImport, WalletImport {
|
||||
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
|
||||
try {
|
||||
Gson gson = new Gson();
|
||||
CoboVaultSinglesigKeystore coboKeystore = gson.fromJson(new InputStreamReader(inputStream), CoboVaultSinglesigKeystore.class);
|
||||
CoboVaultSinglesigKeystore coboKeystore = gson.fromJson(new InputStreamReader(inputStream, StandardCharsets.UTF_8), CoboVaultSinglesigKeystore.class);
|
||||
|
||||
if(coboKeystore.MasterFingerprint == null || coboKeystore.AccountKeyPath == null || coboKeystore.ExtPubKey == null) {
|
||||
throw new ImportException("Not a valid " + getName() + " keystore export");
|
||||
|
||||
@@ -13,6 +13,7 @@ import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
@@ -30,7 +31,7 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
|
||||
|
||||
@Override
|
||||
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
|
||||
InputStreamReader reader = new InputStreamReader(inputStream);
|
||||
InputStreamReader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
|
||||
ColdcardKeystore cck = Storage.getGson().fromJson(reader, ColdcardKeystore.class);
|
||||
|
||||
Keystore keystore = new Keystore("Coldcard");
|
||||
@@ -48,8 +49,8 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
|
||||
keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.p2sh_deriv));
|
||||
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(cck.p2sh));
|
||||
} else if(scriptType.equals(ScriptType.P2SH_P2WSH)) {
|
||||
keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.p2wsh_p2sh_deriv));
|
||||
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(cck.p2wsh_p2sh));
|
||||
keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.p2wsh_p2sh_deriv != null ? cck.p2wsh_p2sh_deriv : cck.p2sh_p2wsh_deriv));
|
||||
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(cck.p2wsh_p2sh != null ? cck.p2wsh_p2sh : cck.p2sh_p2wsh));
|
||||
} else if(scriptType.equals(ScriptType.P2WSH)) {
|
||||
keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.p2wsh_deriv));
|
||||
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(cck.p2wsh));
|
||||
@@ -65,6 +66,8 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
|
||||
public String p2sh;
|
||||
public String p2wsh_p2sh_deriv;
|
||||
public String p2wsh_p2sh;
|
||||
public String p2sh_p2wsh_deriv;
|
||||
public String p2sh_p2wsh;
|
||||
public String p2wsh_deriv;
|
||||
public String p2wsh;
|
||||
public String xpub;
|
||||
@@ -92,7 +95,7 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
|
||||
String derivation = null;
|
||||
|
||||
try {
|
||||
List<String> lines = CharStreams.readLines(new InputStreamReader(inputStream));
|
||||
List<String> lines = CharStreams.readLines(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
|
||||
for (String line : lines) {
|
||||
line = line.trim();
|
||||
if (line.isEmpty()) {
|
||||
@@ -219,4 +222,9 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
|
||||
public boolean isKeystoreImportScannable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isWalletExportScannable() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.lang.reflect.Type;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
|
||||
public class ColdcardSinglesig implements KeystoreFileImport, WalletImport {
|
||||
@@ -53,10 +54,10 @@ public class ColdcardSinglesig implements KeystoreFileImport, WalletImport {
|
||||
Gson gson = new Gson();
|
||||
Type stringStringMap = new TypeToken<Map<String, JsonElement>>() {
|
||||
}.getType();
|
||||
Map<String, JsonElement> map = gson.fromJson(new InputStreamReader(inputStream), stringStringMap);
|
||||
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();
|
||||
@@ -66,7 +67,7 @@ public class ColdcardSinglesig implements KeystoreFileImport, WalletImport {
|
||||
ColdcardKeystore ck = gson.fromJson(map.get(key), ColdcardKeystore.class);
|
||||
|
||||
if(ck.name != null) {
|
||||
ScriptType ckScriptType = ScriptType.valueOf(ck.name.replace("p2wpkh-p2sh", "p2sh_p2wpkh").toUpperCase());
|
||||
ScriptType ckScriptType = ScriptType.valueOf(ck.name.replace("p2wpkh-p2sh", "p2sh_p2wpkh").replace("p2sh-p2wpkh", "p2sh_p2wpkh").toUpperCase());
|
||||
if(ckScriptType.equals(scriptType)) {
|
||||
Keystore keystore = new Keystore();
|
||||
keystore.setLabel(getName());
|
||||
@@ -106,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,8 +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.ExchangeSource;
|
||||
import com.sparrowwallet.sparrow.net.FeeRatesSource;
|
||||
import com.sparrowwallet.sparrow.net.*;
|
||||
import com.sparrowwallet.sparrow.wallet.FeeRatesSelection;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -26,6 +25,7 @@ public class Config {
|
||||
private FeeRatesSelection feeRatesSelection;
|
||||
private Currency fiatCurrency;
|
||||
private ExchangeSource exchangeSource;
|
||||
private boolean validateDerivationPaths = true;
|
||||
private boolean groupByAddress = true;
|
||||
private boolean includeMempoolChange = true;
|
||||
private boolean notifyNewTransactions = true;
|
||||
@@ -37,6 +37,15 @@ public class Config {
|
||||
private List<File> recentWalletFiles;
|
||||
private Integer keyDerivationPeriod;
|
||||
private File hwi;
|
||||
private boolean hdCapture;
|
||||
private ServerType serverType;
|
||||
private String publicElectrumServer;
|
||||
private String coreServer;
|
||||
private CoreAuthType coreAuthType;
|
||||
private File coreDataDir;
|
||||
private String coreAuth;
|
||||
private Boolean coreMultiWallet;
|
||||
private String coreWallet;
|
||||
private String electrumServer;
|
||||
private File electrumServerCert;
|
||||
private boolean useProxy;
|
||||
@@ -71,7 +80,7 @@ public class Config {
|
||||
if(config != null) {
|
||||
return config;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
} catch(Exception e) {
|
||||
log.error("Error opening " + configFile.getAbsolutePath(), e);
|
||||
//Ignore and assume no config
|
||||
}
|
||||
@@ -133,6 +142,10 @@ public class Config {
|
||||
flush();
|
||||
}
|
||||
|
||||
public boolean isFetchRates() {
|
||||
return getExchangeSource() != ExchangeSource.NONE;
|
||||
}
|
||||
|
||||
public ExchangeSource getExchangeSource() {
|
||||
return exchangeSource;
|
||||
}
|
||||
@@ -142,6 +155,15 @@ public class Config {
|
||||
flush();
|
||||
}
|
||||
|
||||
public boolean isValidateDerivationPaths() {
|
||||
return validateDerivationPaths;
|
||||
}
|
||||
|
||||
public void setValidateDerivationPaths(boolean validateDerivationPaths) {
|
||||
this.validateDerivationPaths = validateDerivationPaths;
|
||||
flush();
|
||||
}
|
||||
|
||||
public boolean isGroupByAddress() {
|
||||
return groupByAddress;
|
||||
}
|
||||
@@ -241,6 +263,108 @@ public class Config {
|
||||
flush();
|
||||
}
|
||||
|
||||
public boolean isHdCapture() {
|
||||
return hdCapture;
|
||||
}
|
||||
|
||||
public void setHdCapture(boolean hdCapture) {
|
||||
this.hdCapture = hdCapture;
|
||||
flush();
|
||||
}
|
||||
|
||||
public ServerType getServerType() {
|
||||
return serverType;
|
||||
}
|
||||
|
||||
public void setServerType(ServerType serverType) {
|
||||
this.serverType = serverType;
|
||||
flush();
|
||||
}
|
||||
|
||||
public boolean hasServerAddress() {
|
||||
return getServerAddress() != null && !getServerAddress().isEmpty();
|
||||
}
|
||||
|
||||
public String getServerAddress() {
|
||||
return getServerType() == ServerType.BITCOIN_CORE ? getCoreServer() : (getServerType() == ServerType.PUBLIC_ELECTRUM_SERVER ? getPublicElectrumServer() : 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 getPublicElectrumServer() {
|
||||
return publicElectrumServer;
|
||||
}
|
||||
|
||||
public void setPublicElectrumServer(String publicElectrumServer) {
|
||||
this.publicElectrumServer = publicElectrumServer;
|
||||
flush();
|
||||
}
|
||||
|
||||
public String getCoreServer() {
|
||||
return coreServer;
|
||||
}
|
||||
|
||||
public void setCoreServer(String coreServer) {
|
||||
this.coreServer = coreServer;
|
||||
flush();
|
||||
}
|
||||
|
||||
public CoreAuthType getCoreAuthType() {
|
||||
return coreAuthType;
|
||||
}
|
||||
|
||||
public void setCoreAuthType(CoreAuthType coreAuthType) {
|
||||
this.coreAuthType = coreAuthType;
|
||||
flush();
|
||||
}
|
||||
|
||||
public File getCoreDataDir() {
|
||||
return coreDataDir;
|
||||
}
|
||||
|
||||
public void setCoreDataDir(File coreDataDir) {
|
||||
this.coreDataDir = coreDataDir;
|
||||
flush();
|
||||
}
|
||||
|
||||
public String getCoreAuth() {
|
||||
return coreAuth;
|
||||
}
|
||||
|
||||
public void setCoreAuth(String coreAuth) {
|
||||
this.coreAuth = coreAuth;
|
||||
flush();
|
||||
}
|
||||
|
||||
public Boolean getCoreMultiWallet() {
|
||||
return coreMultiWallet;
|
||||
}
|
||||
|
||||
public void setCoreMultiWallet(Boolean coreMultiWallet) {
|
||||
this.coreMultiWallet = coreMultiWallet;
|
||||
flush();
|
||||
}
|
||||
|
||||
public String getCoreWallet() {
|
||||
return coreWallet;
|
||||
}
|
||||
|
||||
public void setCoreWallet(String coreWallet) {
|
||||
this.coreWallet = coreWallet;
|
||||
flush();
|
||||
}
|
||||
|
||||
public String getElectrumServer() {
|
||||
return electrumServer;
|
||||
}
|
||||
@@ -277,12 +401,13 @@ public class Config {
|
||||
flush();
|
||||
}
|
||||
|
||||
private void flush() {
|
||||
private synchronized void flush() {
|
||||
Gson gson = getGson();
|
||||
try {
|
||||
File configFile = getConfigFile();
|
||||
Writer writer = new FileWriter(configFile);
|
||||
gson.toJson(this, writer);
|
||||
writer.flush();
|
||||
writer.close();
|
||||
} catch (IOException e) {
|
||||
//Ignore
|
||||
|
||||
@@ -58,9 +58,9 @@ public class Electrum implements KeystoreFileImport, WalletImport, WalletExport
|
||||
Reader reader;
|
||||
if(password != null) {
|
||||
ECKey decryptionKey = Pbkdf2KeyDeriver.DEFAULT_INSTANCE.deriveECKey(password);
|
||||
reader = new InputStreamReader(new InflaterInputStream(new ECIESInputStream(inputStream, decryptionKey)));
|
||||
reader = new InputStreamReader(new InflaterInputStream(new ECIESInputStream(inputStream, decryptionKey)), StandardCharsets.UTF_8);
|
||||
} else {
|
||||
reader = new InputStreamReader(inputStream);
|
||||
reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -161,8 +161,18 @@ public class Electrum implements KeystoreFileImport, WalletImport, WalletExport
|
||||
}
|
||||
|
||||
keystore.setSeed(new DeterministicSeed(mnemonic, passphrase, 0, DeterministicSeed.Type.ELECTRUM));
|
||||
if(derivationPath.equals("m/0")) {
|
||||
derivationPath = "m/0'";
|
||||
|
||||
//Ensure the derivation path from the seed matches the provided xpub
|
||||
String[] possibleDerivations = {"m", "m/0", "m/0'"};
|
||||
for(String possibleDerivation : possibleDerivations) {
|
||||
List<ChildNumber> derivation = KeyDerivation.parsePath(possibleDerivation);
|
||||
DeterministicKey derivedKey = keystore.getExtendedMasterPrivateKey().getKey(derivation);
|
||||
DeterministicKey derivedKeyPublicOnly = derivedKey.dropPrivateBytes().dropParent();
|
||||
ExtendedKey xpub = new ExtendedKey(derivedKeyPublicOnly, derivedKey.getParentFingerprint(), derivation.isEmpty() ? ChildNumber.ZERO : derivation.get(derivation.size() - 1));
|
||||
if(xpub.equals(xPub)) {
|
||||
derivationPath = possibleDerivation;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
keystore.setSource(KeystoreSource.SW_WATCH);
|
||||
@@ -173,6 +183,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 +369,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.";
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.google.common.io.ByteStreams;
|
||||
import com.google.common.io.CharStreams;
|
||||
import com.google.gson.*;
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.drongo.OutputDescriptor;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
import com.sparrowwallet.drongo.psbt.PSBTParseException;
|
||||
@@ -29,7 +30,7 @@ import java.util.zip.ZipInputStream;
|
||||
|
||||
public class Hwi {
|
||||
private static final Logger log = LoggerFactory.getLogger(Hwi.class);
|
||||
private static final String TEMP_FILE_PREFIX = "hwi-1.2.1-";
|
||||
private static final String VERSION_PREFIX = "hwi-2.0.0-rc.2";
|
||||
|
||||
private static boolean isPromptActive = false;
|
||||
|
||||
@@ -42,11 +43,17 @@ public class Hwi {
|
||||
command = List.of(getHwiExecutable(Command.ENUMERATE).getAbsolutePath(), Command.ENUMERATE.toString());
|
||||
}
|
||||
|
||||
isPromptActive = true;
|
||||
String output = execute(command);
|
||||
Device[] devices = getGson().fromJson(output, Device[].class);
|
||||
if(devices == null) {
|
||||
throw new ImportException("Error scanning, check devices are ready");
|
||||
}
|
||||
return Arrays.stream(devices).filter(device -> device != null && device.getModel() != null).collect(Collectors.toList());
|
||||
} catch(IOException e) {
|
||||
throw new ImportException(e);
|
||||
} finally {
|
||||
isPromptActive = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,32 +91,33 @@ public class Hwi {
|
||||
if(result.get("xpub") != null) {
|
||||
return result.get("xpub").getAsString();
|
||||
} else {
|
||||
throw new ImportException("Could not retrieve xpub - reconnect your device and try again.");
|
||||
JsonElement error = result.get("error");
|
||||
if(error != null) {
|
||||
throw new ImportException(error.getAsString());
|
||||
} else {
|
||||
throw new ImportException("Could not retrieve xpub - reconnect your device and try again.");
|
||||
}
|
||||
}
|
||||
} catch(IOException e) {
|
||||
throw new ImportException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public String displayAddress(Device device, String passphrase, ScriptType scriptType, String derivationPath) throws DisplayAddressException {
|
||||
public String displayAddress(Device device, String passphrase, ScriptType scriptType, OutputDescriptor outputDescriptor) throws DisplayAddressException {
|
||||
try {
|
||||
if(!List.of(ScriptType.P2PKH, ScriptType.P2SH_P2WPKH, ScriptType.P2WPKH).contains(scriptType)) {
|
||||
throw new IllegalArgumentException("Cannot display address for script type " + scriptType + ": Only single sig types supported");
|
||||
if(!Arrays.asList(ScriptType.SINGLE_HASH_TYPES).contains(scriptType)) {
|
||||
throw new IllegalArgumentException("Cannot display address for script type " + scriptType + ": Only single hash types supported");
|
||||
}
|
||||
|
||||
String type = null;
|
||||
if(scriptType == ScriptType.P2SH_P2WPKH) {
|
||||
type = "--sh_wpkh";
|
||||
} else if(scriptType == ScriptType.P2WPKH) {
|
||||
type = "--wpkh";
|
||||
}
|
||||
//Remove replace once HWI-2.0.0 is released - see https://github.com/bitcoin-core/HWI/pull/488
|
||||
String descriptor = outputDescriptor.toString().replace("sortedmulti", "multi");
|
||||
|
||||
isPromptActive = false;
|
||||
isPromptActive = true;
|
||||
String output;
|
||||
if(passphrase != null && !passphrase.isEmpty() && device.getModel().externalPassphraseEntry()) {
|
||||
output = execute(getDeviceCommand(device, passphrase, Command.DISPLAY_ADDRESS, "--path", derivationPath, type));
|
||||
output = execute(getDeviceCommand(device, passphrase, Command.DISPLAY_ADDRESS, "--desc", descriptor));
|
||||
} else {
|
||||
output = execute(getDeviceCommand(device, Command.DISPLAY_ADDRESS, "--path", derivationPath, type));
|
||||
output = execute(getDeviceCommand(device, Command.DISPLAY_ADDRESS, "--desc", descriptor));
|
||||
}
|
||||
|
||||
JsonObject result = JsonParser.parseString(output).getAsJsonObject();
|
||||
@@ -125,12 +133,14 @@ public class Hwi {
|
||||
}
|
||||
} catch(IOException e) {
|
||||
throw new DisplayAddressException(e);
|
||||
} finally {
|
||||
isPromptActive = false;
|
||||
}
|
||||
}
|
||||
|
||||
public String signMessage(Device device, String passphrase, String message, String derivationPath) throws SignMessageException {
|
||||
try {
|
||||
isPromptActive = false;
|
||||
isPromptActive = true;
|
||||
String output;
|
||||
if(passphrase != null && !passphrase.isEmpty() && device.getModel().externalPassphraseEntry()) {
|
||||
output = execute(getDeviceCommand(device, passphrase, Command.SIGN_MESSAGE, message, derivationPath));
|
||||
@@ -151,6 +161,8 @@ public class Hwi {
|
||||
}
|
||||
} catch(IOException e) {
|
||||
throw new SignMessageException(e);
|
||||
} finally {
|
||||
isPromptActive = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,7 +177,6 @@ public class Hwi {
|
||||
} else {
|
||||
output = execute(getDeviceCommand(device, Command.SIGN_TX, psbtBase64));
|
||||
}
|
||||
isPromptActive = false;
|
||||
|
||||
JsonObject result = JsonParser.parseString(output).getAsJsonObject();
|
||||
if(result.get("psbt") != null) {
|
||||
@@ -187,6 +198,8 @@ public class Hwi {
|
||||
throw new SignTransactionException("Could not sign PSBT", e);
|
||||
} catch(PSBTParseException e) {
|
||||
throw new SignTransactionException("Could not parsed signed PSBT", e);
|
||||
} finally {
|
||||
isPromptActive = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,7 +212,7 @@ public class Hwi {
|
||||
private synchronized File getHwiExecutable(Command command) {
|
||||
File hwiExecutable = Config.get().getHwi();
|
||||
if(hwiExecutable != null && hwiExecutable.exists()) {
|
||||
if(command.isTestFirst() && (!hwiExecutable.getAbsolutePath().contains(TEMP_FILE_PREFIX) || !testHwi(hwiExecutable))) {
|
||||
if(command.isTestFirst() && (!hwiExecutable.getAbsolutePath().contains(VERSION_PREFIX) || !testHwi(hwiExecutable))) {
|
||||
if(Platform.getCurrent() == Platform.OSX) {
|
||||
deleteDirectory(hwiExecutable.getParentFile());
|
||||
} else {
|
||||
@@ -218,8 +231,8 @@ public class Hwi {
|
||||
//The check will still happen on first invocation, but will not thereafter
|
||||
//See https://github.com/bitcoin-core/HWI/issues/327 for details
|
||||
if(platform == Platform.OSX) {
|
||||
InputStream inputStream = Hwi.class.getResourceAsStream("/native/osx/x64/hwi-1.2.1-mac-amd64-signed.zip");
|
||||
Path tempHwiDirPath = Files.createTempDirectory(TEMP_FILE_PREFIX, PosixFilePermissions.asFileAttribute(ownerExecutableWritable));
|
||||
InputStream inputStream = Hwi.class.getResourceAsStream("/native/osx/x64/" + VERSION_PREFIX + "-mac-amd64-signed.zip");
|
||||
Path tempHwiDirPath = Files.createTempDirectory(VERSION_PREFIX, PosixFilePermissions.asFileAttribute(ownerExecutableWritable));
|
||||
File tempHwiDir = tempHwiDirPath.toFile();
|
||||
//tempHwiDir.deleteOnExit();
|
||||
log.debug("Using temp HWI path: " + tempHwiDir.getAbsolutePath());
|
||||
@@ -227,16 +240,20 @@ public class Hwi {
|
||||
File tempExec = null;
|
||||
ZipInputStream zis = new ZipInputStream(inputStream);
|
||||
ZipEntry zipEntry = zis.getNextEntry();
|
||||
while (zipEntry != null) {
|
||||
File newFile = newFile(tempHwiDir, zipEntry, ownerExecutableWritable);
|
||||
//newFile.deleteOnExit();
|
||||
FileOutputStream fos = new FileOutputStream(newFile);
|
||||
ByteStreams.copy(zis, new FileOutputStream(newFile));
|
||||
fos.flush();
|
||||
fos.close();
|
||||
while(zipEntry != null) {
|
||||
if(zipEntry.isDirectory()) {
|
||||
newDirectory(tempHwiDir, zipEntry, ownerExecutableWritable);
|
||||
} else {
|
||||
File newFile = newFile(tempHwiDir, zipEntry, ownerExecutableWritable);
|
||||
//newFile.deleteOnExit();
|
||||
FileOutputStream fos = new FileOutputStream(newFile);
|
||||
ByteStreams.copy(zis, new FileOutputStream(newFile));
|
||||
fos.flush();
|
||||
fos.close();
|
||||
|
||||
if (zipEntry.getName().equals("hwi")) {
|
||||
tempExec = newFile;
|
||||
if(zipEntry.getName().equals("hwi")) {
|
||||
tempExec = newFile;
|
||||
}
|
||||
}
|
||||
|
||||
zipEntry = zis.getNextEntry();
|
||||
@@ -250,10 +267,10 @@ public class Hwi {
|
||||
Path tempExecPath;
|
||||
if(platform == Platform.WINDOWS) {
|
||||
inputStream = Hwi.class.getResourceAsStream("/native/windows/x64/hwi.exe");
|
||||
tempExecPath = Files.createTempFile(TEMP_FILE_PREFIX, null);
|
||||
tempExecPath = Files.createTempFile(VERSION_PREFIX, null);
|
||||
} else {
|
||||
inputStream = Hwi.class.getResourceAsStream("/native/linux/x64/hwi");
|
||||
tempExecPath = Files.createTempFile(TEMP_FILE_PREFIX, null, PosixFilePermissions.asFileAttribute(ownerExecutableWritable));
|
||||
tempExecPath = Files.createTempFile(VERSION_PREFIX, null, PosixFilePermissions.asFileAttribute(ownerExecutableWritable));
|
||||
}
|
||||
|
||||
File tempExec = tempExecPath.toFile();
|
||||
@@ -299,6 +316,20 @@ public class Hwi {
|
||||
return directoryToBeDeleted.delete();
|
||||
}
|
||||
|
||||
public static File newDirectory(File destinationDir, ZipEntry zipEntry, Set<PosixFilePermission> setFilePermissions) throws IOException {
|
||||
String destDirPath = destinationDir.getCanonicalPath();
|
||||
|
||||
Path path = Path.of(destDirPath, zipEntry.getName());
|
||||
File destDir = Files.createDirectory(path, PosixFilePermissions.asFileAttribute(setFilePermissions)).toFile();
|
||||
|
||||
String destSubDirPath = destDir.getCanonicalPath();
|
||||
if(!destSubDirPath.startsWith(destDirPath + File.separator)) {
|
||||
throw new IOException("Entry is outside of the target dir: " + zipEntry.getName());
|
||||
}
|
||||
|
||||
return destDir;
|
||||
}
|
||||
|
||||
public static File newFile(File destinationDir, ZipEntry zipEntry, Set<PosixFilePermission> setFilePermissions) throws IOException {
|
||||
String destDirPath = destinationDir.getCanonicalPath();
|
||||
|
||||
@@ -306,7 +337,7 @@ public class Hwi {
|
||||
File destFile = Files.createFile(path, PosixFilePermissions.asFileAttribute(setFilePermissions)).toFile();
|
||||
|
||||
String destFilePath = destFile.getCanonicalPath();
|
||||
if (!destFilePath.startsWith(destDirPath + File.separator)) {
|
||||
if(!destFilePath.startsWith(destDirPath + File.separator)) {
|
||||
throw new IOException("Entry is outside of the target dir: " + zipEntry.getName());
|
||||
}
|
||||
|
||||
@@ -324,30 +355,41 @@ public class Hwi {
|
||||
|
||||
private List<String> getDeviceCommand(Device device, Command command) throws IOException {
|
||||
List<String> elements = new ArrayList<>(List.of(getHwiExecutable(command).getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType(), command.toString()));
|
||||
if(Network.get() != Network.MAINNET) {
|
||||
elements.add(elements.size() - 1, "--testnet");
|
||||
}
|
||||
addChainType(elements);
|
||||
return elements;
|
||||
}
|
||||
|
||||
private List<String> getDeviceCommand(Device device, Command command, String... commandData) throws IOException {
|
||||
List<String> elements = new ArrayList<>(List.of(getHwiExecutable(command).getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType(), command.toString()));
|
||||
if(Network.get() != Network.MAINNET) {
|
||||
elements.add(elements.size() - 1, "--testnet");
|
||||
}
|
||||
addChainType(elements);
|
||||
elements.addAll(Arrays.stream(commandData).filter(Objects::nonNull).collect(Collectors.toList()));
|
||||
return elements;
|
||||
}
|
||||
|
||||
private List<String> getDeviceCommand(Device device, String passphrase, Command command, String... commandData) throws IOException {
|
||||
List<String> elements = new ArrayList<>(List.of(getHwiExecutable(command).getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType(), "--password", passphrase, command.toString()));
|
||||
if(Network.get() != Network.MAINNET) {
|
||||
elements.add(elements.size() - 1, "--testnet");
|
||||
}
|
||||
addChainType(elements);
|
||||
elements.addAll(Arrays.stream(commandData).filter(Objects::nonNull).collect(Collectors.toList()));
|
||||
return elements;
|
||||
}
|
||||
|
||||
private void addChainType(List<String> elements) {
|
||||
if(Network.get() != Network.MAINNET) {
|
||||
elements.add(elements.size() - 1, "--chain");
|
||||
elements.add(elements.size() - 1, getChainName(Network.get()));
|
||||
}
|
||||
}
|
||||
|
||||
private String getChainName(Network network) {
|
||||
if(network == Network.MAINNET) {
|
||||
return "main";
|
||||
} else if(network == Network.TESTNET) {
|
||||
return "test";
|
||||
}
|
||||
|
||||
return network.toString();
|
||||
}
|
||||
|
||||
public static class EnumerateService extends Service<List<Device>> {
|
||||
private final String passphrase;
|
||||
|
||||
@@ -430,13 +472,13 @@ public class Hwi {
|
||||
private final Device device;
|
||||
private final String passphrase;
|
||||
private final ScriptType scriptType;
|
||||
private final String derivationPath;
|
||||
private final OutputDescriptor outputDescriptor;
|
||||
|
||||
public DisplayAddressService(Device device, String passphrase, ScriptType scriptType, String derivationPath) {
|
||||
public DisplayAddressService(Device device, String passphrase, ScriptType scriptType, OutputDescriptor outputDescriptor) {
|
||||
this.device = device;
|
||||
this.passphrase = passphrase;
|
||||
this.scriptType = scriptType;
|
||||
this.derivationPath = derivationPath;
|
||||
this.outputDescriptor = outputDescriptor;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -444,7 +486,7 @@ public class Hwi {
|
||||
return new Task<>() {
|
||||
protected String call() throws DisplayAddressException {
|
||||
Hwi hwi = new Hwi();
|
||||
return hwi.displayAddress(device, passphrase, scriptType, derivationPath);
|
||||
return hwi.displayAddress(device, passphrase, scriptType, outputDescriptor);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
|
||||
public class IOUtils {
|
||||
@@ -13,7 +14,7 @@ public class IOUtils {
|
||||
}
|
||||
|
||||
if(file.exists()) {
|
||||
try(BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(file)))) {
|
||||
try(BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8))) {
|
||||
String line = br.readLine();
|
||||
if(line.startsWith("01000000") || line.startsWith("cHNid")) {
|
||||
return FileType.TEXT;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -12,12 +12,13 @@ import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class SpecterDIY implements KeystoreFileImport {
|
||||
@Override
|
||||
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
|
||||
try {
|
||||
String text = CharStreams.toString(new InputStreamReader(inputStream));
|
||||
String text = CharStreams.toString(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
|
||||
String outputDesc = "sh(" + text + ")";
|
||||
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(outputDesc);
|
||||
Wallet wallet = outputDescriptor.toWallet();
|
||||
|
||||
@@ -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();
|
||||
@@ -51,7 +52,7 @@ public class SpecterDesktop implements WalletImport, WalletExport {
|
||||
public Wallet importWallet(InputStream inputStream, String password) throws ImportException {
|
||||
try {
|
||||
Gson gson = new Gson();
|
||||
SpecterWallet specterWallet = gson.fromJson(new InputStreamReader(inputStream), SpecterWallet.class);
|
||||
SpecterWallet specterWallet = gson.fromJson(new InputStreamReader(inputStream, StandardCharsets.UTF_8), SpecterWallet.class);
|
||||
|
||||
if(specterWallet.descriptor != null) {
|
||||
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(specterWallet.descriptor);
|
||||
@@ -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";
|
||||
|
||||
@@ -10,13 +10,14 @@ import com.sparrowwallet.drongo.crypto.*;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.MnemonicException;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletNode;
|
||||
import com.sparrowwallet.sparrow.MainApp;
|
||||
import javafx.concurrent.Service;
|
||||
import javafx.concurrent.Task;
|
||||
import org.controlsfx.tools.Platform;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.*;
|
||||
import java.lang.reflect.Type;
|
||||
@@ -26,14 +27,18 @@ import java.security.SecureRandom;
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.zip.*;
|
||||
|
||||
import static com.sparrowwallet.drongo.crypto.Argon2KeyDeriver.SPRW1_PARAMETERS;
|
||||
|
||||
public class Storage {
|
||||
private static final Logger log = LoggerFactory.getLogger(Storage.class);
|
||||
public static final ECKey NO_PASSWORD_KEY = ECKey.fromPublicOnly(ECKey.fromPrivate(Utils.hexToBytes("885e5a09708a167ea356a252387aa7c4893d138d632e296df8fbf5c12798bd28")));
|
||||
|
||||
private static final DateFormat BACKUP_DATE_FORMAT = new SimpleDateFormat("yyyyMMddHHmmss");
|
||||
private static final Pattern DATE_PATTERN = Pattern.compile(".+-([0-9]{14}?).*");
|
||||
|
||||
public static final String SPARROW_DIR = ".sparrow";
|
||||
public static final String WINDOWS_SPARROW_DIR = "Sparrow";
|
||||
@@ -41,6 +46,7 @@ public class Storage {
|
||||
public static final String WALLETS_BACKUP_DIR = "backup";
|
||||
public static final String HEADER_MAGIC_1 = "SPRW1";
|
||||
private static final int BINARY_HEADER_LENGTH = 28;
|
||||
public static final String TEMP_BACKUP_EXTENSION = "tmp";
|
||||
|
||||
private File walletFile;
|
||||
private final Gson gson;
|
||||
@@ -81,17 +87,49 @@ public class Storage {
|
||||
return gsonBuilder.setPrettyPrinting().disableHtmlEscaping().create();
|
||||
}
|
||||
|
||||
public Wallet loadWallet() throws IOException {
|
||||
Reader reader = new FileReader(walletFile);
|
||||
public WalletBackupAndKey loadWallet() throws IOException {
|
||||
Wallet wallet = loadWallet(walletFile);
|
||||
|
||||
Wallet backupWallet = null;
|
||||
File[] backups = getBackups("json." + TEMP_BACKUP_EXTENSION);
|
||||
if(backups.length > 0) {
|
||||
try {
|
||||
backupWallet = loadWallet(backups[0]);
|
||||
} catch(Exception e) {
|
||||
log.error("Error loading backup wallet " + TEMP_BACKUP_EXTENSION, e);
|
||||
}
|
||||
}
|
||||
|
||||
encryptionPubKey = NO_PASSWORD_KEY;
|
||||
return new WalletBackupAndKey(wallet, backupWallet, null);
|
||||
}
|
||||
|
||||
public Wallet loadWallet(File jsonFile) throws IOException {
|
||||
Reader reader = new FileReader(jsonFile);
|
||||
Wallet wallet = gson.fromJson(reader, Wallet.class);
|
||||
reader.close();
|
||||
|
||||
encryptionPubKey = NO_PASSWORD_KEY;
|
||||
return wallet;
|
||||
}
|
||||
|
||||
public WalletAndKey loadWallet(CharSequence password) throws IOException, StorageException {
|
||||
InputStream fileStream = new FileInputStream(walletFile);
|
||||
public WalletBackupAndKey loadWallet(CharSequence password) throws IOException, StorageException {
|
||||
WalletAndKey walletAndKey = loadWallet(walletFile, password);
|
||||
|
||||
WalletAndKey backupAndKey = new WalletAndKey(null, null);
|
||||
File[] backups = getBackups(TEMP_BACKUP_EXTENSION, "json." + TEMP_BACKUP_EXTENSION);
|
||||
if(backups.length > 0) {
|
||||
try {
|
||||
backupAndKey = loadWallet(backups[0], password);
|
||||
} catch(Exception e) {
|
||||
log.error("Error loading backup wallet " + TEMP_BACKUP_EXTENSION, e);
|
||||
}
|
||||
}
|
||||
|
||||
return new WalletBackupAndKey(walletAndKey.wallet, backupAndKey.wallet, walletAndKey.key);
|
||||
}
|
||||
|
||||
public WalletAndKey loadWallet(File encryptedFile, CharSequence password) throws IOException, StorageException {
|
||||
InputStream fileStream = new FileInputStream(encryptedFile);
|
||||
ECKey encryptionKey = getEncryptionKey(password, fileStream);
|
||||
|
||||
InputStream inputStream = new InflaterInputStream(new ECIESInputStream(fileStream, encryptionKey, getEncryptionMagic()));
|
||||
@@ -168,6 +206,18 @@ public class Storage {
|
||||
}
|
||||
|
||||
public void backupWallet() throws IOException {
|
||||
backupWallet(null);
|
||||
}
|
||||
|
||||
public void backupTempWallet() {
|
||||
try {
|
||||
backupWallet(TEMP_BACKUP_EXTENSION);
|
||||
} catch(IOException e) {
|
||||
log.error("Error creating ." + TEMP_BACKUP_EXTENSION + " backup wallet", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void backupWallet(String extension) throws IOException {
|
||||
File backupDir = getWalletsBackupDir();
|
||||
|
||||
Date backupDate = new Date();
|
||||
@@ -180,10 +230,52 @@ public class Storage {
|
||||
backupName += dateSuffix;
|
||||
}
|
||||
|
||||
if(extension != null) {
|
||||
backupName += "." + extension;
|
||||
}
|
||||
|
||||
File backupFile = new File(backupDir, backupName);
|
||||
Files.copy(walletFile, backupFile);
|
||||
}
|
||||
|
||||
public void deleteBackups() {
|
||||
deleteBackups(null);
|
||||
}
|
||||
|
||||
public void deleteBackups(String extension) {
|
||||
File[] backups = getBackups(extension);
|
||||
for(File backup : backups) {
|
||||
backup.delete();
|
||||
}
|
||||
}
|
||||
|
||||
private File[] getBackups(String extension) {
|
||||
return getBackups(extension, null);
|
||||
}
|
||||
|
||||
private File[] getBackups(String extension, String notExtension) {
|
||||
File backupDir = getWalletsBackupDir();
|
||||
File[] backups = backupDir.listFiles((dir, name) -> {
|
||||
return name.startsWith(Files.getNameWithoutExtension(walletFile.getName()) + "-") &&
|
||||
getBackupDate(name) != null &&
|
||||
(extension == null || name.endsWith("." + extension)) &&
|
||||
(notExtension == null || !name.endsWith("." + notExtension));
|
||||
});
|
||||
|
||||
Arrays.sort(backups, Comparator.comparing(o -> getBackupDate(((File)o).getName())).reversed());
|
||||
|
||||
return backups;
|
||||
}
|
||||
|
||||
private String getBackupDate(String backupFileName) {
|
||||
Matcher matcher = DATE_PATTERN.matcher(backupFileName);
|
||||
if(matcher.matches()) {
|
||||
return matcher.group(1);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public ECKey getEncryptionPubKey() {
|
||||
return encryptionPubKey;
|
||||
}
|
||||
@@ -238,6 +330,8 @@ public class Storage {
|
||||
}
|
||||
|
||||
keyDeriver = new Argon2KeyDeriver(salt);
|
||||
} else if(inputStream != null) {
|
||||
inputStream.skip(BINARY_HEADER_LENGTH);
|
||||
}
|
||||
|
||||
return keyDeriver;
|
||||
@@ -465,7 +559,16 @@ public class Storage {
|
||||
}
|
||||
}
|
||||
|
||||
public static class LoadWalletService extends Service<WalletAndKey> {
|
||||
public static class WalletBackupAndKey extends WalletAndKey {
|
||||
public final Wallet backupWallet;
|
||||
|
||||
public WalletBackupAndKey(Wallet wallet, Wallet backupWallet, Key key) {
|
||||
super(wallet, key);
|
||||
this.backupWallet = backupWallet;
|
||||
}
|
||||
}
|
||||
|
||||
public static class LoadWalletService extends Service<WalletBackupAndKey> {
|
||||
private final Storage storage;
|
||||
private final SecureString password;
|
||||
|
||||
@@ -475,12 +578,12 @@ public class Storage {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Task<WalletAndKey> createTask() {
|
||||
protected Task<WalletBackupAndKey> createTask() {
|
||||
return new Task<>() {
|
||||
protected WalletAndKey call() throws IOException, StorageException, MnemonicException {
|
||||
WalletAndKey walletAndKey = storage.loadWallet(password);
|
||||
protected WalletBackupAndKey call() throws IOException, StorageException {
|
||||
WalletBackupAndKey walletBackupAndKey = storage.loadWallet(password);
|
||||
password.clear();
|
||||
return walletAndKey;
|
||||
return walletBackupAndKey;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -74,7 +74,7 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
|
||||
public Map<String, ScriptHashTx[]> getScriptHashHistory(Transport transport, Wallet wallet, Map<String, String> pathScriptHashes, boolean failOnError) {
|
||||
JsonRpcClient client = new JsonRpcClient(transport);
|
||||
BatchRequestBuilder<String, ScriptHashTx[]> batchRequest = client.createBatchRequest().keysType(String.class).returnType(ScriptHashTx[].class);
|
||||
EventManager.get().post(new WalletHistoryStatusEvent(wallet, false, "Loading transactions"));
|
||||
EventManager.get().post(new WalletHistoryStatusEvent(wallet, true, "Loading transactions"));
|
||||
|
||||
for(String path : pathScriptHashes.keySet()) {
|
||||
batchRequest.add(path, "blockchain.scripthash.get_history", pathScriptHashes.get(path));
|
||||
@@ -130,7 +130,7 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
|
||||
public Map<String, String> subscribeScriptHashes(Transport transport, Wallet wallet, Map<String, String> pathScriptHashes) {
|
||||
JsonRpcClient client = new JsonRpcClient(transport);
|
||||
BatchRequestBuilder<String, String> batchRequest = client.createBatchRequest().keysType(String.class).returnType(String.class);
|
||||
EventManager.get().post(new WalletHistoryStatusEvent(wallet, false, "Finding transactions"));
|
||||
EventManager.get().post(new WalletHistoryStatusEvent(wallet, true, "Finding transactions"));
|
||||
|
||||
for(String path : pathScriptHashes.keySet()) {
|
||||
batchRequest.add(path, "blockchain.scripthash.subscribe", pathScriptHashes.get(path));
|
||||
@@ -151,7 +151,7 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
|
||||
public Map<Integer, String> getBlockHeaders(Transport transport, Wallet wallet, Set<Integer> blockHeights) {
|
||||
JsonRpcClient client = new JsonRpcClient(transport);
|
||||
BatchRequestBuilder<Integer, String> batchRequest = client.createBatchRequest().keysType(Integer.class).returnType(String.class);
|
||||
EventManager.get().post(new WalletHistoryStatusEvent(wallet, false, "Retrieving blocks"));
|
||||
EventManager.get().post(new WalletHistoryStatusEvent(wallet, true, "Retrieving blocks"));
|
||||
|
||||
for(Integer height : blockHeights) {
|
||||
batchRequest.add(height, "blockchain.block.header", height);
|
||||
@@ -171,7 +171,7 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
|
||||
public Map<String, String> getTransactions(Transport transport, Wallet wallet, Set<String> txids) {
|
||||
JsonRpcClient client = new JsonRpcClient(transport);
|
||||
BatchRequestBuilder<String, String> batchRequest = client.createBatchRequest().keysType(String.class).returnType(String.class);
|
||||
EventManager.get().post(new WalletHistoryStatusEvent(wallet, false, "Retrieving transactions"));
|
||||
EventManager.get().post(new WalletHistoryStatusEvent(wallet, true, "Retrieving transactions"));
|
||||
|
||||
for(String txid : txids) {
|
||||
batchRequest.add(txid, "blockchain.transaction.get", txid);
|
||||
@@ -204,7 +204,8 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
|
||||
}
|
||||
|
||||
try {
|
||||
return new RetryLogic<Map<String, VerboseTransaction>>(MAX_RETRIES, RETRY_DELAY, IllegalStateException.class).getResult(batchRequest::execute);
|
||||
//The server may return an error if the transaction has not yet been broadcasted - this is a valid state so only try once
|
||||
return new RetryLogic<Map<String, VerboseTransaction>>(1, RETRY_DELAY, IllegalStateException.class).getResult(batchRequest::execute);
|
||||
} catch(JsonRpcBatchException e) {
|
||||
log.warn("Some errors retrieving transactions: " + e.getErrors());
|
||||
return (Map<String, VerboseTransaction>)e.getSuccesses();
|
||||
|
||||
@@ -0,0 +1,322 @@
|
||||
package com.sparrowwallet.sparrow.net;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.drongo.OutputDescriptor;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHash;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.*;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import dev.bwt.libbwt.daemon.CallbackNotifier;
|
||||
import dev.bwt.libbwt.daemon.NativeBwtDaemon;
|
||||
import javafx.application.Platform;
|
||||
import javafx.concurrent.Service;
|
||||
import javafx.concurrent.Task;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Duration;
|
||||
import java.util.*;
|
||||
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;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void start(CallbackNotifier callback) {
|
||||
start(Collections.emptyList(), null, null, null, callback);
|
||||
}
|
||||
|
||||
private void start(Collection<Wallet> wallets, CallbackNotifier callback) {
|
||||
List<Wallet> validWallets = wallets.stream().filter(Wallet::isValid).collect(Collectors.toList());
|
||||
|
||||
List<String> outputDescriptors = new ArrayList<>();
|
||||
for(Wallet wallet : validWallets) {
|
||||
OutputDescriptor receiveOutputDescriptor = OutputDescriptor.getOutputDescriptor(wallet, KeyPurpose.RECEIVE);
|
||||
outputDescriptors.add(receiveOutputDescriptor.toString(false, false));
|
||||
OutputDescriptor changeOutputDescriptor = OutputDescriptor.getOutputDescriptor(wallet, KeyPurpose.CHANGE);
|
||||
outputDescriptors.add(changeOutputDescriptor.toString(false, false));
|
||||
}
|
||||
|
||||
int rescanSince = validWallets.stream().filter(wallet -> wallet.getBirthDate() != null).mapToInt(wallet -> (int)(wallet.getBirthDate().getTime() / 1000)).min().orElse(-1);
|
||||
int gapLimit = validWallets.stream().filter(wallet -> wallet.getGapLimit() > 0).mapToInt(Wallet::getGapLimit).max().orElse(Wallet.DEFAULT_LOOKAHEAD);
|
||||
|
||||
boolean forceRescan = false;
|
||||
for(Wallet wallet : validWallets) {
|
||||
Date txBirthDate = wallet.getTransactions().values().stream().map(BlockTransactionHash::getDate).filter(Objects::nonNull).min(Date::compareTo).orElse(null);
|
||||
if((wallet.getBirthDate() != null && txBirthDate != null && wallet.getBirthDate().before(txBirthDate)) || (txBirthDate == null && wallet.getStoredBlockHeight() != null && wallet.getStoredBlockHeight() == 0)) {
|
||||
forceRescan = true;
|
||||
}
|
||||
}
|
||||
|
||||
start(outputDescriptors, rescanSince, forceRescan, gapLimit, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the bwt daemon with the provided wallets
|
||||
* Blocks until the daemon is shut down.
|
||||
*
|
||||
* @param outputDescriptors descriptors of keys to add to Bitcoin Core
|
||||
* @param rescanSince seconds since epoch to start scanning keys
|
||||
* @param gapLimit desired gap limit beyond last used address
|
||||
* @param callback object receiving notifications
|
||||
*/
|
||||
private void start(List<String> outputDescriptors, Integer rescanSince, Boolean forceRescan, Integer gapLimit, CallbackNotifier callback) {
|
||||
BwtConfig bwtConfig = new BwtConfig();
|
||||
bwtConfig.network = Network.get() == Network.MAINNET ? "bitcoin" : Network.get().getName();
|
||||
|
||||
if(!outputDescriptors.isEmpty()) {
|
||||
bwtConfig.descriptors = outputDescriptors;
|
||||
bwtConfig.rescanSince = (rescanSince == null || rescanSince < 0 ? "now" : rescanSince);
|
||||
bwtConfig.forceRescan = forceRescan;
|
||||
bwtConfig.gapLimit = gapLimit;
|
||||
} else {
|
||||
bwtConfig.requireAddresses = false;
|
||||
}
|
||||
|
||||
bwtConfig.verbose = log.isDebugEnabled() ? 2 : 0;
|
||||
if(!log.isDebugEnabled()) {
|
||||
bwtConfig.setupLogger = false;
|
||||
}
|
||||
|
||||
bwtConfig.electrumAddr = "127.0.0.1:0";
|
||||
bwtConfig.electrumSkipMerkle = true;
|
||||
|
||||
Config config = Config.get();
|
||||
bwtConfig.bitcoindUrl = config.getCoreServer();
|
||||
if(config.getCoreAuthType() == CoreAuthType.COOKIE && config.getCoreDataDir() != null) {
|
||||
bwtConfig.bitcoindDir = config.getCoreDataDir().getAbsolutePath() + "/";
|
||||
} else {
|
||||
bwtConfig.bitcoindAuth = config.getCoreAuth();
|
||||
}
|
||||
if(config.getCoreMultiWallet() != Boolean.FALSE) {
|
||||
bwtConfig.bitcoindWallet = config.getCoreWallet();
|
||||
}
|
||||
bwtConfig.createWalletIfMissing = true;
|
||||
|
||||
Gson gson = new Gson();
|
||||
String jsonConfig = gson.toJson(bwtConfig);
|
||||
log.debug("Configuring bwt: " + jsonConfig);
|
||||
|
||||
NativeBwtDaemon.start(jsonConfig, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shut down the BWT daemon
|
||||
*
|
||||
*/
|
||||
private void shutdown() {
|
||||
if(shutdownPtr == null) {
|
||||
terminating = true;
|
||||
return;
|
||||
}
|
||||
|
||||
NativeBwtDaemon.shutdown(shutdownPtr);
|
||||
this.terminating = false;
|
||||
this.ready = false;
|
||||
this.shutdownPtr = null;
|
||||
}
|
||||
|
||||
public boolean isRunning() {
|
||||
return shutdownPtr != null;
|
||||
}
|
||||
|
||||
public boolean isReady() {
|
||||
return ready;
|
||||
}
|
||||
|
||||
public boolean isTerminating() {
|
||||
return terminating;
|
||||
}
|
||||
|
||||
public ConnectionService getConnectionService(Collection<Wallet> wallets) {
|
||||
return wallets != null ? new ConnectionService(wallets) : new ConnectionService();
|
||||
}
|
||||
|
||||
public DisconnectionService getDisconnectionService() {
|
||||
return new DisconnectionService();
|
||||
}
|
||||
|
||||
private static class BwtConfig {
|
||||
@SerializedName("network")
|
||||
public String network;
|
||||
|
||||
@SerializedName("bitcoind_url")
|
||||
public String bitcoindUrl;
|
||||
|
||||
@SerializedName("bitcoind_auth")
|
||||
public String bitcoindAuth;
|
||||
|
||||
@SerializedName("bitcoind_dir")
|
||||
public String bitcoindDir;
|
||||
|
||||
@SerializedName("bitcoind_cookie")
|
||||
public String bitcoindCookie;
|
||||
|
||||
@SerializedName("bitcoind_wallet")
|
||||
public String bitcoindWallet;
|
||||
|
||||
@SerializedName("create_wallet_if_missing")
|
||||
public Boolean createWalletIfMissing;
|
||||
|
||||
@SerializedName("descriptors")
|
||||
public List<String> descriptors;
|
||||
|
||||
@SerializedName("xpubs")
|
||||
public String xpubs;
|
||||
|
||||
@SerializedName("rescan_since")
|
||||
public Object rescanSince;
|
||||
|
||||
@SerializedName("force_rescan")
|
||||
public Boolean forceRescan;
|
||||
|
||||
@SerializedName("gap_limit")
|
||||
public Integer gapLimit;
|
||||
|
||||
@SerializedName("initial_import_size")
|
||||
public Integer initialImportSize;
|
||||
|
||||
@SerializedName("verbose")
|
||||
public Integer verbose;
|
||||
|
||||
@SerializedName("electrum_addr")
|
||||
public String electrumAddr;
|
||||
|
||||
@SerializedName("electrum_skip_merkle")
|
||||
public Boolean electrumSkipMerkle;
|
||||
|
||||
@SerializedName("require_addresses")
|
||||
public Boolean requireAddresses;
|
||||
|
||||
@SerializedName("setup_logger")
|
||||
public Boolean setupLogger;
|
||||
|
||||
@SerializedName("http_addr")
|
||||
public String httpAddr;
|
||||
}
|
||||
|
||||
public final class ConnectionService extends Service<Void> {
|
||||
private final Collection<Wallet> wallets;
|
||||
|
||||
public ConnectionService() {
|
||||
this.wallets = null;
|
||||
}
|
||||
|
||||
public ConnectionService(Collection<Wallet> wallets) {
|
||||
this.wallets = wallets;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Task<Void> createTask() {
|
||||
return new Task<>() {
|
||||
protected Void call() {
|
||||
CallbackNotifier notifier = new CallbackNotifier() {
|
||||
@Override
|
||||
public void onBooting(long shutdownPtr) {
|
||||
log.debug("Booting bwt");
|
||||
|
||||
Bwt.this.shutdownPtr = shutdownPtr;
|
||||
if(terminating) {
|
||||
Bwt.this.shutdown();
|
||||
terminating = false;
|
||||
} else {
|
||||
Platform.runLater(() -> EventManager.get().post(new BwtBootStatusEvent("Connecting to Bitcoin Core node at " + Config.get().getCoreServer() + "...")));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSyncProgress(float progress, int tip) {
|
||||
int percent = (int) (progress * 100.0);
|
||||
Date tipDate = new Date((long)tip * 1000);
|
||||
log.debug("Syncing " + percent + "%");
|
||||
if(!terminating) {
|
||||
Platform.runLater(() -> EventManager.get().post(new BwtSyncStatusEvent("Syncing" + (percent < 100 ? " (" + percent + "%)" : ""), percent, tipDate)));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScanProgress(float progress, int remaining) {
|
||||
int percent = (int) (progress * 100.0);
|
||||
Duration remainingDuration = Duration.ofSeconds(remaining);
|
||||
log.debug("Scanning " + percent + "%");
|
||||
if(!terminating) {
|
||||
Platform.runLater(() -> EventManager.get().post(new BwtScanStatusEvent("Scanning" + (percent < 100 ? " (" + percent + "%)" : ""), percent, remainingDuration)));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onElectrumReady(String addr) {
|
||||
log.debug("Electrum ready");
|
||||
if(!terminating) {
|
||||
Platform.runLater(() -> EventManager.get().post(new BwtElectrumReadyStatusEvent("Electrum server ready", addr)));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHttpReady(String addr) {
|
||||
log.info("http ready at " + addr);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReady() {
|
||||
log.debug("Bwt ready");
|
||||
ready = true;
|
||||
if(!terminating) {
|
||||
Platform.runLater(() -> EventManager.get().post(new BwtReadyStatusEvent("Server ready")));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if(wallets == null) {
|
||||
Bwt.this.start(notifier);
|
||||
} else {
|
||||
Bwt.this.start(wallets, notifier);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public final class DisconnectionService extends Service<Void> {
|
||||
@Override
|
||||
protected Task<Void> createTask() {
|
||||
return new Task<>() {
|
||||
protected Void call() {
|
||||
Bwt.this.shutdown();
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.sparrowwallet.sparrow.net;
|
||||
|
||||
public enum CoreAuthType {
|
||||
COOKIE, USERPASS;
|
||||
}
|
||||
@@ -9,11 +9,14 @@ import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.protocol.*;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.sparrow.event.ConnectionEvent;
|
||||
import com.sparrowwallet.sparrow.event.FeeRatesUpdatedEvent;
|
||||
import com.sparrowwallet.sparrow.event.TorStatusEvent;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.*;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.wallet.SendController;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.concurrent.ScheduledService;
|
||||
@@ -24,6 +27,8 @@ import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.locks.Condition;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class ElectrumServer {
|
||||
@@ -39,24 +44,40 @@ public class ElectrumServer {
|
||||
|
||||
private static ElectrumServerRpc electrumServerRpc = new SimpleElectrumServerRpc();
|
||||
|
||||
private static String bwtElectrumServer;
|
||||
|
||||
private static synchronized Transport getTransport() throws ServerException {
|
||||
if(transport == null) {
|
||||
try {
|
||||
String electrumServer = Config.get().getElectrumServer();
|
||||
File electrumServerCert = Config.get().getElectrumServerCert();
|
||||
String proxyServer = Config.get().getProxyServer();
|
||||
String electrumServer = null;
|
||||
File electrumServerCert = null;
|
||||
String proxyServer = null;
|
||||
|
||||
if(Config.get().getServerType() == ServerType.PUBLIC_ELECTRUM_SERVER) {
|
||||
electrumServer = Config.get().getPublicElectrumServer();
|
||||
proxyServer = Config.get().getProxyServer();
|
||||
} else if(Config.get().getServerType() == ServerType.BITCOIN_CORE) {
|
||||
if(bwtElectrumServer == null) {
|
||||
throw new ServerConfigException("Could not connect to Bitcoin Core RPC");
|
||||
}
|
||||
electrumServer = bwtElectrumServer;
|
||||
} else if(Config.get().getServerType() == ServerType.ELECTRUM_SERVER) {
|
||||
electrumServer = Config.get().getElectrumServer();
|
||||
electrumServerCert = Config.get().getElectrumServerCert();
|
||||
proxyServer = Config.get().getProxyServer();
|
||||
}
|
||||
|
||||
if(electrumServer == null) {
|
||||
throw new ServerException("Electrum server URL not specified");
|
||||
throw new ServerConfigException("Electrum server URL not specified");
|
||||
}
|
||||
|
||||
if(electrumServerCert != null && !electrumServerCert.exists()) {
|
||||
throw new ServerException("Electrum server certificate file not found");
|
||||
throw new ServerConfigException("Electrum server certificate file not found");
|
||||
}
|
||||
|
||||
Protocol protocol = Protocol.getProtocol(electrumServer);
|
||||
if(protocol == null) {
|
||||
throw new ServerException("Electrum server URL must start with " + Protocol.TCP.toUrlString() + " or " + Protocol.SSL.toUrlString());
|
||||
throw new ServerConfigException("Electrum server URL must start with " + Protocol.TCP.toUrlString() + " or " + Protocol.SSL.toUrlString());
|
||||
}
|
||||
|
||||
HostAndPort server = protocol.getServerHostAndPort(electrumServer);
|
||||
@@ -76,7 +97,7 @@ public class ElectrumServer {
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new ServerException(e);
|
||||
throw new ServerConfigException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,7 +175,7 @@ public class ElectrumServer {
|
||||
|
||||
if(!newReferences.isEmpty()) {
|
||||
//Look for additional nodes to fetch history for by considering the inputs and outputs of new transactions found
|
||||
log.debug("Found new transactions: " + newReferences);
|
||||
log.debug(wallet.getName() + " found new transactions: " + newReferences);
|
||||
Set<WalletNode> additionalNodes = new HashSet<>();
|
||||
Map<String, WalletNode> walletScriptHashes = getAllScriptHashes(wallet);
|
||||
for(BlockTransactionHash reference : newReferences) {
|
||||
@@ -611,6 +632,9 @@ public class ElectrumServer {
|
||||
Map<Integer, Double> targetBlocksFeeRatesSats = new TreeMap<>();
|
||||
for(Integer target : targetBlocksFeeRatesBtcKb.keySet()) {
|
||||
long minFeeRateSatsKb = (long)(targetBlocksFeeRatesBtcKb.get(target) * Transaction.SATOSHIS_PER_BITCOIN);
|
||||
if(minFeeRateSatsKb < 0) {
|
||||
minFeeRateSatsKb = 1000;
|
||||
}
|
||||
targetBlocksFeeRatesSats.put(target, minFeeRateSatsKb / 1000d);
|
||||
}
|
||||
|
||||
@@ -663,6 +687,24 @@ public class ElectrumServer {
|
||||
}
|
||||
}
|
||||
|
||||
public Set<String> getMempoolScriptHashes(Wallet wallet, Sha256Hash txId, Set<WalletNode> transactionNodes) throws ServerException {
|
||||
Map<String, String> pathScriptHashes = new LinkedHashMap<>(transactionNodes.size());
|
||||
for(WalletNode node : transactionNodes) {
|
||||
pathScriptHashes.put(node.getDerivationPath(), getScriptHash(wallet, node));
|
||||
}
|
||||
|
||||
Set<String> mempoolScriptHashes = new LinkedHashSet<>();
|
||||
Map<String, ScriptHashTx[]> result = electrumServerRpc.getScriptHashHistory(getTransport(), wallet, pathScriptHashes, true);
|
||||
for(String path : result.keySet()) {
|
||||
ScriptHashTx[] txes = result.get(path);
|
||||
if(Arrays.stream(txes).map(ScriptHashTx::getBlockchainTransactionHash).anyMatch(ref -> txId.equals(ref.getHash()) && ref.getHeight() <= 0)) {
|
||||
mempoolScriptHashes.add(pathScriptHashes.get(path));
|
||||
}
|
||||
}
|
||||
|
||||
return mempoolScriptHashes;
|
||||
}
|
||||
|
||||
public static Map<String, WalletNode> getAllScriptHashes(Wallet wallet) {
|
||||
Map<String, WalletNode> scriptHashes = new HashMap<>();
|
||||
List<KeyPurpose> purposes = List.of(KeyPurpose.RECEIVE, KeyPurpose.CHANGE);
|
||||
@@ -740,7 +782,10 @@ public class ElectrumServer {
|
||||
private boolean firstCall = true;
|
||||
private Thread reader;
|
||||
private long feeRatesRetrievedAt;
|
||||
private StringProperty statusProperty = new SimpleStringProperty();
|
||||
private final Bwt bwt = new Bwt();
|
||||
private final ReentrantLock bwtStartLock = new ReentrantLock();
|
||||
private final Condition bwtStartCondition = bwtStartLock.newCondition();
|
||||
private Throwable bwtStartException;
|
||||
|
||||
public ConnectionService() {
|
||||
this(true);
|
||||
@@ -755,6 +800,45 @@ public class ElectrumServer {
|
||||
return new Task<>() {
|
||||
protected FeeRatesUpdatedEvent call() throws ServerException {
|
||||
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();
|
||||
} finally {
|
||||
bwtStartLock.unlock();
|
||||
}
|
||||
});
|
||||
Platform.runLater(bwtConnectionService::start);
|
||||
|
||||
try {
|
||||
bwtStartLock.lock();
|
||||
bwtStartCondition.await();
|
||||
|
||||
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();
|
||||
return null;
|
||||
} finally {
|
||||
bwtStartLock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(firstCall) {
|
||||
electrumServer.connect();
|
||||
|
||||
@@ -819,16 +903,26 @@ public class ElectrumServer {
|
||||
public void resetConnection() {
|
||||
try {
|
||||
closeActiveConnection();
|
||||
shutdown();
|
||||
firstCall = true;
|
||||
} catch (ServerException e) {
|
||||
log.error("Error closing connection during connection reset", e);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isConnecting() {
|
||||
return isRunning() && Config.get().getServerType() == ServerType.BITCOIN_CORE && bwt.isRunning() && !bwt.isReady();
|
||||
}
|
||||
|
||||
public boolean isConnected() {
|
||||
return isRunning() && (Config.get().getServerType() != ServerType.BITCOIN_CORE || (bwt.isRunning() && bwt.isReady()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean cancel() {
|
||||
try {
|
||||
closeActiveConnection();
|
||||
shutdown();
|
||||
} catch (ServerException e) {
|
||||
log.error("Error closing connection", e);
|
||||
}
|
||||
@@ -836,6 +930,24 @@ public class ElectrumServer {
|
||||
return super.cancel();
|
||||
}
|
||||
|
||||
private void shutdown() {
|
||||
if(Config.get().getServerType() == ServerType.BITCOIN_CORE && bwt.isRunning()) {
|
||||
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());
|
||||
});
|
||||
disconnectionService.start();
|
||||
} else if(subscribe) {
|
||||
EventManager.get().post(new DisconnectionEvent());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
super.reset();
|
||||
@@ -848,12 +960,32 @@ public class ElectrumServer {
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void torStatus(TorStatusEvent event) {
|
||||
statusProperty.set(event.getStatus());
|
||||
public void bwtElectrumReadyStatus(BwtElectrumReadyStatusEvent event) {
|
||||
if(this.isRunning()) {
|
||||
ElectrumServer.bwtElectrumServer = Protocol.TCP.toUrlString(HostAndPort.fromString(event.getElectrumAddr()));
|
||||
}
|
||||
}
|
||||
|
||||
public StringProperty statusProperty() {
|
||||
return statusProperty;
|
||||
@Subscribe
|
||||
public void bwtReadyStatus(BwtReadyStatusEvent event) {
|
||||
if(this.isRunning()) {
|
||||
try {
|
||||
bwtStartLock.lock();
|
||||
bwtStartCondition.signal();
|
||||
} finally {
|
||||
bwtStartLock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void bwtShutdown(BwtShutdownEvent event) {
|
||||
try {
|
||||
bwtStartLock.lock();
|
||||
bwtStartCondition.signal();
|
||||
} finally {
|
||||
bwtStartLock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -902,6 +1034,38 @@ public class ElectrumServer {
|
||||
}
|
||||
}
|
||||
|
||||
public static class TransactionMempoolService extends ScheduledService<Set<String>> {
|
||||
private final Wallet wallet;
|
||||
private final Sha256Hash txId;
|
||||
private final Set<WalletNode> nodes;
|
||||
private final IntegerProperty iterationCount = new SimpleIntegerProperty(0);
|
||||
|
||||
public TransactionMempoolService(Wallet wallet, Sha256Hash txId, Set<WalletNode> nodes) {
|
||||
this.wallet = wallet;
|
||||
this.txId = txId;
|
||||
this.nodes = nodes;
|
||||
}
|
||||
|
||||
public int getIterationCount() {
|
||||
return iterationCount.get();
|
||||
}
|
||||
|
||||
public IntegerProperty iterationCountProperty() {
|
||||
return iterationCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Task<Set<String>> createTask() {
|
||||
return new Task<>() {
|
||||
protected Set<String> call() throws ServerException {
|
||||
iterationCount.set(iterationCount.get() + 1);
|
||||
ElectrumServer electrumServer = new ElectrumServer();
|
||||
return electrumServer.getMempoolScriptHashes(wallet, txId, nodes);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static class TransactionReferenceService extends Service<Map<Sha256Hash, BlockTransaction>> {
|
||||
private final Set<Sha256Hash> references;
|
||||
private String scriptHash;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -18,7 +18,7 @@ import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public enum FeeRatesSource {
|
||||
ELECTRUM_SERVER("Electrum Server") {
|
||||
ELECTRUM_SERVER("Server") {
|
||||
@Override
|
||||
public Map<Integer, Double> getBlockTargetFeeRates(Map<Integer, Double> defaultblockTargetFeeRates) {
|
||||
return Collections.emptyMap();
|
||||
@@ -40,6 +40,9 @@ public enum FeeRatesSource {
|
||||
};
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(FeeRatesSource.class);
|
||||
public static final int BLOCKS_IN_HALF_HOUR = 3;
|
||||
public static final int BLOCKS_IN_HOUR = 6;
|
||||
public static final int BLOCKS_IN_TWO_HOURS = 12;
|
||||
|
||||
private final String name;
|
||||
|
||||
@@ -61,16 +64,20 @@ public enum FeeRatesSource {
|
||||
Gson gson = new Gson();
|
||||
ThreeTierRates threeTierRates = gson.fromJson(reader, ThreeTierRates.class);
|
||||
for(Integer blockTarget : defaultblockTargetFeeRates.keySet()) {
|
||||
if(blockTarget < 3) {
|
||||
if(blockTarget < BLOCKS_IN_HALF_HOUR) {
|
||||
blockTargetFeeRates.put(blockTarget, threeTierRates.fastestFee);
|
||||
} else if(blockTarget < 6) {
|
||||
} else if(blockTarget < BLOCKS_IN_HOUR) {
|
||||
blockTargetFeeRates.put(blockTarget, threeTierRates.halfHourFee);
|
||||
} else if(blockTarget <= 10 || defaultblockTargetFeeRates.get(blockTarget) > threeTierRates.hourFee) {
|
||||
} else if(blockTarget < BLOCKS_IN_TWO_HOURS || defaultblockTargetFeeRates.get(blockTarget) > threeTierRates.hourFee) {
|
||||
blockTargetFeeRates.put(blockTarget, threeTierRates.hourFee);
|
||||
} else {
|
||||
blockTargetFeeRates.put(blockTarget, defaultblockTargetFeeRates.get(blockTarget));
|
||||
}
|
||||
}
|
||||
|
||||
if(threeTierRates.minimumFee != null) {
|
||||
blockTargetFeeRates.put(Integer.MAX_VALUE, threeTierRates.minimumFee);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("Error retrieving recommended fee rates from " + url, e);
|
||||
}
|
||||
@@ -98,5 +105,6 @@ public enum FeeRatesSource {
|
||||
Double fastestFee;
|
||||
Double halfHourFee;
|
||||
Double hourFee;
|
||||
Double minimumFee;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
package com.sparrowwallet.sparrow.net;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.FileSystemNotFoundException;
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.ProviderNotFoundException;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
|
||||
/**
|
||||
* A simple library class which helps with loading dynamic libraries stored in the
|
||||
* JAR archive. These libraries usually contain implementation of some methods in
|
||||
* native code (using JNI - Java Native Interface).
|
||||
*
|
||||
* @see <a href="http://adamheinrich.com/blog/2012/how-to-load-native-jni-library-from-jar">http://adamheinrich.com/blog/2012/how-to-load-native-jni-library-from-jar</a>
|
||||
* @see <a href="https://github.com/adamheinrich/native-utils">https://github.com/adamheinrich/native-utils</a>
|
||||
*
|
||||
*/
|
||||
public class NativeUtils {
|
||||
|
||||
/**
|
||||
* The minimum length a prefix for a file has to have according to {@link File#createTempFile(String, String)}}.
|
||||
*/
|
||||
private static final int MIN_PREFIX_LENGTH = 3;
|
||||
public static final String NATIVE_FOLDER_PATH_PREFIX = "nativeutils";
|
||||
|
||||
/**
|
||||
* Temporary directory which will contain the DLLs.
|
||||
*/
|
||||
private static File temporaryDir;
|
||||
|
||||
/**
|
||||
* Private constructor - this class will never be instanced
|
||||
*/
|
||||
private NativeUtils() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads library from current JAR archive
|
||||
*
|
||||
* The file from JAR is copied into system temporary directory and then loaded. The temporary file is deleted after
|
||||
* exiting.
|
||||
* Method uses String as filename because the pathname is "abstract", not system-dependent.
|
||||
*
|
||||
* @param path The path of file inside JAR as absolute path (beginning with '/'), e.g. /package/File.ext
|
||||
* @throws IOException If temporary file creation or read/write operation fails
|
||||
* @throws IllegalArgumentException If source file (param path) does not exist
|
||||
* @throws IllegalArgumentException If the path is not absolute or if the filename is shorter than three characters
|
||||
* (restriction of {@link File#createTempFile(java.lang.String, java.lang.String)}).
|
||||
* @throws FileNotFoundException If the file could not be found inside the JAR.
|
||||
*/
|
||||
public static void loadLibraryFromJar(String path) throws IOException {
|
||||
|
||||
if (null == path || !path.startsWith("/")) {
|
||||
throw new IllegalArgumentException("The path has to be absolute (start with '/').");
|
||||
}
|
||||
|
||||
// Obtain filename from path
|
||||
String[] parts = path.split("/");
|
||||
String filename = (parts.length > 1) ? parts[parts.length - 1] : null;
|
||||
|
||||
// Check if the filename is okay
|
||||
if (filename == null || filename.length() < MIN_PREFIX_LENGTH) {
|
||||
throw new IllegalArgumentException("The filename has to be at least 3 characters long.");
|
||||
}
|
||||
|
||||
// Prepare temporary file
|
||||
if (temporaryDir == null) {
|
||||
temporaryDir = createTempDirectory(NATIVE_FOLDER_PATH_PREFIX);
|
||||
temporaryDir.deleteOnExit();
|
||||
}
|
||||
|
||||
File temp = new File(temporaryDir, filename);
|
||||
|
||||
try (InputStream is = NativeUtils.class.getResourceAsStream(path)) {
|
||||
Files.copy(is, temp.toPath(), StandardCopyOption.REPLACE_EXISTING);
|
||||
} catch (IOException e) {
|
||||
temp.delete();
|
||||
throw e;
|
||||
} catch (NullPointerException e) {
|
||||
temp.delete();
|
||||
throw new FileNotFoundException("File " + path + " was not found inside JAR.");
|
||||
}
|
||||
|
||||
try {
|
||||
System.load(temp.getAbsolutePath());
|
||||
} finally {
|
||||
if (isPosixCompliant()) {
|
||||
// Assume POSIX compliant file system, can be deleted after loading
|
||||
temp.delete();
|
||||
} else {
|
||||
// Assume non-POSIX, and don't delete until last file descriptor closed
|
||||
temp.deleteOnExit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isPosixCompliant() {
|
||||
try {
|
||||
return FileSystems.getDefault()
|
||||
.supportedFileAttributeViews()
|
||||
.contains("posix");
|
||||
} catch (FileSystemNotFoundException
|
||||
| ProviderNotFoundException
|
||||
| SecurityException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static File createTempDirectory(String prefix) throws IOException {
|
||||
String tempDir = System.getProperty("java.io.tmpdir");
|
||||
File generatedDir = new File(tempDir, prefix + System.nanoTime());
|
||||
|
||||
if (!generatedDir.mkdir())
|
||||
throw new IOException("Failed to create temp directory " + generatedDir.getName());
|
||||
|
||||
return generatedDir;
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
@@ -64,6 +65,27 @@ public enum Protocol {
|
||||
public Transport getTransport(HostAndPort server, File serverCert, HostAndPort proxy) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException {
|
||||
return new ProxyTcpOverTlsTransport(server, serverCert, proxy);
|
||||
}
|
||||
},
|
||||
HTTP {
|
||||
@Override
|
||||
public Transport getTransport(HostAndPort server) throws KeyManagementException, NoSuchAlgorithmException {
|
||||
throw new UnsupportedOperationException("No transport supported for HTTP");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Transport getTransport(HostAndPort server, File serverCert) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException {
|
||||
throw new UnsupportedOperationException("No transport supported for HTTP");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Transport getTransport(HostAndPort server, HostAndPort proxy) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException {
|
||||
throw new UnsupportedOperationException("No transport supported for HTTP");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Transport getTransport(HostAndPort server, File serverCert, HostAndPort proxy) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException {
|
||||
throw new UnsupportedOperationException("No transport supported for HTTP");
|
||||
}
|
||||
};
|
||||
|
||||
public abstract Transport getTransport(HostAndPort server) throws KeyManagementException, NoSuchAlgorithmException;
|
||||
@@ -95,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) {
|
||||
@@ -105,6 +127,9 @@ public enum Protocol {
|
||||
if(url.startsWith("ssl://")) {
|
||||
return SSL;
|
||||
}
|
||||
if(url.startsWith("http://")) {
|
||||
return HTTP;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.sparrowwallet.sparrow.net;
|
||||
|
||||
public enum PublicElectrumServer {
|
||||
BLOCKSTREAM_INFO("blockstream.info", "ssl://blockstream.info:700"),
|
||||
ELECTRUM_BLOCKSTREAM_INFO("electrum.blockstream.info", "ssl://electrum.blockstream.info:50002"),
|
||||
LUKECHILDS_CO("bitcoin.lukechilds.co", "ssl://bitcoin.lukechilds.co:50002"),
|
||||
EMZY_DE("electrum.emzy.de", "ssl://electrum.emzy.de:50002"),
|
||||
BITAROO_NET("electrum.bitaroo.net", "ssl://electrum.bitaroo.net:50002");
|
||||
|
||||
PublicElectrumServer(String name, String url) {
|
||||
this.name = name;
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
private final String name;
|
||||
private final String url;
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public static PublicElectrumServer fromUrl(String url) {
|
||||
for(PublicElectrumServer server : values()) {
|
||||
if(server.url.equals(url)) {
|
||||
return server;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.sparrowwallet.sparrow.net;
|
||||
|
||||
public class ServerConfigException extends ServerException {
|
||||
public ServerConfigException() {
|
||||
}
|
||||
|
||||
public ServerConfigException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public ServerConfigException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public ServerConfigException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.sparrowwallet.sparrow.net;
|
||||
|
||||
public enum ServerType {
|
||||
BITCOIN_CORE("Bitcoin Core"), ELECTRUM_SERVER("Private Electrum"), PUBLIC_ELECTRUM_SERVER("Public Electrum");
|
||||
|
||||
private final String name;
|
||||
|
||||
ServerType(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
@@ -75,7 +75,7 @@ public class SimpleElectrumServerRpc implements ElectrumServerRpc {
|
||||
|
||||
Map<String, ScriptHashTx[]> result = new LinkedHashMap<>();
|
||||
for(String path : pathScriptHashes.keySet()) {
|
||||
EventManager.get().post(new WalletHistoryStatusEvent(wallet, false, "Loading transactions for " + path));
|
||||
EventManager.get().post(new WalletHistoryStatusEvent(wallet, true, "Loading transactions for " + path));
|
||||
try {
|
||||
ScriptHashTx[] scriptHashTxes = new RetryLogic<ScriptHashTx[]>(MAX_RETRIES, RETRY_DELAY, List.of(IllegalStateException.class, IllegalArgumentException.class)).getResult(() ->
|
||||
client.createRequest().returnAs(ScriptHashTx[].class).method("blockchain.scripthash.get_history").id(path + "-" + idCounter.incrementAndGet()).params(pathScriptHashes.get(path)).execute());
|
||||
@@ -120,7 +120,7 @@ public class SimpleElectrumServerRpc implements ElectrumServerRpc {
|
||||
|
||||
Map<String, String> result = new LinkedHashMap<>();
|
||||
for(String path : pathScriptHashes.keySet()) {
|
||||
EventManager.get().post(new WalletHistoryStatusEvent(wallet, false, "Finding transactions for " + path));
|
||||
EventManager.get().post(new WalletHistoryStatusEvent(wallet, true, "Finding transactions for " + path));
|
||||
try {
|
||||
String scriptHash = new RetryLogic<String>(MAX_RETRIES, RETRY_DELAY, List.of(IllegalStateException.class, IllegalArgumentException.class)).getResult(() ->
|
||||
client.createRequest().returnAs(String.class).method("blockchain.scripthash.subscribe").id(path + "-" + idCounter.incrementAndGet()).params(pathScriptHashes.get(path)).executeNullable());
|
||||
@@ -140,7 +140,7 @@ public class SimpleElectrumServerRpc implements ElectrumServerRpc {
|
||||
|
||||
Map<Integer, String> result = new LinkedHashMap<>();
|
||||
for(Integer blockHeight : blockHeights) {
|
||||
EventManager.get().post(new WalletHistoryStatusEvent(wallet, false, "Retrieving block at height " + blockHeight));
|
||||
EventManager.get().post(new WalletHistoryStatusEvent(wallet, true, "Retrieving block at height " + blockHeight));
|
||||
try {
|
||||
String blockHeader = new RetryLogic<String>(MAX_RETRIES, RETRY_DELAY, List.of(IllegalStateException.class, IllegalArgumentException.class)).getResult(() ->
|
||||
client.createRequest().returnAs(String.class).method("blockchain.block.header").id(idCounter.incrementAndGet()).params(blockHeight).execute());
|
||||
@@ -161,7 +161,7 @@ public class SimpleElectrumServerRpc implements ElectrumServerRpc {
|
||||
|
||||
Map<String, String> result = new LinkedHashMap<>();
|
||||
for(String txid : txids) {
|
||||
EventManager.get().post(new WalletHistoryStatusEvent(wallet, false, "Retrieving transaction [" + txid.substring(0, 6) + "]"));
|
||||
EventManager.get().post(new WalletHistoryStatusEvent(wallet, true, "Retrieving transaction [" + txid.substring(0, 6) + "]"));
|
||||
try {
|
||||
String rawTxHex = new RetryLogic<String>(MAX_RETRIES, RETRY_DELAY, List.of(IllegalStateException.class, IllegalArgumentException.class)).getResult(() ->
|
||||
client.createRequest().returnAs(String.class).method("blockchain.transaction.get").id(idCounter.incrementAndGet()).params(txid).execute());
|
||||
@@ -181,7 +181,8 @@ public class SimpleElectrumServerRpc implements ElectrumServerRpc {
|
||||
Map<String, VerboseTransaction> result = new LinkedHashMap<>();
|
||||
for(String txid : txids) {
|
||||
try {
|
||||
VerboseTransaction verboseTransaction = new RetryLogic<VerboseTransaction>(MAX_RETRIES, RETRY_DELAY, IllegalStateException.class).getResult(() ->
|
||||
//The server may return an error if the transaction has not yet been broadcasted - this is a valid state so only try once
|
||||
VerboseTransaction verboseTransaction = new RetryLogic<VerboseTransaction>(1, RETRY_DELAY, IllegalStateException.class).getResult(() ->
|
||||
client.createRequest().returnAs(VerboseTransaction.class).method("blockchain.transaction.get").id(idCounter.incrementAndGet()).params(txid, true).execute());
|
||||
result.put(txid, verboseTransaction);
|
||||
} catch(Exception e) {
|
||||
@@ -213,7 +214,7 @@ public class SimpleElectrumServerRpc implements ElectrumServerRpc {
|
||||
verboseTransaction.blockhash = Sha256Hash.ZERO_HASH.toString();
|
||||
result.put(txid, verboseTransaction);
|
||||
} catch(Exception ex) {
|
||||
throw new ElectrumServerRpcException("Error retrieving transaction: ", ex);
|
||||
//ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,10 +32,10 @@ public class SubscriptionService {
|
||||
|
||||
Set<String> existingStatuses = ElectrumServer.getSubscribedScriptHashes().get(scriptHash);
|
||||
if(existingStatuses == null) {
|
||||
log.warn("Received script hash status update for unsubscribed script hash: " + scriptHash);
|
||||
log.debug("Received script hash status update for unsubscribed script hash: " + scriptHash);
|
||||
ElectrumServer.updateSubscribedScriptHashStatus(scriptHash, status);
|
||||
} else if(existingStatuses.contains(status)) {
|
||||
log.warn("Received script hash status update, but status has not changed");
|
||||
log.debug("Received script hash status update, but status has not changed");
|
||||
return;
|
||||
} else {
|
||||
String oldStatus = Iterables.getLast(existingStatuses);
|
||||
|
||||
@@ -12,6 +12,7 @@ import org.slf4j.LoggerFactory;
|
||||
import javax.net.SocketFactory;
|
||||
import java.io.*;
|
||||
import java.net.Socket;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.Condition;
|
||||
@@ -46,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
|
||||
@@ -130,9 +135,11 @@ public class TcpTransport implements Transport, Closeable {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
|
||||
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
|
||||
|
||||
while(running) {
|
||||
try {
|
||||
String received = readInputStream();
|
||||
String received = readInputStream(in);
|
||||
if(received.contains("method") && !received.contains("error")) {
|
||||
//Handle subscription notification
|
||||
jsonRpcServer.handle(received, subscriptionService);
|
||||
@@ -147,7 +154,7 @@ public class TcpTransport implements Transport, Closeable {
|
||||
//Restore interrupt status and continue
|
||||
Thread.currentThread().interrupt();
|
||||
} catch(Exception e) {
|
||||
log.debug("Connection error while reading", e);
|
||||
log.trace("Connection error while reading", e);
|
||||
if(running) {
|
||||
lastException = e;
|
||||
reading = false;
|
||||
@@ -157,17 +164,25 @@ public class TcpTransport implements Transport, Closeable {
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch(IOException e) {
|
||||
log.error("Error opening socket inputstream", e);
|
||||
if(running) {
|
||||
lastException = e;
|
||||
reading = false;
|
||||
readingCondition.signal();
|
||||
//Allow this thread to terminate as we will need to reconnect with a new transport anyway
|
||||
running = false;
|
||||
}
|
||||
} finally {
|
||||
readLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
protected String readInputStream() throws IOException {
|
||||
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
|
||||
protected String readInputStream(BufferedReader in) throws IOException {
|
||||
String response = in.readLine();
|
||||
|
||||
if(response == null) {
|
||||
throw new IOException("Could not connect to server at " + Config.get().getElectrumServer());
|
||||
throw new IOException("Could not connect to server at " + Config.get().getServerAddress());
|
||||
}
|
||||
|
||||
return response;
|
||||
|
||||
@@ -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,7 +16,9 @@ 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;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -43,9 +46,10 @@ 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())) {
|
||||
try(InputStreamReader reader = new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8)) {
|
||||
Gson gson = new Gson();
|
||||
return gson.fromJson(reader, VersionCheck.class);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.sparrowwallet.sparrow.preferences;
|
||||
|
||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.control.UnlabeledToggleSwitch;
|
||||
import com.sparrowwallet.sparrow.event.*;
|
||||
@@ -32,6 +33,9 @@ public class GeneralPreferencesController extends PreferencesDetailController {
|
||||
@FXML
|
||||
private ComboBox<ExchangeSource> exchangeSource;
|
||||
|
||||
@FXML
|
||||
private UnlabeledToggleSwitch validateDerivationPaths;
|
||||
|
||||
@FXML
|
||||
private UnlabeledToggleSwitch groupByAddress;
|
||||
|
||||
@@ -58,6 +62,8 @@ public class GeneralPreferencesController extends PreferencesDetailController {
|
||||
public void initializeView(Config config) {
|
||||
if(config.getBitcoinUnit() != null) {
|
||||
bitcoinUnit.setValue(config.getBitcoinUnit());
|
||||
} else {
|
||||
bitcoinUnit.setValue(BitcoinUnit.AUTO);
|
||||
}
|
||||
|
||||
bitcoinUnit.valueProperty().addListener((observable, oldValue, newValue) -> {
|
||||
@@ -91,6 +97,12 @@ public class GeneralPreferencesController extends PreferencesDetailController {
|
||||
|
||||
updateCurrencies(exchangeSource.getSelectionModel().getSelectedItem());
|
||||
|
||||
validateDerivationPaths.setSelected(config.isValidateDerivationPaths());
|
||||
validateDerivationPaths.selectedProperty().addListener((observableValue, oldValue, newValue) -> {
|
||||
config.setValidateDerivationPaths(newValue);
|
||||
System.setProperty(Wallet.ALLOW_DERIVATIONS_MATCHING_OTHER_SCRIPT_TYPES_PROPERTY, Boolean.toString(!newValue));
|
||||
});
|
||||
|
||||
groupByAddress.setSelected(config.isGroupByAddress());
|
||||
includeMempoolChange.setSelected(config.isIncludeMempoolChange());
|
||||
groupByAddress.selectedProperty().addListener((observableValue, oldValue, newValue) -> {
|
||||
|
||||
@@ -3,6 +3,8 @@ package com.sparrowwallet.sparrow.preferences;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.fxml.Initializable;
|
||||
@@ -24,6 +26,10 @@ public class PreferencesController implements Initializable {
|
||||
@FXML
|
||||
private StackPane preferencesPane;
|
||||
|
||||
private final BooleanProperty closing = new SimpleBooleanProperty(false);
|
||||
|
||||
private final BooleanProperty reconnectOnClosing = new SimpleBooleanProperty(false);
|
||||
|
||||
@Override
|
||||
public void initialize(URL location, ResourceBundle resources) {
|
||||
|
||||
@@ -56,6 +62,18 @@ public class PreferencesController implements Initializable {
|
||||
}
|
||||
}
|
||||
|
||||
BooleanProperty closingProperty() {
|
||||
return closing;
|
||||
}
|
||||
|
||||
public boolean isReconnectOnClosing() {
|
||||
return reconnectOnClosing.get();
|
||||
}
|
||||
|
||||
public BooleanProperty reconnectOnClosingProperty() {
|
||||
return reconnectOnClosing;
|
||||
}
|
||||
|
||||
FXMLLoader setPreferencePane(String fxmlName) {
|
||||
preferencesPane.getChildren().removeAll(preferencesPane.getChildren());
|
||||
|
||||
|
||||
@@ -15,8 +15,6 @@ import org.controlsfx.tools.Borders;
|
||||
import java.io.IOException;
|
||||
|
||||
public class PreferencesDialog extends Dialog<Boolean> {
|
||||
private final boolean existingConnection;
|
||||
|
||||
public PreferencesDialog() {
|
||||
this(null);
|
||||
}
|
||||
@@ -48,12 +46,13 @@ public class PreferencesDialog extends Dialog<Boolean> {
|
||||
dialogPane.getButtonTypes().addAll(newWalletButtonType);
|
||||
}
|
||||
|
||||
dialogPane.setPrefWidth(650);
|
||||
dialogPane.setPrefHeight(550);
|
||||
dialogPane.setPrefWidth(750);
|
||||
dialogPane.setPrefHeight(600);
|
||||
|
||||
existingConnection = ElectrumServer.isConnected();
|
||||
preferencesController.reconnectOnClosingProperty().set(AppServices.isConnecting() || AppServices.isConnected());
|
||||
setOnCloseRequest(event -> {
|
||||
if(existingConnection && !ElectrumServer.isConnected()) {
|
||||
preferencesController.closingProperty().set(true);
|
||||
if(preferencesController.isReconnectOnClosing() && !(AppServices.isConnecting() || AppServices.isConnected())) {
|
||||
EventManager.get().post(new RequestConnectEvent());
|
||||
}
|
||||
});
|
||||
|
||||
+506
-90
@@ -1,27 +1,29 @@
|
||||
package com.sparrowwallet.sparrow.preferences;
|
||||
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.google.common.net.HostAndPort;
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.Mode;
|
||||
import com.sparrowwallet.sparrow.control.TextFieldValidator;
|
||||
import com.sparrowwallet.sparrow.control.UnlabeledToggleSwitch;
|
||||
import com.sparrowwallet.sparrow.event.ConnectionEvent;
|
||||
import com.sparrowwallet.sparrow.event.RequestDisconnectEvent;
|
||||
import com.sparrowwallet.sparrow.event.*;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.net.ElectrumServer;
|
||||
import com.sparrowwallet.sparrow.net.Protocol;
|
||||
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.Button;
|
||||
import javafx.scene.control.Control;
|
||||
import javafx.scene.control.TextArea;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.text.Font;
|
||||
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.control.SegmentedButton;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.controlsfx.validation.ValidationResult;
|
||||
import org.controlsfx.validation.ValidationSupport;
|
||||
@@ -30,30 +32,98 @@ import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import tornadofx.control.Field;
|
||||
import tornadofx.control.Form;
|
||||
|
||||
import javax.net.ssl.SSLHandshakeException;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
public class ServerPreferencesController extends PreferencesDetailController {
|
||||
private static final Logger log = LoggerFactory.getLogger(ServerPreferencesController.class);
|
||||
|
||||
@FXML
|
||||
private TextField host;
|
||||
private ToggleGroup serverTypeToggleGroup;
|
||||
|
||||
@FXML
|
||||
private TextField port;
|
||||
private SegmentedButton serverTypeSegmentedButton;
|
||||
|
||||
@FXML
|
||||
private UnlabeledToggleSwitch useSsl;
|
||||
private ToggleButton publicElectrumToggle;
|
||||
|
||||
@FXML
|
||||
private TextField certificate;
|
||||
private Form publicElectrumForm;
|
||||
|
||||
@FXML
|
||||
private Button certificateSelect;
|
||||
private ComboBox<PublicElectrumServer> publicElectrumServer;
|
||||
|
||||
@FXML
|
||||
private UnlabeledToggleSwitch publicUseProxy;
|
||||
|
||||
@FXML
|
||||
private TextField publicProxyHost;
|
||||
|
||||
@FXML
|
||||
private TextField publicProxyPort;
|
||||
|
||||
@FXML
|
||||
private Form coreForm;
|
||||
|
||||
@FXML
|
||||
private TextField coreHost;
|
||||
|
||||
@FXML
|
||||
private TextField corePort;
|
||||
|
||||
@FXML
|
||||
private ToggleGroup coreAuthToggleGroup;
|
||||
|
||||
@FXML
|
||||
private Field coreDataDirField;
|
||||
|
||||
@FXML
|
||||
private TextField coreDataDir;
|
||||
|
||||
@FXML
|
||||
private Button coreDataDirSelect;
|
||||
|
||||
@FXML
|
||||
private Field coreUserPassField;
|
||||
|
||||
@FXML
|
||||
private TextField coreUser;
|
||||
|
||||
@FXML
|
||||
private PasswordField corePass;
|
||||
|
||||
@FXML
|
||||
private UnlabeledToggleSwitch coreMultiWallet;
|
||||
|
||||
@FXML
|
||||
private TextField coreWallet;
|
||||
|
||||
@FXML
|
||||
private Form electrumForm;
|
||||
|
||||
@FXML
|
||||
private TextField electrumHost;
|
||||
|
||||
@FXML
|
||||
private TextField electrumPort;
|
||||
|
||||
@FXML
|
||||
private UnlabeledToggleSwitch electrumUseSsl;
|
||||
|
||||
@FXML
|
||||
private TextField electrumCertificate;
|
||||
|
||||
@FXML
|
||||
private Button electrumCertificateSelect;
|
||||
|
||||
@FXML
|
||||
private UnlabeledToggleSwitch useProxy;
|
||||
@@ -75,35 +145,126 @@ public class ServerPreferencesController extends PreferencesDetailController {
|
||||
|
||||
private final ValidationSupport validationSupport = new ValidationSupport();
|
||||
|
||||
private TorService torService;
|
||||
|
||||
private ElectrumServer.ConnectionService connectionService;
|
||||
|
||||
private Boolean useSslOriginal;
|
||||
|
||||
@Override
|
||||
public void initializeView(Config config) {
|
||||
EventManager.get().register(this);
|
||||
getMasterController().closingProperty().addListener((observable, oldValue, newValue) -> {
|
||||
EventManager.get().unregister(this);
|
||||
});
|
||||
|
||||
Platform.runLater(this::setupValidation);
|
||||
|
||||
port.setTextFormatter(new TextFieldValidator(TextFieldValidator.ValidationModus.MAX_INTEGERS, 5).getFormatter());
|
||||
proxyPort.setTextFormatter(new TextFieldValidator(TextFieldValidator.ValidationModus.MAX_INTEGERS, 5).getFormatter());
|
||||
publicElectrumForm.managedProperty().bind(publicElectrumForm.visibleProperty());
|
||||
coreForm.managedProperty().bind(coreForm.visibleProperty());
|
||||
electrumForm.managedProperty().bind(electrumForm.visibleProperty());
|
||||
serverTypeToggleGroup.selectedToggleProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if(serverTypeToggleGroup.getSelectedToggle() != null) {
|
||||
ServerType existingType = config.getServerType();
|
||||
ServerType serverType = (ServerType)newValue.getUserData();
|
||||
publicElectrumForm.setVisible(serverType == ServerType.PUBLIC_ELECTRUM_SERVER);
|
||||
coreForm.setVisible(serverType == ServerType.BITCOIN_CORE);
|
||||
electrumForm.setVisible(serverType == ServerType.ELECTRUM_SERVER);
|
||||
config.setServerType(serverType);
|
||||
testConnection.setGraphic(getGlyph(FontAwesome5.Glyph.QUESTION_CIRCLE, ""));
|
||||
testResults.clear();
|
||||
if(existingType != serverType) {
|
||||
EventManager.get().post(new ServerTypeChangedEvent(serverType));
|
||||
}
|
||||
} else if(oldValue != null) {
|
||||
oldValue.setSelected(true);
|
||||
}
|
||||
});
|
||||
ServerType serverType = config.getServerType() != null ?
|
||||
(config.getServerType() == ServerType.PUBLIC_ELECTRUM_SERVER && Network.get() != Network.MAINNET ? ServerType.BITCOIN_CORE : config.getServerType()) :
|
||||
(config.getCoreServer() == null && config.getElectrumServer() != null ? ServerType.ELECTRUM_SERVER :
|
||||
(config.getCoreServer() != null || Network.get() != Network.MAINNET ? ServerType.BITCOIN_CORE : ServerType.PUBLIC_ELECTRUM_SERVER));
|
||||
if(Network.get() != Network.MAINNET) {
|
||||
serverTypeSegmentedButton.getButtons().remove(publicElectrumToggle);
|
||||
serverTypeToggleGroup.getToggles().remove(publicElectrumToggle);
|
||||
}
|
||||
serverTypeToggleGroup.selectToggle(serverTypeToggleGroup.getToggles().stream().filter(toggle -> toggle.getUserData() == serverType).findFirst().orElse(null));
|
||||
|
||||
host.textProperty().addListener(getElectrumServerListener(config));
|
||||
port.textProperty().addListener(getElectrumServerListener(config));
|
||||
publicElectrumServer.getSelectionModel().selectedItemProperty().addListener(getPublicElectrumServerListener(config));
|
||||
|
||||
publicUseProxy.selectedProperty().bindBidirectional(useProxy.selectedProperty());
|
||||
publicProxyHost.textProperty().bindBidirectional(proxyHost.textProperty());
|
||||
publicProxyPort.textProperty().bindBidirectional(proxyPort.textProperty());
|
||||
|
||||
corePort.setTextFormatter(new TextFieldValidator(TextFieldValidator.ValidationModus.MAX_INTEGERS, 5).getFormatter());
|
||||
electrumPort.setTextFormatter(new TextFieldValidator(TextFieldValidator.ValidationModus.MAX_INTEGERS, 5).getFormatter());
|
||||
proxyPort.setTextFormatter(new TextFieldValidator(TextFieldValidator.ValidationModus.MAX_INTEGERS, 5).getFormatter());
|
||||
publicProxyPort.setTextFormatter(new TextFieldValidator(TextFieldValidator.ValidationModus.MAX_INTEGERS, 5).getFormatter());
|
||||
|
||||
coreHost.textProperty().addListener(getBitcoinCoreListener(config));
|
||||
corePort.textProperty().addListener(getBitcoinCoreListener(config));
|
||||
|
||||
coreUser.textProperty().addListener(getBitcoinAuthListener(config));
|
||||
corePass.textProperty().addListener(getBitcoinAuthListener(config));
|
||||
|
||||
coreWallet.textProperty().addListener(getBitcoinWalletListener(config));
|
||||
|
||||
electrumHost.textProperty().addListener(getElectrumServerListener(config));
|
||||
electrumPort.textProperty().addListener(getElectrumServerListener(config));
|
||||
|
||||
proxyHost.textProperty().addListener(getProxyListener(config));
|
||||
proxyPort.textProperty().addListener(getProxyListener(config));
|
||||
|
||||
useSsl.selectedProperty().addListener((observable, oldValue, newValue) -> {
|
||||
setElectrumServerInConfig(config);
|
||||
certificate.setDisable(!newValue);
|
||||
certificateSelect.setDisable(!newValue);
|
||||
coreDataDirField.managedProperty().bind(coreDataDirField.visibleProperty());
|
||||
coreUserPassField.managedProperty().bind(coreUserPassField.visibleProperty());
|
||||
coreUserPassField.visibleProperty().bind(coreDataDirField.visibleProperty().not());
|
||||
coreAuthToggleGroup.selectedToggleProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if(coreAuthToggleGroup.getSelectedToggle() != null) {
|
||||
CoreAuthType coreAuthType = (CoreAuthType)newValue.getUserData();
|
||||
coreDataDirField.setVisible(coreAuthType == CoreAuthType.COOKIE);
|
||||
config.setCoreAuthType(coreAuthType);
|
||||
} else if(oldValue != null) {
|
||||
oldValue.setSelected(true);
|
||||
}
|
||||
});
|
||||
CoreAuthType coreAuthType = config.getCoreAuthType() != null ? config.getCoreAuthType() : CoreAuthType.COOKIE;
|
||||
coreAuthToggleGroup.selectToggle(coreAuthToggleGroup.getToggles().stream().filter(toggle -> toggle.getUserData() == coreAuthType).findFirst().orElse(null));
|
||||
|
||||
coreDataDir.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
File dataDir = getDirectory(newValue);
|
||||
config.setCoreDataDir(dataDir);
|
||||
});
|
||||
|
||||
certificate.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
File crtFile = getCertificate(newValue);
|
||||
if(crtFile != null) {
|
||||
config.setElectrumServerCert(crtFile);
|
||||
} else {
|
||||
config.setElectrumServerCert(null);
|
||||
coreDataDirSelect.setOnAction(event -> {
|
||||
Stage window = new Stage();
|
||||
|
||||
DirectoryChooser directorChooser = new DirectoryChooser();
|
||||
directorChooser.setTitle("Select Bitcoin Core Data Directory");
|
||||
directorChooser.setInitialDirectory(config.getCoreDataDir() != null ? config.getCoreDataDir() : new File(System.getProperty("user.home")));
|
||||
|
||||
File dataDir = directorChooser.showDialog(window);
|
||||
if(dataDir != null) {
|
||||
coreDataDir.setText(dataDir.getAbsolutePath());
|
||||
}
|
||||
});
|
||||
|
||||
certificateSelect.setOnAction(event -> {
|
||||
coreMultiWallet.selectedProperty().addListener((observable, oldValue, newValue) -> {
|
||||
config.setCoreMultiWallet(newValue);
|
||||
coreWallet.setDisable(!newValue);
|
||||
});
|
||||
|
||||
electrumUseSsl.selectedProperty().addListener((observable, oldValue, newValue) -> {
|
||||
setElectrumServerInConfig(config);
|
||||
electrumCertificate.setDisable(!newValue);
|
||||
electrumCertificateSelect.setDisable(!newValue);
|
||||
});
|
||||
|
||||
electrumCertificate.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
File crtFile = getCertificate(newValue);
|
||||
config.setElectrumServerCert(crtFile);
|
||||
});
|
||||
|
||||
electrumCertificateSelect.setOnAction(event -> {
|
||||
Stage window = new Stage();
|
||||
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
@@ -115,7 +276,7 @@ public class ServerPreferencesController extends PreferencesDetailController {
|
||||
|
||||
File file = fileChooser.showOpenDialog(window);
|
||||
if(file != null) {
|
||||
certificate.setText(file.getAbsolutePath());
|
||||
electrumCertificate.setText(file.getAbsolutePath());
|
||||
}
|
||||
});
|
||||
|
||||
@@ -125,47 +286,33 @@ public class ServerPreferencesController extends PreferencesDetailController {
|
||||
proxyHost.setText(proxyHost.getText().trim());
|
||||
proxyHost.setDisable(!newValue);
|
||||
proxyPort.setDisable(!newValue);
|
||||
|
||||
if(newValue) {
|
||||
useSsl.setSelected(true);
|
||||
useSsl.setDisable(true);
|
||||
} else {
|
||||
useSsl.setDisable(false);
|
||||
}
|
||||
publicProxyHost.setDisable(!newValue);
|
||||
publicProxyPort.setDisable(!newValue);
|
||||
});
|
||||
|
||||
boolean isConnected = ElectrumServer.isConnected();
|
||||
setFieldsEditable(!isConnected);
|
||||
boolean isConnected = AppServices.isConnecting() || AppServices.isConnected();
|
||||
|
||||
if(AppServices.isConnecting()) {
|
||||
testResults.appendText("Connecting to server, please wait...");
|
||||
}
|
||||
|
||||
testConnection.managedProperty().bind(testConnection.visibleProperty());
|
||||
testConnection.setVisible(!isConnected);
|
||||
setTestResultsFont();
|
||||
testConnection.setOnAction(event -> {
|
||||
testResults.setText("Connecting to " + config.getElectrumServer() + "...");
|
||||
testConnection.setGraphic(getGlyph(FontAwesome5.Glyph.ELLIPSIS_H, null));
|
||||
testResults.setText("Connecting to " + config.getServerAddress() + "...");
|
||||
|
||||
ElectrumServer.ConnectionService connectionService = new ElectrumServer.ConnectionService(false);
|
||||
connectionService.setPeriod(Duration.ZERO);
|
||||
EventManager.get().register(connectionService);
|
||||
connectionService.statusProperty().addListener((observable, oldValue, newValue) -> {
|
||||
testResults.setText(testResults.getText() + "\n" + newValue);
|
||||
});
|
||||
|
||||
connectionService.setOnSucceeded(successEvent -> {
|
||||
EventManager.get().unregister(connectionService);
|
||||
ConnectionEvent connectionEvent = (ConnectionEvent)connectionService.getValue();
|
||||
showConnectionSuccess(connectionEvent.getServerVersion(), connectionEvent.getServerBanner());
|
||||
connectionService.cancel();
|
||||
});
|
||||
connectionService.setOnFailed(workerStateEvent -> {
|
||||
EventManager.get().unregister(connectionService);
|
||||
showConnectionFailure(workerStateEvent);
|
||||
connectionService.cancel();
|
||||
});
|
||||
connectionService.start();
|
||||
if(Config.get().requiresTor() && Tor.getDefault() == null) {
|
||||
startTor();
|
||||
} else {
|
||||
startElectrumConnection();
|
||||
}
|
||||
});
|
||||
|
||||
editConnection.managedProperty().bind(editConnection.visibleProperty());
|
||||
editConnection.setVisible(isConnected);
|
||||
editConnection.setDisable(AppServices.isConnecting());
|
||||
editConnection.setOnAction(event -> {
|
||||
EventManager.get().post(new RequestDisconnectEvent());
|
||||
setFieldsEditable(true);
|
||||
@@ -173,37 +320,78 @@ public class ServerPreferencesController extends PreferencesDetailController {
|
||||
testConnection.setVisible(true);
|
||||
});
|
||||
|
||||
PublicElectrumServer configPublicElectrumServer = PublicElectrumServer.fromUrl(config.getPublicElectrumServer());
|
||||
if(configPublicElectrumServer == null) {
|
||||
publicElectrumServer.setValue(PublicElectrumServer.values()[new Random().nextInt(PublicElectrumServer.values().length)]);
|
||||
} else {
|
||||
publicElectrumServer.setValue(configPublicElectrumServer);
|
||||
}
|
||||
|
||||
String coreServer = config.getCoreServer();
|
||||
if(coreServer != null) {
|
||||
Protocol protocol = Protocol.getProtocol(coreServer);
|
||||
|
||||
if(protocol != null) {
|
||||
HostAndPort server = protocol.getServerHostAndPort(coreServer);
|
||||
coreHost.setText(server.getHost());
|
||||
if(server.hasPort()) {
|
||||
corePort.setText(Integer.toString(server.getPort()));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
coreHost.setText("127.0.0.1");
|
||||
corePort.setText(String.valueOf(Network.get().getDefaultPort()));
|
||||
}
|
||||
|
||||
coreDataDir.setText(config.getCoreDataDir() != null ? config.getCoreDataDir().getAbsolutePath() : getDefaultCoreDataDir().getAbsolutePath());
|
||||
|
||||
if(config.getCoreAuth() != null) {
|
||||
String[] userPass = config.getCoreAuth().split(":");
|
||||
if(userPass.length > 0) {
|
||||
coreUser.setText(userPass[0]);
|
||||
}
|
||||
if(userPass.length > 1) {
|
||||
corePass.setText(userPass[1]);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
if(protocol != null) {
|
||||
boolean ssl = protocol.equals(Protocol.SSL);
|
||||
useSsl.setSelected(ssl);
|
||||
certificate.setDisable(!ssl);
|
||||
certificateSelect.setDisable(!ssl);
|
||||
electrumUseSsl.setSelected(ssl);
|
||||
electrumCertificate.setDisable(!ssl);
|
||||
electrumCertificateSelect.setDisable(!ssl);
|
||||
|
||||
HostAndPort server = protocol.getServerHostAndPort(electrumServer);
|
||||
host.setText(server.getHost());
|
||||
electrumHost.setText(server.getHost());
|
||||
if(server.hasPort()) {
|
||||
port.setText(Integer.toString(server.getPort()));
|
||||
electrumPort.setText(Integer.toString(server.getPort()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File certificateFile = config.getElectrumServerCert();
|
||||
if(certificateFile != null) {
|
||||
certificate.setText(certificateFile.getAbsolutePath());
|
||||
electrumCertificate.setText(certificateFile.getAbsolutePath());
|
||||
}
|
||||
|
||||
useProxy.setSelected(config.isUseProxy());
|
||||
proxyHost.setDisable(!config.isUseProxy());
|
||||
proxyPort.setDisable(!config.isUseProxy());
|
||||
|
||||
if(config.isUseProxy()) {
|
||||
useSsl.setSelected(true);
|
||||
useSsl.setDisable(true);
|
||||
}
|
||||
publicProxyHost.setDisable(!config.isUseProxy());
|
||||
publicProxyPort.setDisable(!config.isUseProxy());
|
||||
|
||||
String proxyServer = config.getProxyServer();
|
||||
if(proxyServer != null) {
|
||||
@@ -213,17 +401,110 @@ 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.setOnSucceeded(workerStateEvent -> {
|
||||
Tor.setDefault(torService.getValue());
|
||||
torService.cancel();
|
||||
testResults.appendText("\nTor running, connecting to " + Config.get().getServerAddress() + "...");
|
||||
startElectrumConnection();
|
||||
});
|
||||
torService.setOnFailed(workerStateEvent -> {
|
||||
torService.cancel();
|
||||
testResults.appendText("\nTor failed to start");
|
||||
showConnectionFailure(workerStateEvent.getSource().getException());
|
||||
});
|
||||
|
||||
torService.start();
|
||||
}
|
||||
|
||||
private void startElectrumConnection() {
|
||||
if(connectionService != null && connectionService.isRunning()) {
|
||||
connectionService.cancel();
|
||||
}
|
||||
|
||||
connectionService = new ElectrumServer.ConnectionService(false);
|
||||
connectionService.setPeriod(Duration.hours(1));
|
||||
connectionService.setRestartOnFailure(false);
|
||||
EventManager.get().register(connectionService);
|
||||
|
||||
useSslOriginal = null;
|
||||
|
||||
connectionService.setOnSucceeded(successEvent -> {
|
||||
EventManager.get().unregister(connectionService);
|
||||
ConnectionEvent connectionEvent = (ConnectionEvent)connectionService.getValue();
|
||||
showConnectionSuccess(connectionEvent.getServerVersion(), connectionEvent.getServerBanner());
|
||||
getMasterController().reconnectOnClosingProperty().set(true);
|
||||
Config.get().setMode(Mode.ONLINE);
|
||||
connectionService.cancel();
|
||||
});
|
||||
connectionService.setOnFailed(workerStateEvent -> {
|
||||
EventManager.get().unregister(connectionService);
|
||||
showConnectionFailure(workerStateEvent.getSource().getException());
|
||||
connectionService.cancel();
|
||||
|
||||
if(Config.get().getServerType() == ServerType.ELECTRUM_SERVER) {
|
||||
if(useSslOriginal == null) {
|
||||
Integer portAsInteger = getPort(electrumPort.getText());
|
||||
if(!electrumUseSsl.isSelected() && portAsInteger != null && portAsInteger == TcpOverTlsTransport.DEFAULT_PORT) {
|
||||
useSslOriginal = false;
|
||||
electrumUseSsl.setSelected(true);
|
||||
} else if(electrumUseSsl.isSelected() && portAsInteger != null && portAsInteger == TcpTransport.DEFAULT_PORT) {
|
||||
useSslOriginal = true;
|
||||
electrumUseSsl.setSelected(false);
|
||||
}
|
||||
|
||||
if(useSslOriginal != null) {
|
||||
EventManager.get().register(connectionService);
|
||||
connectionService.reset();
|
||||
connectionService.start();
|
||||
}
|
||||
} else {
|
||||
electrumUseSsl.setSelected(useSslOriginal);
|
||||
useSslOriginal = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
connectionService.start();
|
||||
}
|
||||
|
||||
private void setFieldsEditable(boolean editable) {
|
||||
host.setEditable(editable);
|
||||
port.setEditable(editable);
|
||||
useSsl.setDisable(!editable);
|
||||
certificate.setEditable(editable);
|
||||
certificateSelect.setDisable(!editable);
|
||||
serverTypeToggleGroup.getToggles().forEach(toggle -> ((ToggleButton)toggle).setDisable(!editable));
|
||||
|
||||
publicElectrumServer.setDisable(!editable);
|
||||
publicUseProxy.setDisable(!editable);
|
||||
publicProxyHost.setDisable(!editable);
|
||||
publicProxyPort.setDisable(!editable);
|
||||
|
||||
coreHost.setDisable(!editable);
|
||||
corePort.setDisable(!editable);
|
||||
coreAuthToggleGroup.getToggles().forEach(toggle -> ((ToggleButton)toggle).setDisable(!editable));
|
||||
coreDataDir.setDisable(!editable);
|
||||
coreDataDirSelect.setDisable(!editable);
|
||||
coreUser.setDisable(!editable);
|
||||
corePass.setDisable(!editable);
|
||||
coreMultiWallet.setDisable(!editable);
|
||||
coreWallet.setDisable(!editable);
|
||||
|
||||
electrumHost.setDisable(!editable);
|
||||
electrumPort.setDisable(!editable);
|
||||
electrumUseSsl.setDisable(!editable);
|
||||
electrumCertificate.setDisable(!editable);
|
||||
electrumCertificateSelect.setDisable(!editable);
|
||||
useProxy.setDisable(!editable);
|
||||
proxyHost.setEditable(editable);
|
||||
proxyPort.setEditable(editable);
|
||||
proxyHost.setDisable(!editable);
|
||||
proxyPort.setDisable(!editable);
|
||||
}
|
||||
|
||||
private void showConnectionSuccess(List<String> serverVersion, String serverBanner) {
|
||||
@@ -239,25 +520,56 @@ 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"));
|
||||
}
|
||||
|
||||
private void setupValidation() {
|
||||
validationSupport.registerValidator(host, Validator.combine(
|
||||
validationSupport.registerValidator(publicProxyHost, Validator.combine(
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Proxy host required", publicUseProxy.isSelected() && newValue.isEmpty()),
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid host name", getHost(newValue) == null)
|
||||
));
|
||||
|
||||
validationSupport.registerValidator(port, Validator.combine(
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid port", !newValue.isEmpty() && !isValidPort(Integer.parseInt(newValue)))
|
||||
validationSupport.registerValidator(publicProxyPort, Validator.combine(
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid proxy port", !newValue.isEmpty() && !isValidPort(Integer.parseInt(newValue)))
|
||||
));
|
||||
|
||||
validationSupport.registerValidator(coreHost, Validator.combine(
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid Core host", getHost(newValue) == null)
|
||||
));
|
||||
|
||||
validationSupport.registerValidator(corePort, Validator.combine(
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid Core port", !newValue.isEmpty() && !isValidPort(Integer.parseInt(newValue)))
|
||||
));
|
||||
|
||||
validationSupport.registerValidator(coreDataDir, Validator.combine(
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Core Data Dir required", coreAuthToggleGroup.getSelectedToggle().getUserData() == CoreAuthType.COOKIE && (newValue.isEmpty() || getDirectory(newValue) == null))
|
||||
));
|
||||
|
||||
validationSupport.registerValidator(coreUser, Validator.combine(
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Core user required", coreAuthToggleGroup.getSelectedToggle().getUserData() == CoreAuthType.USERPASS && newValue.isEmpty())
|
||||
));
|
||||
|
||||
validationSupport.registerValidator(corePass, Validator.combine(
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Core pass required", coreAuthToggleGroup.getSelectedToggle().getUserData() == CoreAuthType.USERPASS && newValue.isEmpty())
|
||||
));
|
||||
|
||||
validationSupport.registerValidator(electrumHost, Validator.combine(
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid Electrum host", getHost(newValue) == null)
|
||||
));
|
||||
|
||||
validationSupport.registerValidator(electrumPort, Validator.combine(
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid Electrum port", !newValue.isEmpty() && !isValidPort(Integer.parseInt(newValue)))
|
||||
));
|
||||
|
||||
validationSupport.registerValidator(proxyHost, Validator.combine(
|
||||
@@ -269,13 +581,51 @@ public class ServerPreferencesController extends PreferencesDetailController {
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid proxy port", !newValue.isEmpty() && !isValidPort(Integer.parseInt(newValue)))
|
||||
));
|
||||
|
||||
validationSupport.registerValidator(certificate, Validator.combine(
|
||||
validationSupport.registerValidator(electrumCertificate, Validator.combine(
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid certificate file", newValue != null && !newValue.isEmpty() && getCertificate(newValue) == null)
|
||||
));
|
||||
|
||||
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private ChangeListener<PublicElectrumServer> getPublicElectrumServerListener(Config config) {
|
||||
return (observable, oldValue, newValue) -> {
|
||||
config.setPublicElectrumServer(newValue.getUrl());
|
||||
};
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private ChangeListener<String> getBitcoinCoreListener(Config config) {
|
||||
return (observable, oldValue, newValue) -> {
|
||||
setCoreServerInConfig(config);
|
||||
};
|
||||
}
|
||||
|
||||
private void setCoreServerInConfig(Config config) {
|
||||
String hostAsString = getHost(coreHost.getText());
|
||||
Integer portAsInteger = getPort(corePort.getText());
|
||||
if(hostAsString != null && portAsInteger != null && isValidPort(portAsInteger)) {
|
||||
config.setCoreServer(Protocol.HTTP.toUrlString(hostAsString, portAsInteger));
|
||||
} else if(hostAsString != null) {
|
||||
config.setCoreServer(Protocol.HTTP.toUrlString(hostAsString));
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private ChangeListener<String> getBitcoinAuthListener(Config config) {
|
||||
return (observable, oldValue, newValue) -> {
|
||||
config.setCoreAuth(coreUser.getText() + ":" + corePass.getText());
|
||||
};
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private ChangeListener<String> getBitcoinWalletListener(Config config) {
|
||||
return (observable, oldValue, newValue) -> {
|
||||
config.setCoreWallet(coreWallet.getText());
|
||||
};
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private ChangeListener<String> getElectrumServerListener(Config config) {
|
||||
return (observable, oldValue, newValue) -> {
|
||||
@@ -284,8 +634,8 @@ public class ServerPreferencesController extends PreferencesDetailController {
|
||||
}
|
||||
|
||||
private void setElectrumServerInConfig(Config config) {
|
||||
String hostAsString = getHost(host.getText());
|
||||
Integer portAsInteger = getPort(port.getText());
|
||||
String hostAsString = getHost(electrumHost.getText());
|
||||
Integer portAsInteger = getPort(electrumPort.getText());
|
||||
if(hostAsString != null && portAsInteger != null && isValidPort(portAsInteger)) {
|
||||
config.setElectrumServer(getProtocol().toUrlString(hostAsString, portAsInteger));
|
||||
} else if(hostAsString != null) {
|
||||
@@ -311,7 +661,7 @@ public class ServerPreferencesController extends PreferencesDetailController {
|
||||
}
|
||||
|
||||
private Protocol getProtocol() {
|
||||
return (useSsl.isSelected() ? Protocol.SSL : Protocol.TCP);
|
||||
return (electrumUseSsl.isSelected() ? Protocol.SSL : Protocol.TCP);
|
||||
}
|
||||
|
||||
private String getHost(String text) {
|
||||
@@ -330,6 +680,19 @@ public class ServerPreferencesController extends PreferencesDetailController {
|
||||
}
|
||||
}
|
||||
|
||||
private File getDirectory(String dirLocation) {
|
||||
try {
|
||||
File dirFile = new File(dirLocation);
|
||||
if(!dirFile.exists() || !dirFile.isDirectory()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return dirFile;
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private File getCertificate(String crtFileLocation) {
|
||||
try {
|
||||
File crtFile = new File(crtFileLocation);
|
||||
@@ -357,4 +720,57 @@ public class ServerPreferencesController extends PreferencesDetailController {
|
||||
private static boolean isValidPort(int port) {
|
||||
return port >= 0 && port <= 65535;
|
||||
}
|
||||
|
||||
private File getDefaultCoreDataDir() {
|
||||
org.controlsfx.tools.Platform platform = org.controlsfx.tools.Platform.getCurrent();
|
||||
if(platform == org.controlsfx.tools.Platform.OSX) {
|
||||
return new File(System.getProperty("user.home") + "/Library/Application Support/Bitcoin");
|
||||
} else if(platform == org.controlsfx.tools.Platform.WINDOWS) {
|
||||
return new File(System.getenv("APPDATA") + "/Bitcoin");
|
||||
} else {
|
||||
return new File(System.getProperty("user.home") + "/.bitcoin");
|
||||
}
|
||||
}
|
||||
|
||||
private void setTestResultsFont() {
|
||||
org.controlsfx.tools.Platform platform = org.controlsfx.tools.Platform.getCurrent();
|
||||
if(platform == org.controlsfx.tools.Platform.OSX) {
|
||||
testResults.setFont(Font.font("Monaco", 11));
|
||||
} else if(platform == org.controlsfx.tools.Platform.WINDOWS) {
|
||||
testResults.setFont(Font.font("Lucida Console", 11));
|
||||
} else {
|
||||
testResults.setFont(Font.font("monospace", 11));
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void bwtStatus(BwtStatusEvent event) {
|
||||
if(!(event instanceof BwtSyncStatusEvent)) {
|
||||
testResults.appendText("\n" + event.getStatus());
|
||||
}
|
||||
if(event instanceof BwtReadyStatusEvent) {
|
||||
editConnection.setDisable(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void bwtSyncStatus(BwtSyncStatusEvent event) {
|
||||
editConnection.setDisable(false);
|
||||
if(connectionService != null && connectionService.isRunning() && event.getProgress() < 100) {
|
||||
DateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm");
|
||||
testResults.appendText("\nThe connection to the Bitcoin Core node was successful, but it is still syncing and cannot be used yet.");
|
||||
testResults.appendText("\nCurrently " + event.getProgress() + "% completed to date " + dateFormat.format(event.getTip()));
|
||||
testConnection.setGraphic(getGlyph(FontAwesome5.Glyph.QUESTION_CIRCLE, null));
|
||||
connectionService.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void torStatus(TorStatusEvent event) {
|
||||
Platform.runLater(() -> {
|
||||
if(torService != null && torService.isRunning()) {
|
||||
testResults.appendText("\n" + event.getStatus());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user