mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-12-25 05:06:45 +00:00
implement terminal mode
This commit is contained in:
parent
52696b014f
commit
19dedfa070
42 changed files with 3647 additions and 141 deletions
|
@ -104,6 +104,7 @@ dependencies {
|
||||||
implementation('org.apache.commons:commons-lang3:3.7')
|
implementation('org.apache.commons:commons-lang3:3.7')
|
||||||
implementation('net.sourceforge.streamsupport:streamsupport:1.7.0')
|
implementation('net.sourceforge.streamsupport:streamsupport:1.7.0')
|
||||||
implementation('com.github.librepdf:openpdf:1.3.27')
|
implementation('com.github.librepdf:openpdf:1.3.27')
|
||||||
|
implementation('com.googlecode.lanterna:lanterna:3.1.1')
|
||||||
testImplementation('junit:junit:4.12')
|
testImplementation('junit:junit:4.12')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1028,8 +1028,7 @@ public class AppController implements Initializable {
|
||||||
|
|
||||||
private void openWallet(Storage storage, WalletAndKey walletAndKey, AppController appController, boolean forceSameWindow) {
|
private void openWallet(Storage storage, WalletAndKey walletAndKey, AppController appController, boolean forceSameWindow) {
|
||||||
try {
|
try {
|
||||||
checkWalletNetwork(walletAndKey.getWallet());
|
storage.restorePublicKeysFromSeed(walletAndKey.getWallet(), walletAndKey.getKey());
|
||||||
restorePublicKeysFromSeed(storage, walletAndKey.getWallet(), walletAndKey.getKey());
|
|
||||||
if(!walletAndKey.getWallet().isValid()) {
|
if(!walletAndKey.getWallet().isValid()) {
|
||||||
throw new IllegalStateException("Wallet file is not valid.");
|
throw new IllegalStateException("Wallet file is not valid.");
|
||||||
}
|
}
|
||||||
|
@ -1048,97 +1047,6 @@ public class AppController implements Initializable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void checkWalletNetwork(Wallet wallet) {
|
|
||||||
if(wallet.getNetwork() != null && wallet.getNetwork() != Network.get()) {
|
|
||||||
throw new IllegalStateException("Provided " + wallet.getNetwork() + " wallet is invalid on a " + Network.get() + " network. Use a " + wallet.getNetwork() + " configuration to load this wallet.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void restorePublicKeysFromSeed(Storage storage, Wallet wallet, Key key) throws MnemonicException {
|
|
||||||
if(wallet.containsMasterPrivateKeys()) {
|
|
||||||
//Derive xpub and master fingerprint from seed, potentially with passphrase
|
|
||||||
Wallet copy = wallet.copy();
|
|
||||||
for(int i = 0; i < copy.getKeystores().size(); i++) {
|
|
||||||
Keystore copyKeystore = copy.getKeystores().get(i);
|
|
||||||
if(copyKeystore.hasSeed() && copyKeystore.getSeed().getPassphrase() == null) {
|
|
||||||
if(copyKeystore.getSeed().needsPassphrase()) {
|
|
||||||
if(!wallet.isMasterWallet() && wallet.getMasterWallet().getKeystores().size() == copy.getKeystores().size() && wallet.getMasterWallet().getKeystores().get(i).hasSeed()) {
|
|
||||||
copyKeystore.getSeed().setPassphrase(wallet.getMasterWallet().getKeystores().get(i).getSeed().getPassphrase());
|
|
||||||
} else {
|
|
||||||
KeystorePassphraseDialog passphraseDialog = new KeystorePassphraseDialog(wallet.getFullDisplayName(), copyKeystore);
|
|
||||||
Optional<String> optionalPassphrase = passphraseDialog.showAndWait();
|
|
||||||
if(optionalPassphrase.isPresent()) {
|
|
||||||
copyKeystore.getSeed().setPassphrase(optionalPassphrase.get());
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
copyKeystore.getSeed().setPassphrase("");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(wallet.isEncrypted()) {
|
|
||||||
if(key == null) {
|
|
||||||
throw new IllegalStateException("Wallet was not encrypted, but seed is");
|
|
||||||
}
|
|
||||||
|
|
||||||
copy.decrypt(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(wallet.isWhirlpoolMasterWallet()) {
|
|
||||||
String walletId = storage.getWalletId(wallet);
|
|
||||||
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(walletId);
|
|
||||||
whirlpool.setScode(wallet.getMasterMixConfig().getScode());
|
|
||||||
whirlpool.setHDWallet(storage.getWalletId(wallet), copy);
|
|
||||||
Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId);
|
|
||||||
soroban.setHDWallet(copy);
|
|
||||||
}
|
|
||||||
|
|
||||||
StandardAccount standardAccount = wallet.getStandardAccountType();
|
|
||||||
if(standardAccount != null && standardAccount.getMinimumGapLimit() != null && wallet.gapLimit() == null) {
|
|
||||||
wallet.setGapLimit(standardAccount.getMinimumGapLimit());
|
|
||||||
}
|
|
||||||
|
|
||||||
for(int i = 0; i < wallet.getKeystores().size(); i++) {
|
|
||||||
Keystore keystore = wallet.getKeystores().get(i);
|
|
||||||
if(keystore.hasSeed()) {
|
|
||||||
Keystore copyKeystore = copy.getKeystores().get(i);
|
|
||||||
Keystore derivedKeystore = Keystore.fromSeed(copyKeystore.getSeed(), copyKeystore.getKeyDerivation().getDerivation());
|
|
||||||
keystore.setKeyDerivation(derivedKeystore.getKeyDerivation());
|
|
||||||
keystore.setExtendedPublicKey(derivedKeystore.getExtendedPublicKey());
|
|
||||||
keystore.getSeed().setPassphrase(copyKeystore.getSeed().getPassphrase());
|
|
||||||
keystore.setBip47ExtendedPrivateKey(derivedKeystore.getBip47ExtendedPrivateKey());
|
|
||||||
copyKeystore.getSeed().clear();
|
|
||||||
} else if(keystore.hasMasterPrivateExtendedKey()) {
|
|
||||||
Keystore copyKeystore = copy.getKeystores().get(i);
|
|
||||||
Keystore derivedKeystore = Keystore.fromMasterPrivateExtendedKey(copyKeystore.getMasterPrivateExtendedKey(), copyKeystore.getKeyDerivation().getDerivation());
|
|
||||||
keystore.setKeyDerivation(derivedKeystore.getKeyDerivation());
|
|
||||||
keystore.setExtendedPublicKey(derivedKeystore.getExtendedPublicKey());
|
|
||||||
keystore.setBip47ExtendedPrivateKey(derivedKeystore.getBip47ExtendedPrivateKey());
|
|
||||||
copyKeystore.getMasterPrivateKey().clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for(Wallet childWallet : wallet.getChildWallets()) {
|
|
||||||
if(childWallet.isBip47()) {
|
|
||||||
try {
|
|
||||||
Keystore masterKeystore = wallet.getKeystores().get(0);
|
|
||||||
Keystore keystore = childWallet.getKeystores().get(0);
|
|
||||||
keystore.setBip47ExtendedPrivateKey(masterKeystore.getBip47ExtendedPrivateKey());
|
|
||||||
List<ChildNumber> derivation = keystore.getKeyDerivation().getDerivation();
|
|
||||||
keystore.setKeyDerivation(new KeyDerivation(masterKeystore.getKeyDerivation().getMasterFingerprint(), derivation));
|
|
||||||
DeterministicKey pubKey = keystore.getBip47ExtendedPrivateKey().getKey().dropPrivateBytes().dropParent();
|
|
||||||
keystore.setExtendedPublicKey(new ExtendedKey(pubKey, keystore.getBip47ExtendedPrivateKey().getParentFingerprint(), derivation.get(derivation.size() - 1)));
|
|
||||||
} catch(Exception e) {
|
|
||||||
log.error("Cannot prepare BIP47 keystore", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void importWallet(ActionEvent event) {
|
public void importWallet(ActionEvent event) {
|
||||||
WalletImportDialog dlg = new WalletImportDialog();
|
WalletImportDialog dlg = new WalletImportDialog();
|
||||||
Optional<Wallet> optionalWallet = dlg.showAndWait();
|
Optional<Wallet> optionalWallet = dlg.showAndWait();
|
||||||
|
@ -1234,14 +1142,12 @@ public class AppController implements Initializable {
|
||||||
try {
|
try {
|
||||||
storage.setEncryptionPubKey(Storage.NO_PASSWORD_KEY);
|
storage.setEncryptionPubKey(Storage.NO_PASSWORD_KEY);
|
||||||
storage.saveWallet(wallet);
|
storage.saveWallet(wallet);
|
||||||
checkWalletNetwork(wallet);
|
storage.restorePublicKeysFromSeed(wallet, null);
|
||||||
restorePublicKeysFromSeed(storage, wallet, null);
|
|
||||||
addWalletTabOrWindow(storage, wallet, false);
|
addWalletTabOrWindow(storage, wallet, false);
|
||||||
|
|
||||||
for(Wallet childWallet : wallet.getChildWallets()) {
|
for(Wallet childWallet : wallet.getChildWallets()) {
|
||||||
storage.saveWallet(childWallet);
|
storage.saveWallet(childWallet);
|
||||||
checkWalletNetwork(childWallet);
|
storage.restorePublicKeysFromSeed(childWallet, null);
|
||||||
restorePublicKeysFromSeed(storage, childWallet, null);
|
|
||||||
addWalletTabOrWindow(storage, childWallet, false);
|
addWalletTabOrWindow(storage, childWallet, false);
|
||||||
}
|
}
|
||||||
Platform.runLater(() -> selectTab(wallet));
|
Platform.runLater(() -> selectTab(wallet));
|
||||||
|
@ -1261,8 +1167,7 @@ public class AppController implements Initializable {
|
||||||
wallet.encrypt(key);
|
wallet.encrypt(key);
|
||||||
storage.setEncryptionPubKey(encryptionPubKey);
|
storage.setEncryptionPubKey(encryptionPubKey);
|
||||||
storage.saveWallet(wallet);
|
storage.saveWallet(wallet);
|
||||||
checkWalletNetwork(wallet);
|
storage.restorePublicKeysFromSeed(wallet, key);
|
||||||
restorePublicKeysFromSeed(storage, wallet, key);
|
|
||||||
addWalletTabOrWindow(storage, wallet, false);
|
addWalletTabOrWindow(storage, wallet, false);
|
||||||
|
|
||||||
for(Wallet childWallet : wallet.getChildWallets()) {
|
for(Wallet childWallet : wallet.getChildWallets()) {
|
||||||
|
@ -1270,8 +1175,7 @@ public class AppController implements Initializable {
|
||||||
childWallet.encrypt(key);
|
childWallet.encrypt(key);
|
||||||
}
|
}
|
||||||
storage.saveWallet(childWallet);
|
storage.saveWallet(childWallet);
|
||||||
checkWalletNetwork(childWallet);
|
storage.restorePublicKeysFromSeed(childWallet, key);
|
||||||
restorePublicKeysFromSeed(storage, childWallet, key);
|
|
||||||
addWalletTabOrWindow(storage, childWallet, false);
|
addWalletTabOrWindow(storage, childWallet, false);
|
||||||
}
|
}
|
||||||
Platform.runLater(() -> selectTab(wallet));
|
Platform.runLater(() -> selectTab(wallet));
|
||||||
|
|
|
@ -97,6 +97,8 @@ public class AppServices {
|
||||||
|
|
||||||
private final SorobanServices sorobanServices = new SorobanServices();
|
private final SorobanServices sorobanServices = new SorobanServices();
|
||||||
|
|
||||||
|
private InteractionServices interactionServices;
|
||||||
|
|
||||||
private static PayNymService payNymService;
|
private static PayNymService payNymService;
|
||||||
|
|
||||||
private final MainApp application;
|
private final MainApp application;
|
||||||
|
@ -173,8 +175,9 @@ public class AppServices {
|
||||||
openFiles(event.getFiles(), null);
|
openFiles(event.getFiles(), null);
|
||||||
};
|
};
|
||||||
|
|
||||||
public AppServices(MainApp application) {
|
private AppServices(MainApp application, InteractionServices interactionServices) {
|
||||||
this.application = application;
|
this.application = application;
|
||||||
|
this.interactionServices = interactionServices;
|
||||||
EventManager.get().register(this);
|
EventManager.get().register(this);
|
||||||
EventManager.get().register(whirlpoolServices);
|
EventManager.get().register(whirlpoolServices);
|
||||||
EventManager.get().register(sorobanServices);
|
EventManager.get().register(sorobanServices);
|
||||||
|
@ -500,7 +503,11 @@ public class AppServices {
|
||||||
}
|
}
|
||||||
|
|
||||||
static void initialize(MainApp application) {
|
static void initialize(MainApp application) {
|
||||||
INSTANCE = new AppServices(application);
|
INSTANCE = new AppServices(application, new DefaultInteractionServices());
|
||||||
|
}
|
||||||
|
|
||||||
|
static void initialize(MainApp application, InteractionServices interactionServices) {
|
||||||
|
INSTANCE = new AppServices(application, interactionServices);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static AppServices get() {
|
public static AppServices get() {
|
||||||
|
@ -515,6 +522,10 @@ public class AppServices {
|
||||||
return get().sorobanServices;
|
return get().sorobanServices;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static InteractionServices getInteractionServices() {
|
||||||
|
return get().interactionServices;
|
||||||
|
}
|
||||||
|
|
||||||
public static PayNymService getPayNymService() {
|
public static PayNymService getPayNymService() {
|
||||||
if(payNymService == null) {
|
if(payNymService == null) {
|
||||||
HostAndPort torProxy = getTorProxy();
|
HostAndPort torProxy = getTorProxy();
|
||||||
|
@ -746,40 +757,7 @@ public class AppServices {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Optional<ButtonType> showAlertDialog(String title, String content, Alert.AlertType alertType, Node graphic, ButtonType... buttons) {
|
public static Optional<ButtonType> showAlertDialog(String title, String content, Alert.AlertType alertType, Node graphic, ButtonType... buttons) {
|
||||||
Alert alert = new Alert(alertType, content, buttons);
|
return getInteractionServices().showAlert(title, content, alertType, graphic, buttons);
|
||||||
setStageIcon(alert.getDialogPane().getScene().getWindow());
|
|
||||||
alert.getDialogPane().getScene().getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
|
||||||
alert.setTitle(title);
|
|
||||||
alert.setHeaderText(title);
|
|
||||||
if(graphic != null) {
|
|
||||||
alert.setGraphic(graphic);
|
|
||||||
}
|
|
||||||
|
|
||||||
Pattern linkPattern = Pattern.compile("\\[(http.+)]");
|
|
||||||
Matcher matcher = linkPattern.matcher(content);
|
|
||||||
if(matcher.find()) {
|
|
||||||
String link = matcher.group(1);
|
|
||||||
HyperlinkLabel hyperlinkLabel = new HyperlinkLabel(content);
|
|
||||||
hyperlinkLabel.setMaxWidth(Double.MAX_VALUE);
|
|
||||||
hyperlinkLabel.setMaxHeight(Double.MAX_VALUE);
|
|
||||||
hyperlinkLabel.getStyleClass().add("content");
|
|
||||||
Label label = new Label();
|
|
||||||
hyperlinkLabel.setPrefWidth(Math.max(360, TextUtils.computeTextWidth(label.getFont(), link, 0.0D) + 50));
|
|
||||||
hyperlinkLabel.setOnAction(event -> {
|
|
||||||
alert.close();
|
|
||||||
get().getApplication().getHostServices().showDocument(link);
|
|
||||||
});
|
|
||||||
alert.getDialogPane().setContent(hyperlinkLabel);
|
|
||||||
}
|
|
||||||
|
|
||||||
String[] lines = content.split("\r\n|\r|\n");
|
|
||||||
if(lines.length > 3 || org.controlsfx.tools.Platform.getCurrent() == org.controlsfx.tools.Platform.WINDOWS) {
|
|
||||||
double numLines = Arrays.stream(lines).mapToDouble(line -> Math.ceil(TextUtils.computeTextWidth(Font.getDefault(), line, 0) / 300)).sum();
|
|
||||||
alert.getDialogPane().setPrefHeight(200 + numLines * 20);
|
|
||||||
}
|
|
||||||
|
|
||||||
moveToActiveWindowScreen(alert);
|
|
||||||
return alert.showAndWait();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void setStageIcon(Window window) {
|
public static void setStageIcon(Window window) {
|
||||||
|
@ -1165,6 +1143,10 @@ public class AppServices {
|
||||||
@Subscribe
|
@Subscribe
|
||||||
public void requestDisconnect(RequestDisconnectEvent event) {
|
public void requestDisconnect(RequestDisconnectEvent event) {
|
||||||
onlineProperty.set(false);
|
onlineProperty.set(false);
|
||||||
|
//Ensure services don't try to reconnect
|
||||||
|
connectionService.cancel();
|
||||||
|
ratesService.cancel();
|
||||||
|
versionCheckService.cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Subscribe
|
@Subscribe
|
||||||
|
|
|
@ -17,6 +17,9 @@ public class Args {
|
||||||
@Parameter(names = { "--level", "-l" }, description = "Set log level")
|
@Parameter(names = { "--level", "-l" }, description = "Set log level")
|
||||||
public Level level;
|
public Level level;
|
||||||
|
|
||||||
|
@Parameter(names = { "--terminal", "-t" }, description = "Terminal mode", arity = 0)
|
||||||
|
public boolean terminal;
|
||||||
|
|
||||||
@Parameter(names = { "--help", "-h" }, description = "Show usage", help = true)
|
@Parameter(names = { "--help", "-h" }, description = "Show usage", help = true)
|
||||||
public boolean help;
|
public boolean help;
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
package com.sparrowwallet.sparrow;
|
||||||
|
|
||||||
|
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||||
|
import com.sparrowwallet.sparrow.control.KeystorePassphraseDialog;
|
||||||
|
import com.sparrowwallet.sparrow.control.TextUtils;
|
||||||
|
import javafx.scene.Node;
|
||||||
|
import javafx.scene.control.Alert;
|
||||||
|
import javafx.scene.control.ButtonType;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.text.Font;
|
||||||
|
import org.controlsfx.control.HyperlinkLabel;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import static com.sparrowwallet.sparrow.AppServices.moveToActiveWindowScreen;
|
||||||
|
import static com.sparrowwallet.sparrow.AppServices.setStageIcon;
|
||||||
|
|
||||||
|
public class DefaultInteractionServices implements InteractionServices {
|
||||||
|
@Override
|
||||||
|
public Optional<ButtonType> showAlert(String title, String content, Alert.AlertType alertType, Node graphic, 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);
|
||||||
|
if(graphic != null) {
|
||||||
|
alert.setGraphic(graphic);
|
||||||
|
}
|
||||||
|
|
||||||
|
Pattern linkPattern = Pattern.compile("\\[(http.+)]");
|
||||||
|
Matcher matcher = linkPattern.matcher(content);
|
||||||
|
if(matcher.find()) {
|
||||||
|
String link = matcher.group(1);
|
||||||
|
HyperlinkLabel hyperlinkLabel = new HyperlinkLabel(content);
|
||||||
|
hyperlinkLabel.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
hyperlinkLabel.setMaxHeight(Double.MAX_VALUE);
|
||||||
|
hyperlinkLabel.getStyleClass().add("content");
|
||||||
|
Label label = new Label();
|
||||||
|
hyperlinkLabel.setPrefWidth(Math.max(360, TextUtils.computeTextWidth(label.getFont(), link, 0.0D) + 50));
|
||||||
|
hyperlinkLabel.setOnAction(event -> {
|
||||||
|
alert.close();
|
||||||
|
AppServices.get().getApplication().getHostServices().showDocument(link);
|
||||||
|
});
|
||||||
|
alert.getDialogPane().setContent(hyperlinkLabel);
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] lines = content.split("\r\n|\r|\n");
|
||||||
|
if(lines.length > 3 || org.controlsfx.tools.Platform.getCurrent() == org.controlsfx.tools.Platform.WINDOWS) {
|
||||||
|
double numLines = Arrays.stream(lines).mapToDouble(line -> Math.ceil(TextUtils.computeTextWidth(Font.getDefault(), line, 0) / 300)).sum();
|
||||||
|
alert.getDialogPane().setPrefHeight(200 + numLines * 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
moveToActiveWindowScreen(alert);
|
||||||
|
return alert.showAndWait();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<String> requestPassphrase(String walletName, Keystore keystore) {
|
||||||
|
KeystorePassphraseDialog passphraseDialog = new KeystorePassphraseDialog(walletName, keystore);
|
||||||
|
return passphraseDialog.showAndWait();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
package com.sparrowwallet.sparrow;
|
||||||
|
|
||||||
|
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||||
|
import javafx.scene.Node;
|
||||||
|
import javafx.scene.control.Alert;
|
||||||
|
import javafx.scene.control.ButtonType;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public interface InteractionServices {
|
||||||
|
Optional<ButtonType> showAlert(String title, String content, Alert.AlertType alertType, Node graphic, ButtonType... buttons);
|
||||||
|
Optional<String> requestPassphrase(String walletName, Keystore keystore);
|
||||||
|
}
|
|
@ -14,6 +14,8 @@ import com.sparrowwallet.sparrow.preferences.PreferenceGroup;
|
||||||
import com.sparrowwallet.sparrow.preferences.PreferencesDialog;
|
import com.sparrowwallet.sparrow.preferences.PreferencesDialog;
|
||||||
import com.sparrowwallet.sparrow.instance.InstanceException;
|
import com.sparrowwallet.sparrow.instance.InstanceException;
|
||||||
import com.sparrowwallet.sparrow.instance.InstanceList;
|
import com.sparrowwallet.sparrow.instance.InstanceList;
|
||||||
|
import com.sparrowwallet.sparrow.terminal.SparrowTerminal;
|
||||||
|
import com.sparrowwallet.sparrow.terminal.TerminalInteractionServices;
|
||||||
import javafx.application.Application;
|
import javafx.application.Application;
|
||||||
import javafx.scene.text.Font;
|
import javafx.scene.text.Font;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
|
@ -179,6 +181,13 @@ public class MainApp extends Application {
|
||||||
getLogger().info("Using " + Network.get() + " configuration");
|
getLogger().info("Using " + Network.get() + " configuration");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(args.terminal) {
|
||||||
|
MainApp mainApp = new MainApp();
|
||||||
|
AppServices.initialize(mainApp, new TerminalInteractionServices());
|
||||||
|
SparrowTerminal.startTerminal();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
List<String> fileUriArguments = jCommander.getUnknownOptions();
|
List<String> fileUriArguments = jCommander.getUnknownOptions();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
package com.sparrowwallet.sparrow.io;
|
package com.sparrowwallet.sparrow.io;
|
||||||
|
|
||||||
import com.sparrowwallet.drongo.Network;
|
import com.sparrowwallet.drongo.*;
|
||||||
import com.sparrowwallet.drongo.SecureString;
|
|
||||||
import com.sparrowwallet.drongo.Utils;
|
|
||||||
import com.sparrowwallet.drongo.crypto.*;
|
import com.sparrowwallet.drongo.crypto.*;
|
||||||
|
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||||
|
import com.sparrowwallet.drongo.wallet.MnemonicException;
|
||||||
|
import com.sparrowwallet.drongo.wallet.StandardAccount;
|
||||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||||
import com.sparrowwallet.sparrow.AppServices;
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
import com.sparrowwallet.sparrow.MainApp;
|
import com.sparrowwallet.sparrow.MainApp;
|
||||||
|
import com.sparrowwallet.sparrow.soroban.Soroban;
|
||||||
|
import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
|
||||||
import javafx.concurrent.Service;
|
import javafx.concurrent.Service;
|
||||||
import javafx.concurrent.Task;
|
import javafx.concurrent.Task;
|
||||||
import org.apache.commons.lang3.concurrent.BasicThreadFactory;
|
import org.apache.commons.lang3.concurrent.BasicThreadFactory;
|
||||||
|
@ -136,6 +139,98 @@ public class Storage {
|
||||||
closePersistenceService.start();
|
closePersistenceService.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void restorePublicKeysFromSeed(Wallet wallet, Key key) throws MnemonicException {
|
||||||
|
checkWalletNetwork(wallet);
|
||||||
|
|
||||||
|
if(wallet.containsMasterPrivateKeys()) {
|
||||||
|
//Derive xpub and master fingerprint from seed, potentially with passphrase
|
||||||
|
Wallet copy = wallet.copy();
|
||||||
|
for(int i = 0; i < copy.getKeystores().size(); i++) {
|
||||||
|
Keystore copyKeystore = copy.getKeystores().get(i);
|
||||||
|
if(copyKeystore.hasSeed() && copyKeystore.getSeed().getPassphrase() == null) {
|
||||||
|
if(copyKeystore.getSeed().needsPassphrase()) {
|
||||||
|
if(!wallet.isMasterWallet() && wallet.getMasterWallet().getKeystores().size() == copy.getKeystores().size() && wallet.getMasterWallet().getKeystores().get(i).hasSeed()) {
|
||||||
|
copyKeystore.getSeed().setPassphrase(wallet.getMasterWallet().getKeystores().get(i).getSeed().getPassphrase());
|
||||||
|
} else {
|
||||||
|
Optional<String> optionalPassphrase = AppServices.getInteractionServices().requestPassphrase(wallet.getFullDisplayName(), copyKeystore);
|
||||||
|
if(optionalPassphrase.isPresent()) {
|
||||||
|
copyKeystore.getSeed().setPassphrase(optionalPassphrase.get());
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
copyKeystore.getSeed().setPassphrase("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(wallet.isEncrypted()) {
|
||||||
|
if(key == null) {
|
||||||
|
throw new IllegalStateException("Wallet was not encrypted, but seed is");
|
||||||
|
}
|
||||||
|
|
||||||
|
copy.decrypt(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(wallet.isWhirlpoolMasterWallet()) {
|
||||||
|
String walletId = getWalletId(wallet);
|
||||||
|
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(walletId);
|
||||||
|
whirlpool.setScode(wallet.getMasterMixConfig().getScode());
|
||||||
|
whirlpool.setHDWallet(getWalletId(wallet), copy);
|
||||||
|
Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId);
|
||||||
|
soroban.setHDWallet(copy);
|
||||||
|
}
|
||||||
|
|
||||||
|
StandardAccount standardAccount = wallet.getStandardAccountType();
|
||||||
|
if(standardAccount != null && standardAccount.getMinimumGapLimit() != null && wallet.gapLimit() == null) {
|
||||||
|
wallet.setGapLimit(standardAccount.getMinimumGapLimit());
|
||||||
|
}
|
||||||
|
|
||||||
|
for(int i = 0; i < wallet.getKeystores().size(); i++) {
|
||||||
|
Keystore keystore = wallet.getKeystores().get(i);
|
||||||
|
if(keystore.hasSeed()) {
|
||||||
|
Keystore copyKeystore = copy.getKeystores().get(i);
|
||||||
|
Keystore derivedKeystore = Keystore.fromSeed(copyKeystore.getSeed(), copyKeystore.getKeyDerivation().getDerivation());
|
||||||
|
keystore.setKeyDerivation(derivedKeystore.getKeyDerivation());
|
||||||
|
keystore.setExtendedPublicKey(derivedKeystore.getExtendedPublicKey());
|
||||||
|
keystore.getSeed().setPassphrase(copyKeystore.getSeed().getPassphrase());
|
||||||
|
keystore.setBip47ExtendedPrivateKey(derivedKeystore.getBip47ExtendedPrivateKey());
|
||||||
|
copyKeystore.getSeed().clear();
|
||||||
|
} else if(keystore.hasMasterPrivateExtendedKey()) {
|
||||||
|
Keystore copyKeystore = copy.getKeystores().get(i);
|
||||||
|
Keystore derivedKeystore = Keystore.fromMasterPrivateExtendedKey(copyKeystore.getMasterPrivateExtendedKey(), copyKeystore.getKeyDerivation().getDerivation());
|
||||||
|
keystore.setKeyDerivation(derivedKeystore.getKeyDerivation());
|
||||||
|
keystore.setExtendedPublicKey(derivedKeystore.getExtendedPublicKey());
|
||||||
|
keystore.setBip47ExtendedPrivateKey(derivedKeystore.getBip47ExtendedPrivateKey());
|
||||||
|
copyKeystore.getMasterPrivateKey().clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for(Wallet childWallet : wallet.getChildWallets()) {
|
||||||
|
if(childWallet.isBip47()) {
|
||||||
|
try {
|
||||||
|
Keystore masterKeystore = wallet.getKeystores().get(0);
|
||||||
|
Keystore keystore = childWallet.getKeystores().get(0);
|
||||||
|
keystore.setBip47ExtendedPrivateKey(masterKeystore.getBip47ExtendedPrivateKey());
|
||||||
|
List<ChildNumber> derivation = keystore.getKeyDerivation().getDerivation();
|
||||||
|
keystore.setKeyDerivation(new KeyDerivation(masterKeystore.getKeyDerivation().getMasterFingerprint(), derivation));
|
||||||
|
DeterministicKey pubKey = keystore.getBip47ExtendedPrivateKey().getKey().dropPrivateBytes().dropParent();
|
||||||
|
keystore.setExtendedPublicKey(new ExtendedKey(pubKey, keystore.getBip47ExtendedPrivateKey().getParentFingerprint(), derivation.get(derivation.size() - 1)));
|
||||||
|
} catch(Exception e) {
|
||||||
|
log.error("Cannot prepare BIP47 keystore", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkWalletNetwork(Wallet wallet) {
|
||||||
|
if(wallet.getNetwork() != null && wallet.getNetwork() != Network.get()) {
|
||||||
|
throw new IllegalStateException("Provided " + wallet.getNetwork() + " wallet is invalid on a " + Network.get() + " network. Use a " + wallet.getNetwork() + " configuration to load this wallet.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void backupWallet() throws IOException {
|
public void backupWallet() throws IOException {
|
||||||
if(walletFile.toPath().startsWith(getWalletsDir().toPath())) {
|
if(walletFile.toPath().startsWith(getWalletsDir().toPath())) {
|
||||||
backupWallet(null);
|
backupWallet(null);
|
||||||
|
|
|
@ -0,0 +1,77 @@
|
||||||
|
package com.sparrowwallet.sparrow.terminal;
|
||||||
|
|
||||||
|
import com.googlecode.lanterna.TerminalSize;
|
||||||
|
import com.googlecode.lanterna.gui2.ActionListBox;
|
||||||
|
import com.googlecode.lanterna.gui2.dialogs.ActionListDialogBuilder;
|
||||||
|
import com.googlecode.lanterna.gui2.dialogs.FileDialogBuilder;
|
||||||
|
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||||
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
|
import com.sparrowwallet.sparrow.io.Config;
|
||||||
|
import com.sparrowwallet.sparrow.io.Storage;
|
||||||
|
import com.sparrowwallet.sparrow.terminal.preferences.GeneralDialog;
|
||||||
|
import com.sparrowwallet.sparrow.terminal.preferences.ServerStatusDialog;
|
||||||
|
import com.sparrowwallet.sparrow.terminal.preferences.ServerTypeDialog;
|
||||||
|
import com.sparrowwallet.sparrow.terminal.wallet.LoadWallet;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public class MasterActionListBox extends ActionListBox {
|
||||||
|
public static final int MAX_RECENT_WALLETS = 6;
|
||||||
|
|
||||||
|
public MasterActionListBox(SparrowTerminal sparrowTerminal) {
|
||||||
|
super(new TerminalSize(14, 3));
|
||||||
|
|
||||||
|
addItem("Wallets", () -> {
|
||||||
|
ActionListDialogBuilder builder = new ActionListDialogBuilder();
|
||||||
|
builder.setTitle("Wallets");
|
||||||
|
for(int i = 0; i < Config.get().getRecentWalletFiles().size() && i < MAX_RECENT_WALLETS; i++) {
|
||||||
|
File recentWalletFile = Config.get().getRecentWalletFiles().get(i);
|
||||||
|
Storage storage = new Storage(recentWalletFile);
|
||||||
|
|
||||||
|
Optional<Wallet> optWallet = AppServices.get().getOpenWallets().entrySet().stream()
|
||||||
|
.filter(entry -> entry.getValue().getWalletFile().equals(recentWalletFile)).map(Map.Entry::getKey)
|
||||||
|
.map(wallet -> wallet.isMasterWallet() ? wallet : wallet.getMasterWallet()).findFirst();
|
||||||
|
if(optWallet.isPresent()) {
|
||||||
|
builder.addAction(storage.getWalletName(null) + "*", () -> LoadWallet.getOpeningDialog(optWallet.get()).showDialog(SparrowTerminal.get().getGui()));
|
||||||
|
} else {
|
||||||
|
builder.addAction(storage.getWalletName(null), new LoadWallet(storage));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
builder.addAction("Open Wallet...", () -> {
|
||||||
|
FileDialogBuilder openBuilder = new FileDialogBuilder().setTitle("Open Wallet");
|
||||||
|
openBuilder.setShowHiddenDirectories(true);
|
||||||
|
openBuilder.setSelectedFile(Storage.getWalletsDir());
|
||||||
|
File file = openBuilder.build().showDialog(SparrowTerminal.get().getGui());
|
||||||
|
if(file != null) {
|
||||||
|
LoadWallet loadWallet = new LoadWallet(new Storage(file));
|
||||||
|
SparrowTerminal.get().getGui().getGUIThread().invokeLater(loadWallet);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
builder.build().showDialog(SparrowTerminal.get().getGui());
|
||||||
|
});
|
||||||
|
|
||||||
|
addItem("Preferences", () -> {
|
||||||
|
new ActionListDialogBuilder()
|
||||||
|
.setTitle("Preferences")
|
||||||
|
.addAction("General", () -> {
|
||||||
|
GeneralDialog generalDialog = new GeneralDialog();
|
||||||
|
generalDialog.showDialog(sparrowTerminal.getGui());
|
||||||
|
})
|
||||||
|
.addAction("Server", () -> {
|
||||||
|
if(Config.get().hasServer()) {
|
||||||
|
ServerStatusDialog serverStatusDialog = new ServerStatusDialog();
|
||||||
|
serverStatusDialog.showDialog(sparrowTerminal.getGui());
|
||||||
|
} else {
|
||||||
|
ServerTypeDialog serverTypeDialog = new ServerTypeDialog();
|
||||||
|
serverTypeDialog.showDialog(sparrowTerminal.getGui());
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.build()
|
||||||
|
.showDialog(sparrowTerminal.getGui());
|
||||||
|
});
|
||||||
|
|
||||||
|
addItem("Quit", sparrowTerminal::stop);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
package com.sparrowwallet.sparrow.terminal;
|
||||||
|
|
||||||
|
import com.googlecode.lanterna.gui2.BasicWindow;
|
||||||
|
import com.googlecode.lanterna.gui2.BorderLayout;
|
||||||
|
import com.googlecode.lanterna.gui2.Panel;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class MasterActionWindow extends BasicWindow {
|
||||||
|
public MasterActionWindow(SparrowTerminal sparrowTerminal) {
|
||||||
|
setHints(List.of(Hint.CENTERED));
|
||||||
|
|
||||||
|
Panel panel = new Panel(new BorderLayout());
|
||||||
|
|
||||||
|
MasterActionListBox masterActionListBox = new MasterActionListBox(sparrowTerminal);
|
||||||
|
panel.addComponent(masterActionListBox, BorderLayout.Location.CENTER);
|
||||||
|
|
||||||
|
setComponent(panel);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
package com.sparrowwallet.sparrow.terminal;
|
||||||
|
|
||||||
|
import com.googlecode.lanterna.TextColor;
|
||||||
|
import com.googlecode.lanterna.gui2.DefaultWindowManager;
|
||||||
|
import com.googlecode.lanterna.gui2.EmptySpace;
|
||||||
|
import com.googlecode.lanterna.screen.Screen;
|
||||||
|
import com.googlecode.lanterna.screen.TerminalScreen;
|
||||||
|
import com.googlecode.lanterna.terminal.DefaultTerminalFactory;
|
||||||
|
import com.googlecode.lanterna.terminal.Terminal;
|
||||||
|
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||||
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
|
import com.sparrowwallet.sparrow.EventManager;
|
||||||
|
import com.sparrowwallet.sparrow.terminal.wallet.WalletData;
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class SparrowTerminal {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(SparrowTerminal.class);
|
||||||
|
|
||||||
|
private static SparrowTerminal sparrowTerminal;
|
||||||
|
|
||||||
|
private final Terminal terminal;
|
||||||
|
private final Screen screen;
|
||||||
|
private final SparrowTextGui gui;
|
||||||
|
|
||||||
|
private final Map<Wallet, WalletData> walletData = new HashMap<>();
|
||||||
|
|
||||||
|
private SparrowTerminal() throws IOException {
|
||||||
|
this.terminal = new DefaultTerminalFactory().createTerminal();
|
||||||
|
this.screen = new TerminalScreen(terminal);
|
||||||
|
this.gui = new SparrowTextGui(this, screen, new DefaultWindowManager(), new EmptySpace(TextColor.ANSI.BLUE));
|
||||||
|
EventManager.get().register(gui);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Screen getScreen() {
|
||||||
|
return screen;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SparrowTextGui getGui() {
|
||||||
|
return gui;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<Wallet, WalletData> getWalletData() {
|
||||||
|
return walletData;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stop() {
|
||||||
|
try {
|
||||||
|
screen.stopScreen();
|
||||||
|
terminal.exitPrivateMode();
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
AppServices.get().stop();
|
||||||
|
Platform.exit();
|
||||||
|
});
|
||||||
|
} catch(Exception e) {
|
||||||
|
log.error("Could not stop terminal", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void startTerminal() {
|
||||||
|
try {
|
||||||
|
sparrowTerminal = new SparrowTerminal();
|
||||||
|
sparrowTerminal.getScreen().startScreen();
|
||||||
|
sparrowTerminal.getGui().getMainWindow().waitUntilClosed();
|
||||||
|
} catch(Exception e) {
|
||||||
|
log.error("Could not start terminal", e);
|
||||||
|
System.err.println("Could not start terminal: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SparrowTerminal get() {
|
||||||
|
return sparrowTerminal;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,166 @@
|
||||||
|
package com.sparrowwallet.sparrow.terminal;
|
||||||
|
|
||||||
|
import com.google.common.eventbus.Subscribe;
|
||||||
|
import com.googlecode.lanterna.TerminalSize;
|
||||||
|
import com.googlecode.lanterna.TextColor;
|
||||||
|
import com.googlecode.lanterna.gui2.*;
|
||||||
|
import com.googlecode.lanterna.screen.Screen;
|
||||||
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
|
import com.sparrowwallet.sparrow.event.*;
|
||||||
|
import com.sparrowwallet.sparrow.io.Config;
|
||||||
|
import com.sparrowwallet.sparrow.net.ServerType;
|
||||||
|
import javafx.animation.*;
|
||||||
|
import javafx.beans.property.DoubleProperty;
|
||||||
|
import javafx.beans.property.SimpleDoubleProperty;
|
||||||
|
import javafx.util.Duration;
|
||||||
|
|
||||||
|
public class SparrowTextGui extends MultiWindowTextGUI {
|
||||||
|
private final BasicWindow mainWindow;
|
||||||
|
|
||||||
|
private final Label connectedLabel;
|
||||||
|
private final Label statusLabel;
|
||||||
|
private final ProgressBar statusProgress;
|
||||||
|
|
||||||
|
private PauseTransition wait;
|
||||||
|
private Timeline statusTimeline;
|
||||||
|
private final DoubleProperty progressProperty = new SimpleDoubleProperty();
|
||||||
|
|
||||||
|
public SparrowTextGui(SparrowTerminal sparrowTerminal, Screen screen, WindowManager windowManager, Component background) {
|
||||||
|
super(screen, windowManager, background);
|
||||||
|
|
||||||
|
this.mainWindow = new MasterActionWindow(sparrowTerminal);
|
||||||
|
addWindow(mainWindow);
|
||||||
|
|
||||||
|
Panel panel = new Panel(new BorderLayout());
|
||||||
|
|
||||||
|
Panel titleBar = new Panel(new GridLayout(2));
|
||||||
|
new Label("Sparrow Terminal").addTo(titleBar);
|
||||||
|
this.connectedLabel = new Label("Disconnected");
|
||||||
|
titleBar.addComponent(connectedLabel, GridLayout.createLayoutData(GridLayout.Alignment.END, GridLayout.Alignment.CENTER, true, false));
|
||||||
|
panel.addComponent(titleBar, BorderLayout.Location.TOP);
|
||||||
|
|
||||||
|
panel.addComponent(new EmptySpace(TextColor.ANSI.BLUE));
|
||||||
|
|
||||||
|
Panel statusBar = new Panel(new GridLayout(2));
|
||||||
|
this.statusLabel = new Label("").addTo(statusBar);
|
||||||
|
this.statusProgress = new ProgressBar(0, 100, 10);
|
||||||
|
statusBar.addComponent(statusProgress, GridLayout.createLayoutData(GridLayout.Alignment.END, GridLayout.Alignment.CENTER, true, false));
|
||||||
|
statusProgress.setVisible(false);
|
||||||
|
statusProgress.setLabelFormat(null);
|
||||||
|
progressProperty.addListener((observable, oldValue, newValue) -> statusProgress.setValue((int) (newValue.doubleValue() * 100)));
|
||||||
|
|
||||||
|
panel.addComponent(statusBar, BorderLayout.Location.BOTTOM);
|
||||||
|
getBackgroundPane().setComponent(panel);
|
||||||
|
|
||||||
|
getMainWindow().addWindowListener(new WindowListenerAdapter() {
|
||||||
|
@Override
|
||||||
|
public void onResized(Window window, TerminalSize oldSize, TerminalSize newSize) {
|
||||||
|
titleBar.invalidate();
|
||||||
|
statusBar.invalidate();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
AppServices.get().start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public BasicWindow getMainWindow() {
|
||||||
|
return mainWindow;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setConnectedLabel(boolean connected) {
|
||||||
|
getGUIThread().invokeLater(() -> {
|
||||||
|
connectedLabel.setText(connected ? "Connected" : "Disconnected");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void connectionStart(ConnectionStartEvent event) {
|
||||||
|
statusUpdated(new StatusEvent(event.getStatus(), 120));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void connectionFailed(ConnectionFailedEvent event) {
|
||||||
|
setConnectedLabel(false);
|
||||||
|
statusUpdated(new StatusEvent("Connection failed: " + event.getMessage()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void connection(ConnectionEvent event) {
|
||||||
|
setConnectedLabel(true);
|
||||||
|
statusUpdated(new StatusEvent("Connected to " + Config.get().getServerDisplayName() + " at height " + event.getBlockHeight()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void disconnection(DisconnectionEvent event) {
|
||||||
|
if(!AppServices.isConnecting() && !AppServices.isConnected()) {
|
||||||
|
setConnectedLabel(false);
|
||||||
|
statusUpdated(new StatusEvent("Disconnected"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void statusUpdated(StatusEvent event) {
|
||||||
|
getGUIThread().invokeLater(() -> statusLabel.setText(event.getStatus()));
|
||||||
|
|
||||||
|
if(wait != null && wait.getStatus() == Animation.Status.RUNNING) {
|
||||||
|
wait.stop();
|
||||||
|
}
|
||||||
|
wait = new PauseTransition(Duration.seconds(event.getShowDuration()));
|
||||||
|
wait.setOnFinished((e) -> {
|
||||||
|
if(statusLabel.getText().equals(event.getStatus())) {
|
||||||
|
getGUIThread().invokeLater(() -> statusLabel.setText(""));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
wait.play();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void timedEvent(TimedEvent event) {
|
||||||
|
if(event.getTimeMills() == 0) {
|
||||||
|
getGUIThread().invokeLater(() -> {
|
||||||
|
statusLabel.setText("");
|
||||||
|
statusProgress.setVisible(false);
|
||||||
|
statusProgress.setValue(0);
|
||||||
|
});
|
||||||
|
} else if(event.getTimeMills() < 0) {
|
||||||
|
getGUIThread().invokeLater(() -> {
|
||||||
|
statusLabel.setText(event.getStatus());
|
||||||
|
statusProgress.setVisible(false);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
getGUIThread().invokeLater(() -> {
|
||||||
|
statusLabel.setText(event.getStatus());
|
||||||
|
statusProgress.setVisible(true);
|
||||||
|
});
|
||||||
|
statusTimeline = new Timeline(
|
||||||
|
new KeyFrame(Duration.ZERO, new KeyValue(progressProperty, 0)),
|
||||||
|
new KeyFrame(Duration.millis(event.getTimeMills()), e -> {
|
||||||
|
getGUIThread().invokeLater(() -> {
|
||||||
|
statusLabel.setText("");
|
||||||
|
statusProgress.setVisible(false);
|
||||||
|
});
|
||||||
|
}, new KeyValue(progressProperty, 1))
|
||||||
|
);
|
||||||
|
statusTimeline.setCycleCount(1);
|
||||||
|
statusTimeline.play();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void walletHistoryStarted(WalletHistoryStartedEvent event) {
|
||||||
|
statusUpdated(new StatusEvent("Loading wallet history..."));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void walletHistoryFinished(WalletHistoryFinishedEvent event) {
|
||||||
|
if(statusLabel.getText().equals("Loading wallet history...")) {
|
||||||
|
getGUIThread().invokeLater(() -> statusLabel.setText(""));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void walletHistoryFailed(WalletHistoryFailedEvent event) {
|
||||||
|
walletHistoryFinished(new WalletHistoryFinishedEvent(event.getWallet()));
|
||||||
|
statusUpdated(new StatusEvent("Error retrieving wallet history" + (Config.get().getServerType() == ServerType.PUBLIC_ELECTRUM_SERVER ? ", trying another server..." : "")));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,110 @@
|
||||||
|
package com.sparrowwallet.sparrow.terminal;
|
||||||
|
|
||||||
|
import com.googlecode.lanterna.gui2.dialogs.MessageDialogBuilder;
|
||||||
|
import com.googlecode.lanterna.gui2.dialogs.MessageDialogButton;
|
||||||
|
import com.googlecode.lanterna.gui2.dialogs.TextInputDialogBuilder;
|
||||||
|
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||||
|
import com.sparrowwallet.sparrow.InteractionServices;
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import javafx.scene.Node;
|
||||||
|
import javafx.scene.control.Alert;
|
||||||
|
import javafx.scene.control.ButtonType;
|
||||||
|
|
||||||
|
import java.text.BreakIterator;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public class TerminalInteractionServices implements InteractionServices {
|
||||||
|
private final Object alertShowing = new Object();
|
||||||
|
private final Object passphraseShowing = new Object();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<ButtonType> showAlert(String title, String content, Alert.AlertType alertType, Node graphic, ButtonType... buttons) {
|
||||||
|
if(Platform.isFxApplicationThread()) {
|
||||||
|
SparrowTerminal.get().getGui().getGUIThread().invokeLater(() -> {
|
||||||
|
Optional<ButtonType> optButtonType = showMessageDialog(title, content, buttons);
|
||||||
|
Platform.runLater(() -> Platform.exitNestedEventLoop(alertShowing, optButtonType));
|
||||||
|
});
|
||||||
|
return (Optional<ButtonType>)Platform.enterNestedEventLoop(alertShowing);
|
||||||
|
} else {
|
||||||
|
return showMessageDialog(title, content, buttons);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<ButtonType> showMessageDialog(String title, String content, ButtonType[] buttons) {
|
||||||
|
String formattedContent = formatLines(content, 50);
|
||||||
|
|
||||||
|
MessageDialogBuilder builder = new MessageDialogBuilder().setTitle(title).setText(formattedContent);
|
||||||
|
for(ButtonType buttonType : buttons) {
|
||||||
|
builder.addButton(getButton(buttonType));
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageDialogButton button = builder.build().showDialog(SparrowTerminal.get().getGui());
|
||||||
|
return Arrays.stream(buttons).filter(buttonType -> button.equals(getButton(buttonType))).findFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatLines(String input, int maxLength) {
|
||||||
|
StringBuilder builder = new StringBuilder();
|
||||||
|
BreakIterator boundary = BreakIterator.getLineInstance(Locale.ROOT);
|
||||||
|
boundary.setText(input);
|
||||||
|
int start = boundary.first();
|
||||||
|
int end = boundary.next();
|
||||||
|
int lineLength = 0;
|
||||||
|
|
||||||
|
while(end != BreakIterator.DONE) {
|
||||||
|
String word = input.substring(start,end);
|
||||||
|
lineLength = lineLength + word.length();
|
||||||
|
if (lineLength >= maxLength) {
|
||||||
|
builder.append("\n");
|
||||||
|
lineLength = word.length();
|
||||||
|
}
|
||||||
|
builder.append(word);
|
||||||
|
start = end;
|
||||||
|
end = boundary.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private MessageDialogButton getButton(ButtonType buttonType) {
|
||||||
|
if(ButtonType.OK.equals(buttonType)) {
|
||||||
|
return MessageDialogButton.OK;
|
||||||
|
}
|
||||||
|
if(ButtonType.CANCEL.equals(buttonType)) {
|
||||||
|
return MessageDialogButton.Cancel;
|
||||||
|
}
|
||||||
|
if(ButtonType.YES.equals(buttonType)) {
|
||||||
|
return MessageDialogButton.Yes;
|
||||||
|
}
|
||||||
|
if(ButtonType.NO.equals(buttonType)) {
|
||||||
|
return MessageDialogButton.No;
|
||||||
|
}
|
||||||
|
if(ButtonType.CLOSE.equals(buttonType)) {
|
||||||
|
return MessageDialogButton.Close;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new IllegalArgumentException("Cannot find button for " + buttonType);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<String> requestPassphrase(String walletName, Keystore keystore) {
|
||||||
|
if(Platform.isFxApplicationThread()) {
|
||||||
|
SparrowTerminal.get().getGui().getGUIThread().invokeLater(() -> {
|
||||||
|
Optional<String> optPassphrase = showPassphraseDialog(walletName, keystore);
|
||||||
|
Platform.runLater(() -> Platform.exitNestedEventLoop(passphraseShowing, optPassphrase));
|
||||||
|
});
|
||||||
|
return (Optional<String>)Platform.enterNestedEventLoop(passphraseShowing);
|
||||||
|
} else {
|
||||||
|
return showPassphraseDialog(walletName, keystore);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<String> showPassphraseDialog(String walletName, Keystore keystore) {
|
||||||
|
TextInputDialogBuilder builder = new TextInputDialogBuilder().setTitle("Passphrase for " + walletName);
|
||||||
|
builder.setDescription("Enter the BIP39 passphrase for keystore:\n" + keystore.getLabel());
|
||||||
|
builder.setPasswordInput(true);
|
||||||
|
String passphrase = builder.build().showDialog(SparrowTerminal.get().getGui());
|
||||||
|
return passphrase == null ? Optional.empty() : Optional.of(passphrase);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,139 @@
|
||||||
|
package com.sparrowwallet.sparrow.terminal.preferences;
|
||||||
|
|
||||||
|
import com.googlecode.lanterna.TerminalSize;
|
||||||
|
import com.googlecode.lanterna.gui2.*;
|
||||||
|
import com.googlecode.lanterna.gui2.dialogs.DirectoryDialogBuilder;
|
||||||
|
import com.sparrowwallet.drongo.Network;
|
||||||
|
import com.sparrowwallet.sparrow.io.Config;
|
||||||
|
import com.sparrowwallet.sparrow.io.Server;
|
||||||
|
import com.sparrowwallet.sparrow.net.CoreAuthType;
|
||||||
|
import com.sparrowwallet.sparrow.net.Protocol;
|
||||||
|
import com.sparrowwallet.sparrow.terminal.SparrowTerminal;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class BitcoinCoreDialog extends ServerUrlDialog {
|
||||||
|
private final ComboBox<String> authentication;
|
||||||
|
private final Label dataFolderLabel;
|
||||||
|
private final TextBox dataFolder;
|
||||||
|
private final Button selectDataFolder;
|
||||||
|
private final Label userPassLabel;
|
||||||
|
private final TextBox user;
|
||||||
|
private final TextBox pass;
|
||||||
|
|
||||||
|
public BitcoinCoreDialog() {
|
||||||
|
super("Bitcoin Core");
|
||||||
|
|
||||||
|
setHints(List.of(Hint.CENTERED));
|
||||||
|
Panel mainPanel = new Panel(new GridLayout(3).setHorizontalSpacing(2).setVerticalSpacing(0));
|
||||||
|
|
||||||
|
if(Config.get().getCoreServer() == null) {
|
||||||
|
Config.get().setCoreServer(new Server("http://127.0.0.1:" + Network.get().getDefaultPort()));
|
||||||
|
}
|
||||||
|
addUrlComponents(mainPanel, Config.get().getRecentCoreServers(), Config.get().getCoreServer());
|
||||||
|
addLine(mainPanel);
|
||||||
|
|
||||||
|
mainPanel.addComponent(new Label("Authentication"));
|
||||||
|
authentication = new ComboBox<>("Default", "User/Pass");
|
||||||
|
authentication.setSelectedIndex(Config.get().getCoreAuthType() == CoreAuthType.USERPASS ? 0 : 1);
|
||||||
|
mainPanel.addComponent(authentication);
|
||||||
|
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
|
||||||
|
|
||||||
|
dataFolderLabel = new Label("Data Folder");
|
||||||
|
mainPanel.addComponent(dataFolderLabel);
|
||||||
|
dataFolder = new TextBox(new TerminalSize(30, 1), Config.get().getCoreDataDir() != null ? Config.get().getCoreDataDir().getAbsolutePath() : "");
|
||||||
|
mainPanel.addComponent(dataFolder);
|
||||||
|
selectDataFolder = new Button("Select...");
|
||||||
|
mainPanel.addComponent(selectDataFolder);
|
||||||
|
|
||||||
|
userPassLabel = new Label("User/Pass");
|
||||||
|
mainPanel.addComponent(userPassLabel);
|
||||||
|
user = new TextBox(new TerminalSize(16, 1));
|
||||||
|
mainPanel.addComponent(user);
|
||||||
|
pass = new TextBox(new TerminalSize(16, 1));
|
||||||
|
pass.setMask('*');
|
||||||
|
mainPanel.addComponent(pass);
|
||||||
|
|
||||||
|
if(Config.get().getCoreAuth() != null) {
|
||||||
|
String[] userPass = Config.get().getCoreAuth().split(":");
|
||||||
|
if(userPass.length > 0) {
|
||||||
|
user.setText(userPass[0]);
|
||||||
|
}
|
||||||
|
if(userPass.length > 1) {
|
||||||
|
pass.setText(userPass[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
authentication.addListener((selectedIndex, previousSelection, changedByUserInteraction) -> {
|
||||||
|
dataFolderLabel.setVisible(selectedIndex == 0);
|
||||||
|
dataFolder.setVisible(selectedIndex == 0);
|
||||||
|
selectDataFolder.setVisible(selectedIndex == 0);
|
||||||
|
userPassLabel.setVisible(selectedIndex == 1);
|
||||||
|
user.setVisible(selectedIndex == 1);
|
||||||
|
pass.setVisible(selectedIndex == 1);
|
||||||
|
Config.get().setCoreAuthType(selectedIndex == 0 ? CoreAuthType.COOKIE : CoreAuthType.USERPASS);
|
||||||
|
});
|
||||||
|
authentication.setSelectedIndex(Config.get().getCoreAuthType() == CoreAuthType.USERPASS ? 1 : 0);
|
||||||
|
|
||||||
|
dataFolder.setTextChangeListener((newText, changedByUserInteraction) -> {
|
||||||
|
File dataDir = new File(newText);
|
||||||
|
if(dataDir.exists()) {
|
||||||
|
Config.get().setCoreDataDir(dataDir);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
selectDataFolder.addListener(button -> {
|
||||||
|
DirectoryDialogBuilder builder = new DirectoryDialogBuilder().setTitle("Select Bitcoin Core Data Folder").setActionLabel("Select");
|
||||||
|
builder.setShowHiddenDirectories(true);
|
||||||
|
builder.setSelectedDirectory(Config.get().getCoreDataDir() == null ? new File(System.getProperty("user.home")) : Config.get().getCoreDataDir());
|
||||||
|
File file = builder.build().showDialog(SparrowTerminal.get().getGui());
|
||||||
|
if(file != null) {
|
||||||
|
dataFolder.setText(file.getAbsolutePath());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
user.setTextChangeListener((newText, changedByUserInteraction) -> {
|
||||||
|
setCoreAuth();
|
||||||
|
});
|
||||||
|
pass.setTextChangeListener((newText, changedByUserInteraction) -> {
|
||||||
|
setCoreAuth();
|
||||||
|
});
|
||||||
|
|
||||||
|
addLine(mainPanel);
|
||||||
|
addProxyComponents(mainPanel);
|
||||||
|
|
||||||
|
Panel buttonPanel = new Panel();
|
||||||
|
buttonPanel.setLayoutManager(new GridLayout(2).setHorizontalSpacing(1));
|
||||||
|
buttonPanel.addComponent(new Button("Test", this::onTest));
|
||||||
|
buttonPanel.addComponent(new Button("Done", this::onDone));
|
||||||
|
|
||||||
|
addLine(mainPanel);
|
||||||
|
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
|
||||||
|
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
|
||||||
|
|
||||||
|
buttonPanel.setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.END, GridLayout.Alignment.CENTER,false,false)).addTo(mainPanel);
|
||||||
|
setComponent(mainPanel);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void setServerConfig() {
|
||||||
|
Server currentServer = getCurrentServer();
|
||||||
|
if(currentServer != null) {
|
||||||
|
Config.get().setCoreServer(currentServer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void setServerAlias(Server server) {
|
||||||
|
Config.get().setCoreServerAlias(server);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Protocol getProtocol() {
|
||||||
|
return Protocol.HTTP;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void setProtocol(Protocol protocol) {
|
||||||
|
//empty
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setCoreAuth() {
|
||||||
|
Config.get().setCoreAuth(user.getText() + ":" + pass.getText());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,156 @@
|
||||||
|
package com.sparrowwallet.sparrow.terminal.preferences;
|
||||||
|
|
||||||
|
import com.googlecode.lanterna.TerminalSize;
|
||||||
|
import com.googlecode.lanterna.gui2.*;
|
||||||
|
import com.googlecode.lanterna.gui2.dialogs.DialogWindow;
|
||||||
|
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||||
|
import com.sparrowwallet.sparrow.EventManager;
|
||||||
|
import com.sparrowwallet.sparrow.UnitFormat;
|
||||||
|
import com.sparrowwallet.sparrow.event.BitcoinUnitChangedEvent;
|
||||||
|
import com.sparrowwallet.sparrow.event.FiatCurrencySelectedEvent;
|
||||||
|
import com.sparrowwallet.sparrow.event.UnitFormatChangedEvent;
|
||||||
|
import com.sparrowwallet.sparrow.io.Config;
|
||||||
|
import com.sparrowwallet.sparrow.net.ExchangeSource;
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.util.Currency;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class GeneralDialog extends DialogWindow {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(GeneralDialog.class);
|
||||||
|
|
||||||
|
private final ComboBox<BitcoinUnit> bitcoinUnit;
|
||||||
|
private final ComboBox<String> unitFormat;
|
||||||
|
|
||||||
|
private final ComboBox<Currency> fiatCurrency;
|
||||||
|
private final ComboBox<ExchangeSource> exchangeSource;
|
||||||
|
|
||||||
|
private final ComboBox.Listener fiatCurrencyListener = new ComboBox.Listener() {
|
||||||
|
@Override
|
||||||
|
public void onSelectionChanged(int selectedIndex, int previousSelection, boolean changedByUserInteraction) {
|
||||||
|
Currency newValue = fiatCurrency.getSelectedItem();
|
||||||
|
if(newValue != null) {
|
||||||
|
Config.get().setFiatCurrency(newValue);
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
EventManager.get().post(new FiatCurrencySelectedEvent(exchangeSource.getSelectedItem(), newValue));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public GeneralDialog() {
|
||||||
|
super("General Preferences");
|
||||||
|
|
||||||
|
setHints(List.of(Hint.CENTERED));
|
||||||
|
|
||||||
|
Panel mainPanel = new Panel();
|
||||||
|
mainPanel.setLayoutManager(new GridLayout(2).setHorizontalSpacing(5));
|
||||||
|
|
||||||
|
mainPanel.addComponent(new Label("Bitcoin Unit"));
|
||||||
|
bitcoinUnit = new ComboBox<>(BitcoinUnit.values());
|
||||||
|
mainPanel.addComponent(bitcoinUnit);
|
||||||
|
|
||||||
|
mainPanel.addComponent(new Label("Unit Format"));
|
||||||
|
unitFormat = new ComboBox<>("1,234.56", "1.235,56");
|
||||||
|
mainPanel.addComponent(unitFormat);
|
||||||
|
|
||||||
|
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
|
||||||
|
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
|
||||||
|
|
||||||
|
mainPanel.addComponent(new Label("Currency"));
|
||||||
|
fiatCurrency = new ComboBox<>();
|
||||||
|
mainPanel.addComponent(fiatCurrency);
|
||||||
|
|
||||||
|
mainPanel.addComponent(new Label("Exchange rate source"));
|
||||||
|
exchangeSource = new ComboBox<>(ExchangeSource.values());
|
||||||
|
mainPanel.addComponent(exchangeSource);
|
||||||
|
|
||||||
|
bitcoinUnit.setSelectedItem(Config.get().getBitcoinUnit() == null ? BitcoinUnit.AUTO : Config.get().getBitcoinUnit());
|
||||||
|
unitFormat.setSelectedIndex(Config.get().getUnitFormat() == UnitFormat.COMMA ? 1 : 0);
|
||||||
|
|
||||||
|
if(Config.get().getExchangeSource() == null) {
|
||||||
|
Config.get().setExchangeSource(ExchangeSource.COINGECKO);
|
||||||
|
}
|
||||||
|
exchangeSource.setSelectedItem(Config.get().getExchangeSource());
|
||||||
|
|
||||||
|
bitcoinUnit.addListener((int selectedIndex, int previousSelection, boolean changedByUserInteraction) -> {
|
||||||
|
BitcoinUnit newValue = bitcoinUnit.getSelectedItem();
|
||||||
|
Config.get().setBitcoinUnit(newValue);
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
EventManager.get().post(new BitcoinUnitChangedEvent(newValue));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
unitFormat.addListener((int selectedIndex, int previousSelection, boolean changedByUserInteraction) -> {
|
||||||
|
UnitFormat format = selectedIndex == 1 ? UnitFormat.COMMA : UnitFormat.DOT;
|
||||||
|
Config.get().setUnitFormat(format);
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
EventManager.get().post(new UnitFormatChangedEvent(format));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
exchangeSource.addListener((int selectedIndex, int previousSelection, boolean changedByUserInteraction) -> {
|
||||||
|
ExchangeSource source = exchangeSource.getSelectedItem();
|
||||||
|
Config.get().setExchangeSource(source);
|
||||||
|
updateCurrencies(source);
|
||||||
|
});
|
||||||
|
|
||||||
|
updateCurrencies(exchangeSource.getSelectedItem());
|
||||||
|
|
||||||
|
Panel buttonPanel = new Panel();
|
||||||
|
buttonPanel.setLayoutManager(new GridLayout(2).setHorizontalSpacing(1));
|
||||||
|
buttonPanel.addComponent(new Button("Done", this::onDone).setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.CENTER, GridLayout.Alignment.CENTER, true, false)));
|
||||||
|
|
||||||
|
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
|
||||||
|
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
|
||||||
|
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
|
||||||
|
|
||||||
|
buttonPanel.setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.END, GridLayout.Alignment.CENTER,false,false)).addTo(mainPanel);
|
||||||
|
setComponent(mainPanel);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onDone() {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateCurrencies(ExchangeSource exchangeSource) {
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
ExchangeSource.CurrenciesService currenciesService = new ExchangeSource.CurrenciesService(exchangeSource);
|
||||||
|
currenciesService.setOnSucceeded(event -> {
|
||||||
|
updateCurrencies(currenciesService.getValue());
|
||||||
|
});
|
||||||
|
currenciesService.setOnFailed(event -> {
|
||||||
|
log.error("Error retrieving currencies", event.getSource().getException());
|
||||||
|
});
|
||||||
|
currenciesService.start();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateCurrencies(List<Currency> currencies) {
|
||||||
|
fiatCurrency.removeListener(fiatCurrencyListener);
|
||||||
|
|
||||||
|
fiatCurrency.clearItems();
|
||||||
|
currencies.forEach(fiatCurrency::addItem);
|
||||||
|
|
||||||
|
Currency configCurrency = Config.get().getFiatCurrency();
|
||||||
|
if(configCurrency != null && currencies.contains(configCurrency)) {
|
||||||
|
fiatCurrency.setVisible(true);
|
||||||
|
fiatCurrency.setSelectedItem(configCurrency);
|
||||||
|
} else if(!currencies.isEmpty()) {
|
||||||
|
fiatCurrency.setVisible(true);
|
||||||
|
fiatCurrency.setSelectedIndex(0);
|
||||||
|
Config.get().setFiatCurrency(fiatCurrency.getSelectedItem());
|
||||||
|
} else {
|
||||||
|
fiatCurrency.setVisible(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Always fire event regardless of previous selection to update rates
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
EventManager.get().post(new FiatCurrencySelectedEvent(exchangeSource.getSelectedItem(), fiatCurrency.getSelectedItem()));
|
||||||
|
});
|
||||||
|
|
||||||
|
fiatCurrency.addListener(fiatCurrencyListener);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,104 @@
|
||||||
|
package com.sparrowwallet.sparrow.terminal.preferences;
|
||||||
|
|
||||||
|
import com.googlecode.lanterna.TerminalSize;
|
||||||
|
import com.googlecode.lanterna.gui2.*;
|
||||||
|
import com.googlecode.lanterna.gui2.dialogs.FileDialogBuilder;
|
||||||
|
import com.sparrowwallet.sparrow.io.Config;
|
||||||
|
import com.sparrowwallet.sparrow.io.Server;
|
||||||
|
import com.sparrowwallet.sparrow.net.Protocol;
|
||||||
|
import com.sparrowwallet.sparrow.terminal.SparrowTerminal;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class PrivateElectrumDialog extends ServerUrlDialog {
|
||||||
|
private final ComboBox<String> useSsl;
|
||||||
|
private final TextBox certificate;
|
||||||
|
private final Button selectCertificate;
|
||||||
|
|
||||||
|
public PrivateElectrumDialog() {
|
||||||
|
super("Private Electrum");
|
||||||
|
|
||||||
|
setHints(List.of(Hint.CENTERED));
|
||||||
|
Panel mainPanel = new Panel(new GridLayout(3).setHorizontalSpacing(2).setVerticalSpacing(0));
|
||||||
|
|
||||||
|
if(Config.get().getElectrumServer() == null) {
|
||||||
|
Config.get().setElectrumServer(new Server("tcp://127.0.0.1:50001"));
|
||||||
|
}
|
||||||
|
addUrlComponents(mainPanel, Config.get().getRecentElectrumServers(), Config.get().getElectrumServer());
|
||||||
|
addLine(mainPanel);
|
||||||
|
|
||||||
|
mainPanel.addComponent(new Label("Use SSL?"));
|
||||||
|
useSsl = new ComboBox<>("Yes", "No");
|
||||||
|
useSsl.setSelectedIndex(1);
|
||||||
|
mainPanel.addComponent(useSsl);
|
||||||
|
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
|
||||||
|
|
||||||
|
mainPanel.addComponent(new Label("Certificate"));
|
||||||
|
certificate = new TextBox(new TerminalSize(30, 1), Config.get().getElectrumServerCert() != null ? Config.get().getElectrumServerCert().getAbsolutePath() : "");
|
||||||
|
mainPanel.addComponent(certificate);
|
||||||
|
selectCertificate = new Button("Select...");
|
||||||
|
mainPanel.addComponent(selectCertificate);
|
||||||
|
|
||||||
|
Server configuredServer = Config.get().getElectrumServer();
|
||||||
|
if(configuredServer != null) {
|
||||||
|
Protocol protocol = configuredServer.getProtocol();
|
||||||
|
boolean ssl = protocol.equals(Protocol.SSL);
|
||||||
|
useSsl.setSelectedIndex(ssl ? 0 : 1);
|
||||||
|
certificate.setEnabled(ssl);
|
||||||
|
selectCertificate.setEnabled(ssl);
|
||||||
|
}
|
||||||
|
|
||||||
|
useSsl.addListener((selectedIndex, previousSelection, changedByUserInteraction) -> {
|
||||||
|
setServerConfig();
|
||||||
|
certificate.setEnabled(selectedIndex == 0);
|
||||||
|
selectCertificate.setEnabled(selectedIndex == 0);
|
||||||
|
});
|
||||||
|
certificate.setTextChangeListener((newText, changedByUserInteraction) -> {
|
||||||
|
File crtFile = getCertificate(newText);
|
||||||
|
Config.get().setElectrumServerCert(crtFile);
|
||||||
|
});
|
||||||
|
selectCertificate.addListener(button -> {
|
||||||
|
FileDialogBuilder builder = new FileDialogBuilder().setTitle("Select SSL Certificate").setActionLabel("Select");
|
||||||
|
builder.setShowHiddenDirectories(true);
|
||||||
|
File file = builder.build().showDialog(SparrowTerminal.get().getGui());
|
||||||
|
if(file != null && getCertificate(file.getAbsolutePath()) != null) {
|
||||||
|
certificate.setText(file.getAbsolutePath());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
addLine(mainPanel);
|
||||||
|
addProxyComponents(mainPanel);
|
||||||
|
|
||||||
|
Panel buttonPanel = new Panel();
|
||||||
|
buttonPanel.setLayoutManager(new GridLayout(2).setHorizontalSpacing(1));
|
||||||
|
buttonPanel.addComponent(new Button("Test", this::onTest));
|
||||||
|
buttonPanel.addComponent(new Button("Done", this::onDone));
|
||||||
|
|
||||||
|
addLine(mainPanel);
|
||||||
|
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
|
||||||
|
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
|
||||||
|
|
||||||
|
buttonPanel.setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.END, GridLayout.Alignment.CENTER,false,false)).addTo(mainPanel);
|
||||||
|
setComponent(mainPanel);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void setServerConfig() {
|
||||||
|
Server currentServer = getCurrentServer();
|
||||||
|
if(currentServer != null) {
|
||||||
|
Config.get().setElectrumServer(currentServer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void setServerAlias(Server server) {
|
||||||
|
Config.get().setElectrumServerAlias(server);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Protocol getProtocol() {
|
||||||
|
return (useSsl.getSelectedIndex() == 0 ? Protocol.SSL : Protocol.TCP);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void setProtocol(Protocol protocol) {
|
||||||
|
useSsl.setSelectedIndex(protocol == Protocol.SSL ? 0 : 1);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
package com.sparrowwallet.sparrow.terminal.preferences;
|
||||||
|
|
||||||
|
import com.googlecode.lanterna.TerminalSize;
|
||||||
|
import com.googlecode.lanterna.gui2.*;
|
||||||
|
import com.sparrowwallet.sparrow.io.Config;
|
||||||
|
import com.sparrowwallet.sparrow.net.PublicElectrumServer;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class PublicElectrumDialog extends ServerProxyDialog {
|
||||||
|
private final ComboBox<PublicElectrumServer> url;
|
||||||
|
|
||||||
|
public PublicElectrumDialog() {
|
||||||
|
super("Public Electrum");
|
||||||
|
|
||||||
|
setHints(List.of(Hint.CENTERED));
|
||||||
|
|
||||||
|
Panel mainPanel = new Panel(new GridLayout(3).setHorizontalSpacing(2).setVerticalSpacing(0));
|
||||||
|
mainPanel.addComponent(new Label("Warning!"));
|
||||||
|
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
|
||||||
|
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
|
||||||
|
mainPanel.addComponent(new Label("Using a public server means it can see your transactions"),
|
||||||
|
GridLayout.createLayoutData(GridLayout.Alignment.BEGINNING, GridLayout.Alignment.CENTER,true,false, 3, 1));
|
||||||
|
|
||||||
|
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
|
||||||
|
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
|
||||||
|
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
|
||||||
|
|
||||||
|
mainPanel.addComponent(new Label("URL"));
|
||||||
|
url = new ComboBox<>();
|
||||||
|
for(PublicElectrumServer server : PublicElectrumServer.getServers()) {
|
||||||
|
url.addItem(server);
|
||||||
|
}
|
||||||
|
if(Config.get().getPublicElectrumServer() == null) {
|
||||||
|
Config.get().changePublicServer();
|
||||||
|
}
|
||||||
|
url.setSelectedItem(PublicElectrumServer.fromServer(Config.get().getPublicElectrumServer()));
|
||||||
|
url.addListener((selectedIndex, previousSelection, changedByUserInteraction) -> {
|
||||||
|
if(selectedIndex != previousSelection) {
|
||||||
|
Config.get().setPublicElectrumServer(PublicElectrumServer.values()[selectedIndex].getServer());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
mainPanel.addComponent(url);
|
||||||
|
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
|
||||||
|
|
||||||
|
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
|
||||||
|
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
|
||||||
|
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
|
||||||
|
|
||||||
|
addProxyComponents(mainPanel);
|
||||||
|
|
||||||
|
Panel buttonPanel = new Panel();
|
||||||
|
buttonPanel.setLayoutManager(new GridLayout(2).setHorizontalSpacing(1));
|
||||||
|
buttonPanel.addComponent(new Button("Test", this::onTest));
|
||||||
|
buttonPanel.addComponent(new Button("Done", this::onDone));
|
||||||
|
|
||||||
|
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
|
||||||
|
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
|
||||||
|
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
|
||||||
|
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
|
||||||
|
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
|
||||||
|
|
||||||
|
buttonPanel.setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.END, GridLayout.Alignment.CENTER,false,false)).addTo(mainPanel);
|
||||||
|
setComponent(mainPanel);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,129 @@
|
||||||
|
package com.sparrowwallet.sparrow.terminal.preferences;
|
||||||
|
|
||||||
|
import com.google.common.net.HostAndPort;
|
||||||
|
import com.googlecode.lanterna.TerminalSize;
|
||||||
|
import com.googlecode.lanterna.gui2.*;
|
||||||
|
import com.googlecode.lanterna.gui2.dialogs.DialogWindow;
|
||||||
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
|
import com.sparrowwallet.sparrow.EventManager;
|
||||||
|
import com.sparrowwallet.sparrow.Mode;
|
||||||
|
import com.sparrowwallet.sparrow.event.RequestConnectEvent;
|
||||||
|
import com.sparrowwallet.sparrow.io.Config;
|
||||||
|
import com.sparrowwallet.sparrow.terminal.SparrowTerminal;
|
||||||
|
import javafx.application.Platform;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.security.cert.CertificateFactory;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
public abstract class ServerProxyDialog extends DialogWindow {
|
||||||
|
private ComboBox<String> useProxy;
|
||||||
|
private TextBox proxyHost;
|
||||||
|
private TextBox proxyPort;
|
||||||
|
|
||||||
|
public ServerProxyDialog(String title) {
|
||||||
|
super(title);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void onDone() {
|
||||||
|
close();
|
||||||
|
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
if(Config.get().getMode() == Mode.ONLINE && !(AppServices.isConnecting() || AppServices.isConnected())) {
|
||||||
|
EventManager.get().post(new RequestConnectEvent());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void onTest() {
|
||||||
|
close();
|
||||||
|
|
||||||
|
ServerTestDialog serverTestDialog = new ServerTestDialog();
|
||||||
|
serverTestDialog.showDialog(SparrowTerminal.get().getGui());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void addProxyComponents(Panel mainPanel) {
|
||||||
|
mainPanel.addComponent(new Label("Use Proxy?"));
|
||||||
|
useProxy = new ComboBox<>("Yes", "No");
|
||||||
|
useProxy.setSelectedIndex(Config.get().isUseProxy() ? 0 : 1);
|
||||||
|
useProxy.addListener((selectedIndex, previousSelection, changedByUserInteraction) -> {
|
||||||
|
Config.get().setUseProxy(selectedIndex == 0);
|
||||||
|
});
|
||||||
|
mainPanel.addComponent(useProxy);
|
||||||
|
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
|
||||||
|
|
||||||
|
mainPanel.addComponent(new Label("Proxy URL"));
|
||||||
|
proxyHost = new TextBox(new TerminalSize(30,1)).setValidationPattern(Pattern.compile("[a-zA-Z0-9.]+"));
|
||||||
|
mainPanel.addComponent(proxyHost);
|
||||||
|
|
||||||
|
proxyPort = new TextBox(new TerminalSize(6,1)).setValidationPattern(Pattern.compile("[0-9]*"));
|
||||||
|
mainPanel.addComponent(proxyPort);
|
||||||
|
|
||||||
|
String proxyServer = Config.get().getProxyServer();
|
||||||
|
if(proxyServer != null) {
|
||||||
|
HostAndPort server = HostAndPort.fromString(proxyServer);
|
||||||
|
proxyHost.setText(server.getHost());
|
||||||
|
if(server.hasPort()) {
|
||||||
|
proxyPort.setText(Integer.toString(server.getPort()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
proxyHost.setTextChangeListener((newText, changedByUserInteraction) -> {
|
||||||
|
setProxyConfig();
|
||||||
|
});
|
||||||
|
proxyPort.setTextChangeListener((newText, changedByUserInteraction) -> {
|
||||||
|
setProxyConfig();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setProxyConfig() {
|
||||||
|
String hostAsString = getHost(proxyHost.getText());
|
||||||
|
Integer portAsInteger = getPort(proxyPort.getText());
|
||||||
|
if(hostAsString != null && portAsInteger != null && isValidPort(portAsInteger)) {
|
||||||
|
Config.get().setProxyServer(HostAndPort.fromParts(hostAsString, portAsInteger).toString());
|
||||||
|
} else if(hostAsString != null) {
|
||||||
|
Config.get().setProxyServer(HostAndPort.fromHost(hostAsString).toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String getHost(String text) {
|
||||||
|
try {
|
||||||
|
return HostAndPort.fromHost(text).getHost();
|
||||||
|
} catch(IllegalArgumentException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Integer getPort(String text) {
|
||||||
|
try {
|
||||||
|
return Integer.parseInt(text);
|
||||||
|
} catch(NumberFormatException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static boolean isValidPort(int port) {
|
||||||
|
return port >= 0 && port <= 65535;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected File getCertificate(String crtFileLocation) {
|
||||||
|
try {
|
||||||
|
File crtFile = new File(crtFileLocation);
|
||||||
|
if(!crtFile.exists()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
CertificateFactory.getInstance("X.509").generateCertificate(new FileInputStream(crtFile));
|
||||||
|
return crtFile;
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void addLine(Panel mainPanel) {
|
||||||
|
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
|
||||||
|
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
|
||||||
|
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,74 @@
|
||||||
|
package com.sparrowwallet.sparrow.terminal.preferences;
|
||||||
|
|
||||||
|
import com.googlecode.lanterna.TerminalSize;
|
||||||
|
import com.googlecode.lanterna.gui2.*;
|
||||||
|
import com.googlecode.lanterna.gui2.dialogs.DialogWindow;
|
||||||
|
import com.sparrowwallet.sparrow.EventManager;
|
||||||
|
import com.sparrowwallet.sparrow.Mode;
|
||||||
|
import com.sparrowwallet.sparrow.event.RequestConnectEvent;
|
||||||
|
import com.sparrowwallet.sparrow.event.RequestDisconnectEvent;
|
||||||
|
import com.sparrowwallet.sparrow.io.Config;
|
||||||
|
import com.sparrowwallet.sparrow.terminal.SparrowTerminal;
|
||||||
|
import javafx.application.Platform;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class ServerStatusDialog extends DialogWindow {
|
||||||
|
private final ComboBox<String> connect;
|
||||||
|
|
||||||
|
public ServerStatusDialog() {
|
||||||
|
super("Server Preferences");
|
||||||
|
|
||||||
|
setHints(List.of(Hint.CENTERED));
|
||||||
|
|
||||||
|
Panel mainPanel = new Panel();
|
||||||
|
mainPanel.setLayoutManager(new GridLayout(2).setHorizontalSpacing(5));
|
||||||
|
|
||||||
|
mainPanel.addComponent(new Label("Connect?"));
|
||||||
|
connect = new ComboBox<>();
|
||||||
|
connect.addItem("Yes");
|
||||||
|
connect.addItem("No");
|
||||||
|
connect.setSelectedIndex(Config.get().getMode() == Mode.ONLINE ? 0 : 1);
|
||||||
|
connect.addListener((selectedIndex, previousSelection, changedByUserInteraction) -> {
|
||||||
|
if(selectedIndex != previousSelection) {
|
||||||
|
Config.get().setMode(selectedIndex == 0 ? Mode.ONLINE : Mode.OFFLINE);
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
EventManager.get().post(selectedIndex == 0 ? new RequestConnectEvent() : new RequestDisconnectEvent());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
mainPanel.addComponent(connect);
|
||||||
|
|
||||||
|
mainPanel.addComponent(new Label("Server Type"));
|
||||||
|
mainPanel.addComponent(new Label(Config.get().getServerType().getName()));
|
||||||
|
|
||||||
|
mainPanel.addComponent(new Label("Server"));
|
||||||
|
mainPanel.addComponent(new Label(Config.get().getServer().getDisplayName()));
|
||||||
|
|
||||||
|
Panel buttonPanel = new Panel();
|
||||||
|
buttonPanel.setLayoutManager(new GridLayout(2).setHorizontalSpacing(1));
|
||||||
|
buttonPanel.addComponent(new Button("Edit", this::onEdit).setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.CENTER, GridLayout.Alignment.CENTER, true, false)));
|
||||||
|
buttonPanel.addComponent(new Button("Cancel", this::onCancel));
|
||||||
|
|
||||||
|
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
|
||||||
|
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
|
||||||
|
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
|
||||||
|
|
||||||
|
buttonPanel.setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.END, GridLayout.Alignment.CENTER,false,false)).addTo(mainPanel);
|
||||||
|
setComponent(mainPanel);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onEdit() {
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
EventManager.get().post(new RequestDisconnectEvent());
|
||||||
|
});
|
||||||
|
close();
|
||||||
|
|
||||||
|
ServerTypeDialog serverTypeDialog = new ServerTypeDialog();
|
||||||
|
serverTypeDialog.showDialog(SparrowTerminal.get().getGui());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onCancel() {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,228 @@
|
||||||
|
package com.sparrowwallet.sparrow.terminal.preferences;
|
||||||
|
|
||||||
|
import com.google.common.eventbus.Subscribe;
|
||||||
|
import com.googlecode.lanterna.TerminalSize;
|
||||||
|
import com.googlecode.lanterna.gui2.*;
|
||||||
|
import com.googlecode.lanterna.gui2.dialogs.DialogWindow;
|
||||||
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
|
import com.sparrowwallet.sparrow.EventManager;
|
||||||
|
import com.sparrowwallet.sparrow.Mode;
|
||||||
|
import com.sparrowwallet.sparrow.event.*;
|
||||||
|
import com.sparrowwallet.sparrow.io.Config;
|
||||||
|
import com.sparrowwallet.sparrow.io.Storage;
|
||||||
|
import com.sparrowwallet.sparrow.net.*;
|
||||||
|
import com.sparrowwallet.sparrow.terminal.SparrowTerminal;
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import javafx.scene.control.ButtonType;
|
||||||
|
import javafx.util.Duration;
|
||||||
|
import org.berndpruenster.netlayer.tor.Tor;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.text.DateFormat;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public class ServerTestDialog extends DialogWindow {
|
||||||
|
private final Label testStatus;
|
||||||
|
private final TextBox testResults;
|
||||||
|
|
||||||
|
private TorService torService;
|
||||||
|
private ElectrumServer.ConnectionService connectionService;
|
||||||
|
|
||||||
|
public ServerTestDialog() {
|
||||||
|
super("Server Test");
|
||||||
|
|
||||||
|
setHints(List.of(Hint.CENTERED));
|
||||||
|
|
||||||
|
Panel mainPanel = new Panel(new GridLayout(1));
|
||||||
|
|
||||||
|
this.testStatus = new Label("");
|
||||||
|
mainPanel.addComponent(testStatus);
|
||||||
|
|
||||||
|
this.testResults = new TextBox(new TerminalSize(60, 10));
|
||||||
|
testResults.setReadOnly(true);
|
||||||
|
mainPanel.addComponent(testResults);
|
||||||
|
|
||||||
|
Panel buttonPanel = new Panel();
|
||||||
|
buttonPanel.setLayoutManager(new GridLayout(3).setHorizontalSpacing(1));
|
||||||
|
buttonPanel.addComponent(new Button("Back", this::onBack).setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.CENTER, GridLayout.Alignment.CENTER, false, false)));
|
||||||
|
buttonPanel.addComponent(new Button("Test", this::onTest).setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.CENTER, GridLayout.Alignment.CENTER, true, false)));
|
||||||
|
buttonPanel.addComponent(new Button("Done", this::onDone));
|
||||||
|
|
||||||
|
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
|
||||||
|
|
||||||
|
buttonPanel.setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.END, GridLayout.Alignment.CENTER,false,false)).addTo(mainPanel);
|
||||||
|
setComponent(mainPanel);
|
||||||
|
|
||||||
|
EventManager.get().register(this);
|
||||||
|
|
||||||
|
onTest();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onBack() {
|
||||||
|
close();
|
||||||
|
|
||||||
|
if(Config.get().getServerType() == ServerType.PUBLIC_ELECTRUM_SERVER) {
|
||||||
|
PublicElectrumDialog publicElectrumServer = new PublicElectrumDialog();
|
||||||
|
publicElectrumServer.showDialog(SparrowTerminal.get().getGui());
|
||||||
|
} else if(Config.get().getServerType() == ServerType.BITCOIN_CORE) {
|
||||||
|
BitcoinCoreDialog bitcoinCoreDialog = new BitcoinCoreDialog();
|
||||||
|
bitcoinCoreDialog.showDialog(SparrowTerminal.get().getGui());
|
||||||
|
} else if(Config.get().getServerType() == ServerType.ELECTRUM_SERVER) {
|
||||||
|
PrivateElectrumDialog privateElectrumDialog = new PrivateElectrumDialog();
|
||||||
|
privateElectrumDialog.showDialog(SparrowTerminal.get().getGui());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onTest() {
|
||||||
|
testResults.setText("Connecting " + (Config.get().hasServer() ? "to " + Config.get().getServer().getUrl() : "") + "...");
|
||||||
|
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
if(Config.get().requiresInternalTor() && Tor.getDefault() == null) {
|
||||||
|
startTor();
|
||||||
|
} else {
|
||||||
|
startElectrumConnection();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onDone() {
|
||||||
|
EventManager.get().unregister(this);
|
||||||
|
close();
|
||||||
|
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
if(Config.get().getMode() == Mode.ONLINE && !(AppServices.isConnecting() || AppServices.isConnected())) {
|
||||||
|
EventManager.get().post(new RequestConnectEvent());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
appendText("\nTor running, connecting to " + Config.get().getServer().getUrl() + "...");
|
||||||
|
startElectrumConnection();
|
||||||
|
});
|
||||||
|
torService.setOnFailed(workerStateEvent -> {
|
||||||
|
torService.cancel();
|
||||||
|
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);
|
||||||
|
|
||||||
|
connectionService.setOnSucceeded(successEvent -> {
|
||||||
|
EventManager.get().unregister(connectionService);
|
||||||
|
ConnectionEvent connectionEvent = (ConnectionEvent)connectionService.getValue();
|
||||||
|
showConnectionSuccess(connectionEvent.getServerVersion(), connectionEvent.getServerBanner());
|
||||||
|
Config.get().setMode(Mode.ONLINE);
|
||||||
|
connectionService.cancel();
|
||||||
|
Config.get().addRecentServer();
|
||||||
|
});
|
||||||
|
connectionService.setOnFailed(workerStateEvent -> {
|
||||||
|
EventManager.get().unregister(connectionService);
|
||||||
|
showConnectionFailure(workerStateEvent.getSource().getException());
|
||||||
|
connectionService.cancel();
|
||||||
|
});
|
||||||
|
connectionService.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void appendText(String text) {
|
||||||
|
testResults.setText(testResults.getText() + text);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showConnectionSuccess(List<String> serverVersion, String serverBanner) {
|
||||||
|
testStatus.setText("Success");
|
||||||
|
if(serverVersion != null) {
|
||||||
|
testResults.setText("Connected to " + serverVersion.get(0) + " on protocol version " + serverVersion.get(1));
|
||||||
|
if(ElectrumServer.supportsBatching(serverVersion)) {
|
||||||
|
testResults.setText(testResults.getText() + "\nBatched RPC enabled.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(serverBanner != null) {
|
||||||
|
testResults.setText(testResults.getText() + "\nServer Banner: " + serverBanner);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showConnectionFailure(Throwable exception) {
|
||||||
|
String reason = exception.getCause() != null ? exception.getCause().getMessage() : exception.getMessage();
|
||||||
|
if(exception instanceof TlsServerException && exception.getCause() != null) {
|
||||||
|
TlsServerException tlsServerException = (TlsServerException)exception;
|
||||||
|
if(exception.getCause().getMessage().contains("PKIX path building failed")) {
|
||||||
|
File configCrtFile = Config.get().getElectrumServerCert();
|
||||||
|
File savedCrtFile = Storage.getCertificateFile(tlsServerException.getServer().getHost());
|
||||||
|
if(configCrtFile == null && savedCrtFile != null) {
|
||||||
|
Optional<ButtonType> optButton = AppServices.showErrorDialog("SSL Handshake Failed", "The certificate provided by the server at " + tlsServerException.getServer().getHost() + " appears to have changed." +
|
||||||
|
"\n\nThis may indicate a man-in-the-middle attack!" +
|
||||||
|
"\n\nDo you still want to proceed?", ButtonType.NO, ButtonType.YES);
|
||||||
|
if(optButton.isPresent() && optButton.get() == ButtonType.YES) {
|
||||||
|
if(savedCrtFile.delete()) {
|
||||||
|
Platform.runLater(this::startElectrumConnection);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
AppServices.showErrorDialog("Could not delete certificate", "The certificate file at " + savedCrtFile.getAbsolutePath() + " could not be deleted.\n\nPlease delete this file manually.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reason = tlsServerException.getMessage() + "\n\n" + reason;
|
||||||
|
} else if(exception instanceof ProxyServerException) {
|
||||||
|
reason += ". Check if the proxy server is running.";
|
||||||
|
} else if(exception instanceof TorServerAlreadyBoundException) {
|
||||||
|
reason += "\nIs a Tor proxy already running on port " + TorService.PROXY_PORT + "?";
|
||||||
|
} else if(reason != null && reason.contains("Check if Bitcoin Core is running")) {
|
||||||
|
reason += "\n\nSee https://sparrowwallet.com/docs/connect-node.html";
|
||||||
|
}
|
||||||
|
|
||||||
|
testStatus.setText("Failed");
|
||||||
|
testResults.setText("Could not connect:\n\n" + reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void bwtStatus(BwtStatusEvent event) {
|
||||||
|
if(!(event instanceof BwtSyncStatusEvent)) {
|
||||||
|
appendText("\n" + event.getStatus());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void bwtSyncStatus(BwtSyncStatusEvent event) {
|
||||||
|
if(connectionService != null && connectionService.isRunning() && event.getProgress() < 100) {
|
||||||
|
DateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm");
|
||||||
|
appendText("\nThe connection to the Bitcoin Core node was successful, but it is still syncing and cannot be used yet.");
|
||||||
|
appendText("\nCurrently " + event.getProgress() + "% completed to date " + dateFormat.format(event.getTip()));
|
||||||
|
connectionService.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void torStatus(TorStatusEvent event) {
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
if(torService != null && torService.isRunning()) {
|
||||||
|
appendText("\n" + event.getStatus());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,68 @@
|
||||||
|
package com.sparrowwallet.sparrow.terminal.preferences;
|
||||||
|
|
||||||
|
import com.googlecode.lanterna.TerminalSize;
|
||||||
|
import com.googlecode.lanterna.gui2.*;
|
||||||
|
import com.googlecode.lanterna.gui2.dialogs.DialogWindow;
|
||||||
|
import com.sparrowwallet.sparrow.io.Config;
|
||||||
|
import com.sparrowwallet.sparrow.net.ServerType;
|
||||||
|
import com.sparrowwallet.sparrow.terminal.SparrowTerminal;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class ServerTypeDialog extends DialogWindow {
|
||||||
|
private final RadioBoxList<String> type;
|
||||||
|
|
||||||
|
public ServerTypeDialog() {
|
||||||
|
super("Server Type");
|
||||||
|
|
||||||
|
setHints(List.of(Hint.CENTERED));
|
||||||
|
|
||||||
|
Panel mainPanel = new Panel();
|
||||||
|
mainPanel.setLayoutManager(new GridLayout(2).setHorizontalSpacing(5));
|
||||||
|
|
||||||
|
ServerType[] serverTypes = new ServerType[] { ServerType.PUBLIC_ELECTRUM_SERVER, ServerType.BITCOIN_CORE, ServerType.ELECTRUM_SERVER };
|
||||||
|
|
||||||
|
mainPanel.addComponent(new Label("Connect using"));
|
||||||
|
type = new RadioBoxList<>();
|
||||||
|
for(ServerType serverType : serverTypes) {
|
||||||
|
type.addItem(serverType.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
if(Config.get().getServerType() == null) {
|
||||||
|
Config.get().setServerType(ServerType.PUBLIC_ELECTRUM_SERVER);
|
||||||
|
}
|
||||||
|
type.setCheckedItem(Config.get().getServerType().getName());
|
||||||
|
type.addListener((selectedIndex, previousSelection) -> {
|
||||||
|
if(selectedIndex != previousSelection) {
|
||||||
|
Config.get().setServerType(serverTypes[selectedIndex]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
mainPanel.addComponent(type);
|
||||||
|
|
||||||
|
Panel buttonPanel = new Panel();
|
||||||
|
buttonPanel.setLayoutManager(new GridLayout(1).setHorizontalSpacing(1));
|
||||||
|
buttonPanel.addComponent(new Button("Continue", this::onContinue));
|
||||||
|
|
||||||
|
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
|
||||||
|
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
|
||||||
|
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
|
||||||
|
|
||||||
|
buttonPanel.setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.END, GridLayout.Alignment.CENTER,false,false)).addTo(mainPanel);
|
||||||
|
setComponent(mainPanel);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onContinue() {
|
||||||
|
close();
|
||||||
|
|
||||||
|
if(Config.get().getServerType() == ServerType.PUBLIC_ELECTRUM_SERVER) {
|
||||||
|
PublicElectrumDialog publicElectrumServer = new PublicElectrumDialog();
|
||||||
|
publicElectrumServer.showDialog(SparrowTerminal.get().getGui());
|
||||||
|
} else if(Config.get().getServerType() == ServerType.BITCOIN_CORE) {
|
||||||
|
BitcoinCoreDialog bitcoinCoreDialog = new BitcoinCoreDialog();
|
||||||
|
bitcoinCoreDialog.showDialog(SparrowTerminal.get().getGui());
|
||||||
|
} else if(Config.get().getServerType() == ServerType.ELECTRUM_SERVER) {
|
||||||
|
PrivateElectrumDialog privateElectrumDialog = new PrivateElectrumDialog();
|
||||||
|
privateElectrumDialog.showDialog(SparrowTerminal.get().getGui());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,126 @@
|
||||||
|
package com.sparrowwallet.sparrow.terminal.preferences;
|
||||||
|
|
||||||
|
import com.google.common.net.HostAndPort;
|
||||||
|
import com.googlecode.lanterna.TerminalSize;
|
||||||
|
import com.googlecode.lanterna.gui2.*;
|
||||||
|
import com.sparrowwallet.sparrow.io.Server;
|
||||||
|
import com.sparrowwallet.sparrow.net.Protocol;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
public abstract class ServerUrlDialog extends ServerProxyDialog {
|
||||||
|
private ComboBox<ServerItem> host;
|
||||||
|
private TextBox port;
|
||||||
|
private TextBox alias;
|
||||||
|
|
||||||
|
public ServerUrlDialog(String title) {
|
||||||
|
super(title);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void addUrlComponents(Panel mainPanel, List<Server> recentServers, Server configuredServer) {
|
||||||
|
mainPanel.addComponent(new Label("URL"));
|
||||||
|
host = new ComboBox<>();
|
||||||
|
host.setPreferredSize(new TerminalSize(30,1));
|
||||||
|
host.setReadOnly(false);
|
||||||
|
mainPanel.addComponent(host, GridLayout.createLayoutData(GridLayout.Alignment.BEGINNING, GridLayout.Alignment.CENTER, true, false));
|
||||||
|
port = new TextBox(new TerminalSize(6,1)).setValidationPattern(Pattern.compile("[0-9]*"));
|
||||||
|
mainPanel.addComponent(port);
|
||||||
|
|
||||||
|
mainPanel.addComponent(new Label("Alias (optional)"));
|
||||||
|
alias = new TextBox(new TerminalSize(30,1));
|
||||||
|
mainPanel.addComponent(alias);
|
||||||
|
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
|
||||||
|
|
||||||
|
if(configuredServer != null) {
|
||||||
|
HostAndPort hostAndPort = configuredServer.getHostAndPort();
|
||||||
|
recentServers.stream().map(ServerItem::new).forEach(host::addItem);
|
||||||
|
host.setSelectedItem(new ServerItem(configuredServer));
|
||||||
|
if(host.getItemCount() == 0) {
|
||||||
|
host.addItem(new ServerItem(configuredServer));
|
||||||
|
}
|
||||||
|
if(hostAndPort.hasPort()) {
|
||||||
|
port.setText(Integer.toString(hostAndPort.getPort()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if(configuredServer.getAlias() != null) {
|
||||||
|
alias.setText(configuredServer.getAlias());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
host.addListener((selectedIndex, previousSelection, changedByUserInteraction) -> {
|
||||||
|
Optional<Server> optServer = recentServers.stream().filter(server -> server.equals(host.getSelectedItem().getServer())).findFirst();
|
||||||
|
if(optServer.isPresent()) {
|
||||||
|
Server server = optServer.get();
|
||||||
|
port.setText(server.getHostAndPort().hasPort() ? Integer.toString(server.getHostAndPort().getPort()) : "");
|
||||||
|
alias.setText(server.getAlias() == null ? "" : server.getAlias());
|
||||||
|
setProtocol(server.getProtocol());
|
||||||
|
}
|
||||||
|
setServerConfig();
|
||||||
|
});
|
||||||
|
port.setTextChangeListener((newText, changedByUserInteraction) -> {
|
||||||
|
setServerConfig();
|
||||||
|
});
|
||||||
|
alias.setTextChangeListener((newText, changedByUserInteraction) -> {
|
||||||
|
Server currentServer = getCurrentServer();
|
||||||
|
if(currentServer != null && host.getSelectedItem() != null && currentServer.equals(host.getSelectedItem().getServer())) {
|
||||||
|
setServerAlias(currentServer);
|
||||||
|
}
|
||||||
|
setServerConfig();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDone() {
|
||||||
|
setServerConfig();
|
||||||
|
super.onDone();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onTest() {
|
||||||
|
setServerConfig();
|
||||||
|
super.onTest();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract void setServerConfig();
|
||||||
|
|
||||||
|
protected abstract void setServerAlias(Server server);
|
||||||
|
|
||||||
|
protected abstract Protocol getProtocol();
|
||||||
|
|
||||||
|
protected abstract void setProtocol(Protocol protocol);
|
||||||
|
|
||||||
|
protected Server getCurrentServer() {
|
||||||
|
String hostAsString = getHost(host.getText());
|
||||||
|
Integer portAsInteger = getPort(port.getText());
|
||||||
|
if(hostAsString != null && portAsInteger != null && isValidPort(portAsInteger)) {
|
||||||
|
return new Server(getProtocol().toUrlString(hostAsString, portAsInteger), getAlias());
|
||||||
|
} else if(hostAsString != null) {
|
||||||
|
return new Server(getProtocol().toUrlString(hostAsString), getAlias());
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getAlias() {
|
||||||
|
return alias.getText().isEmpty() ? null : alias.getText();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static class ServerItem {
|
||||||
|
private final Server server;
|
||||||
|
|
||||||
|
public ServerItem(Server server) {
|
||||||
|
this.server = server;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Server getServer() {
|
||||||
|
return server;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return server.getHost();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,116 @@
|
||||||
|
package com.sparrowwallet.sparrow.terminal.wallet;
|
||||||
|
|
||||||
|
import com.google.common.eventbus.Subscribe;
|
||||||
|
import com.googlecode.lanterna.TerminalSize;
|
||||||
|
import com.googlecode.lanterna.gui2.*;
|
||||||
|
import com.googlecode.lanterna.gui2.table.Table;
|
||||||
|
import com.googlecode.lanterna.gui2.table.TableModel;
|
||||||
|
import com.sparrowwallet.drongo.KeyPurpose;
|
||||||
|
import com.sparrowwallet.sparrow.event.UnitFormatChangedEvent;
|
||||||
|
import com.sparrowwallet.sparrow.event.WalletHistoryChangedEvent;
|
||||||
|
import com.sparrowwallet.sparrow.event.WalletNodesChangedEvent;
|
||||||
|
import com.sparrowwallet.sparrow.terminal.SparrowTerminal;
|
||||||
|
import com.sparrowwallet.sparrow.terminal.wallet.table.AddressTableCell;
|
||||||
|
import com.sparrowwallet.sparrow.terminal.wallet.table.CoinTableCell;
|
||||||
|
import com.sparrowwallet.sparrow.terminal.wallet.table.TableCell;
|
||||||
|
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||||
|
import com.sparrowwallet.sparrow.wallet.Function;
|
||||||
|
import com.sparrowwallet.sparrow.wallet.NodeEntry;
|
||||||
|
import com.sparrowwallet.sparrow.wallet.WalletForm;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class AddressesDialog extends WalletDialog {
|
||||||
|
private final Table<TableCell> receiveTable;
|
||||||
|
private final Table<TableCell> changeTable;
|
||||||
|
|
||||||
|
public AddressesDialog(WalletForm walletForm) {
|
||||||
|
super(walletForm.getWallet().getFullDisplayName() + " Addresses", walletForm);
|
||||||
|
|
||||||
|
setHints(List.of(Hint.CENTERED, Hint.EXPANDED));
|
||||||
|
|
||||||
|
String[] tableColumns = getTableColumns();
|
||||||
|
receiveTable = new Table<>(tableColumns);
|
||||||
|
receiveTable.setTableCellRenderer(new EntryTableCellRenderer());
|
||||||
|
|
||||||
|
changeTable = new Table<>(tableColumns);
|
||||||
|
changeTable.setTableCellRenderer(new EntryTableCellRenderer());
|
||||||
|
|
||||||
|
updateAddresses();
|
||||||
|
|
||||||
|
Panel buttonPanel = new Panel(new GridLayout(4).setHorizontalSpacing(2).setVerticalSpacing(0));
|
||||||
|
buttonPanel.addComponent(new EmptySpace(new TerminalSize(15, 1)));
|
||||||
|
buttonPanel.addComponent(new EmptySpace(new TerminalSize(15, 1)));
|
||||||
|
buttonPanel.addComponent(new Button("Back", () -> onBack(Function.ADDRESSES)));
|
||||||
|
buttonPanel.addComponent(new Button("Refresh", this::onRefresh));
|
||||||
|
|
||||||
|
Panel mainPanel = new Panel();
|
||||||
|
mainPanel.setLayoutManager(new LinearLayout(Direction.VERTICAL).setSpacing(1));
|
||||||
|
mainPanel.addComponent(receiveTable.withBorder(new EmptyBorder("Receive")));
|
||||||
|
mainPanel.addComponent(changeTable.withBorder(new EmptyBorder("Change")));
|
||||||
|
mainPanel.addComponent(buttonPanel);
|
||||||
|
|
||||||
|
setComponent(mainPanel);
|
||||||
|
|
||||||
|
configureTable(receiveTable, KeyPurpose.RECEIVE);
|
||||||
|
configureTable(changeTable, KeyPurpose.CHANGE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void configureTable(Table<TableCell> table, KeyPurpose keyPurpose) {
|
||||||
|
table.setSelectAction(() -> {
|
||||||
|
NodeEntry nodeEntry = (NodeEntry)getWalletForm().getNodeEntry(keyPurpose).getChildren().get(table.getSelectedRow());
|
||||||
|
close();
|
||||||
|
WalletData walletData = SparrowTerminal.get().getWalletData().get(getWalletForm().getWallet());
|
||||||
|
ReceiveDialog receiveDialog = walletData.getReceiveDialog();
|
||||||
|
receiveDialog.setNodeEntry(nodeEntry);
|
||||||
|
receiveDialog.showDialog(SparrowTerminal.get().getGui());
|
||||||
|
});
|
||||||
|
|
||||||
|
Integer highestUsedReceiveIndex = getWalletForm().getNodeEntry(keyPurpose).getNode().getHighestUsedIndex();
|
||||||
|
if(highestUsedReceiveIndex != null) {
|
||||||
|
table.setSelectedRow(highestUsedReceiveIndex + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String[] getTableColumns() {
|
||||||
|
String address = getWalletForm().getNodeEntry(KeyPurpose.RECEIVE).getAddress().toString();
|
||||||
|
return new String[] {centerPad("Address", address.length()), centerPad("Value", CoinTableCell.TRANSACTION_WIDTH)};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateAddressesLater() {
|
||||||
|
SparrowTerminal.get().getGui().getGUIThread().invokeLater(this::updateAddresses);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateAddresses() {
|
||||||
|
receiveTable.setTableModel(getTableModel(getWalletForm().getNodeEntry(KeyPurpose.RECEIVE)));
|
||||||
|
changeTable.setTableModel(getTableModel(getWalletForm().getNodeEntry(KeyPurpose.CHANGE)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private TableModel<TableCell> getTableModel(NodeEntry nodeEntry) {
|
||||||
|
TableModel<TableCell> tableModel = new TableModel<>(getTableColumns());
|
||||||
|
for(Entry addressEntry : nodeEntry.getChildren()) {
|
||||||
|
tableModel.addRow(new AddressTableCell(addressEntry), new CoinTableCell(addressEntry, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
return tableModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void walletNodesChanged(WalletNodesChangedEvent event) {
|
||||||
|
if(event.getWallet().equals(getWalletForm().getWallet())) {
|
||||||
|
updateAddressesLater();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void walletHistoryChanged(WalletHistoryChangedEvent event) {
|
||||||
|
if(event.getWallet().equals(getWalletForm().getWallet())) {
|
||||||
|
updateAddressesLater();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void unitFormatChanged(UnitFormatChangedEvent event) {
|
||||||
|
updateAddressesLater();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,118 @@
|
||||||
|
package com.sparrowwallet.sparrow.terminal.wallet;
|
||||||
|
|
||||||
|
import com.googlecode.lanterna.TerminalPosition;
|
||||||
|
import com.googlecode.lanterna.TerminalSize;
|
||||||
|
import com.googlecode.lanterna.TerminalTextUtils;
|
||||||
|
import com.googlecode.lanterna.graphics.ThemeDefinition;
|
||||||
|
import com.googlecode.lanterna.gui2.*;
|
||||||
|
|
||||||
|
public class EmptyBorder extends AbstractBorder {
|
||||||
|
private final String title;
|
||||||
|
|
||||||
|
protected EmptyBorder(String title) {
|
||||||
|
if (title == null) {
|
||||||
|
throw new IllegalArgumentException("Cannot create a border with null title");
|
||||||
|
}
|
||||||
|
this.title = title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTitle() {
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return getClass().getSimpleName() + "{" + title + "}";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected BorderRenderer createDefaultRenderer() {
|
||||||
|
return new EmptyBorderRenderer();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class EmptyBorderRenderer implements Border.BorderRenderer {
|
||||||
|
@Override
|
||||||
|
public TerminalSize getPreferredSize(Border component) {
|
||||||
|
EmptyBorder border = (EmptyBorder)component;
|
||||||
|
Component wrappedComponent = border.getComponent();
|
||||||
|
TerminalSize preferredSize;
|
||||||
|
if (wrappedComponent == null) {
|
||||||
|
preferredSize = TerminalSize.ZERO;
|
||||||
|
} else {
|
||||||
|
preferredSize = wrappedComponent.getPreferredSize();
|
||||||
|
}
|
||||||
|
preferredSize = preferredSize.withRelativeColumns(2).withRelativeRows(2);
|
||||||
|
String borderTitle = border.getTitle();
|
||||||
|
return preferredSize.max(new TerminalSize((borderTitle.isEmpty() ? 2 : TerminalTextUtils.getColumnWidth(borderTitle) + 4), 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TerminalPosition getWrappedComponentTopLeftOffset() {
|
||||||
|
return TerminalPosition.OFFSET_1x1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TerminalSize getWrappedComponentSize(TerminalSize borderSize) {
|
||||||
|
return borderSize
|
||||||
|
.withRelativeColumns(-Math.min(2, borderSize.getColumns()))
|
||||||
|
.withRelativeRows(-Math.min(2, borderSize.getRows()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void drawComponent(TextGUIGraphics graphics, Border component) {
|
||||||
|
EmptyBorder border = (EmptyBorder)component;
|
||||||
|
Component wrappedComponent = border.getComponent();
|
||||||
|
if(wrappedComponent == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
TerminalSize drawableArea = graphics.getSize();
|
||||||
|
|
||||||
|
char horizontalLine = ' ';
|
||||||
|
char verticalLine = ' ';
|
||||||
|
char bottomLeftCorner = ' ';
|
||||||
|
char topLeftCorner = ' ';
|
||||||
|
char bottomRightCorner = ' ';
|
||||||
|
char topRightCorner = ' ';
|
||||||
|
char titleLeft = ' ';
|
||||||
|
char titleRight = ' ';
|
||||||
|
|
||||||
|
ThemeDefinition themeDefinition = component.getTheme().getDefinition(AbstractBorder.class);
|
||||||
|
graphics.applyThemeStyle(themeDefinition.getNormal());
|
||||||
|
graphics.setCharacter(0, drawableArea.getRows() - 1, bottomLeftCorner);
|
||||||
|
if(drawableArea.getRows() > 2) {
|
||||||
|
graphics.drawLine(new TerminalPosition(0, drawableArea.getRows() - 2), new TerminalPosition(0, 1), verticalLine);
|
||||||
|
}
|
||||||
|
graphics.setCharacter(0, 0, topLeftCorner);
|
||||||
|
if(drawableArea.getColumns() > 2) {
|
||||||
|
graphics.drawLine(new TerminalPosition(1, 0), new TerminalPosition(drawableArea.getColumns() - 2, 0), horizontalLine);
|
||||||
|
}
|
||||||
|
|
||||||
|
graphics.applyThemeStyle(themeDefinition.getNormal());
|
||||||
|
graphics.setCharacter(drawableArea.getColumns() - 1, 0, topRightCorner);
|
||||||
|
if(drawableArea.getRows() > 2) {
|
||||||
|
graphics.drawLine(new TerminalPosition(drawableArea.getColumns() - 1, 1),
|
||||||
|
new TerminalPosition(drawableArea.getColumns() - 1, drawableArea.getRows() - 2),
|
||||||
|
verticalLine);
|
||||||
|
}
|
||||||
|
graphics.setCharacter(drawableArea.getColumns() - 1, drawableArea.getRows() - 1, bottomRightCorner);
|
||||||
|
if(drawableArea.getColumns() > 2) {
|
||||||
|
graphics.drawLine(new TerminalPosition(1, drawableArea.getRows() - 1),
|
||||||
|
new TerminalPosition(drawableArea.getColumns() - 2, drawableArea.getRows() - 1),
|
||||||
|
horizontalLine);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if(border.getTitle() != null && !border.getTitle().isEmpty() &&
|
||||||
|
drawableArea.getColumns() >= TerminalTextUtils.getColumnWidth(border.getTitle()) + 4) {
|
||||||
|
graphics.applyThemeStyle(themeDefinition.getActive());
|
||||||
|
graphics.putString(2, 0, border.getTitle());
|
||||||
|
|
||||||
|
graphics.applyThemeStyle(themeDefinition.getNormal());
|
||||||
|
graphics.setCharacter(1, 0, titleLeft);
|
||||||
|
graphics.setCharacter(2 + TerminalTextUtils.getColumnWidth(border.getTitle()), 0, titleRight);
|
||||||
|
}
|
||||||
|
|
||||||
|
wrappedComponent.draw(graphics.newTextGraphics(getWrappedComponentTopLeftOffset(), getWrappedComponentSize(drawableArea)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
package com.sparrowwallet.sparrow.terminal.wallet;
|
||||||
|
|
||||||
|
import com.googlecode.lanterna.gui2.table.DefaultTableCellRenderer;
|
||||||
|
import com.sparrowwallet.sparrow.terminal.wallet.table.TableCell;
|
||||||
|
|
||||||
|
public class EntryTableCellRenderer extends DefaultTableCellRenderer<TableCell> {
|
||||||
|
@Override
|
||||||
|
protected String[] getContent(TableCell cell) {
|
||||||
|
String[] lines;
|
||||||
|
if(cell == null) {
|
||||||
|
lines = new String[] { "" };
|
||||||
|
} else {
|
||||||
|
lines = new String[] { cell.formatCell() };
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,190 @@
|
||||||
|
package com.sparrowwallet.sparrow.terminal.wallet;
|
||||||
|
|
||||||
|
import com.googlecode.lanterna.TerminalSize;
|
||||||
|
import com.googlecode.lanterna.gui2.*;
|
||||||
|
import com.googlecode.lanterna.gui2.dialogs.*;
|
||||||
|
import com.sparrowwallet.drongo.SecureString;
|
||||||
|
import com.sparrowwallet.drongo.crypto.InvalidPasswordException;
|
||||||
|
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||||
|
import com.sparrowwallet.sparrow.EventManager;
|
||||||
|
import com.sparrowwallet.sparrow.MainApp;
|
||||||
|
import com.sparrowwallet.sparrow.TabData;
|
||||||
|
import com.sparrowwallet.sparrow.WalletTabData;
|
||||||
|
import com.sparrowwallet.sparrow.event.*;
|
||||||
|
import com.sparrowwallet.sparrow.io.Config;
|
||||||
|
import com.sparrowwallet.sparrow.io.Storage;
|
||||||
|
import com.sparrowwallet.sparrow.io.StorageException;
|
||||||
|
import com.sparrowwallet.sparrow.io.WalletAndKey;
|
||||||
|
import com.sparrowwallet.sparrow.terminal.SparrowTerminal;
|
||||||
|
import com.sparrowwallet.sparrow.wallet.WalletForm;
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import javafx.scene.control.ButtonType;
|
||||||
|
import javafx.stage.Window;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import static com.sparrowwallet.sparrow.AppServices.showErrorDialog;
|
||||||
|
import static com.sparrowwallet.sparrow.terminal.MasterActionListBox.MAX_RECENT_WALLETS;
|
||||||
|
|
||||||
|
public class LoadWallet implements Runnable {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(LoadWallet.class);
|
||||||
|
private final Storage storage;
|
||||||
|
private final LoadingDialog loadingDialog;
|
||||||
|
|
||||||
|
public LoadWallet(Storage storage) {
|
||||||
|
this.storage = storage;
|
||||||
|
this.loadingDialog = new LoadingDialog(storage);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
SparrowTerminal.get().getGui().addWindow(loadingDialog);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if(!storage.isEncrypted()) {
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
Storage.LoadWalletService loadWalletService = new Storage.LoadWalletService(storage);
|
||||||
|
loadWalletService.setExecutor(Storage.LoadWalletService.getSingleThreadedExecutor());
|
||||||
|
loadWalletService.setOnSucceeded(workerStateEvent -> {
|
||||||
|
WalletAndKey walletAndKey = loadWalletService.getValue();
|
||||||
|
SparrowTerminal.get().getGui().getGUIThread().invokeLater(() -> openWallet(storage, walletAndKey));
|
||||||
|
});
|
||||||
|
loadWalletService.setOnFailed(workerStateEvent -> {
|
||||||
|
Throwable exception = workerStateEvent.getSource().getException();
|
||||||
|
if(exception instanceof StorageException) {
|
||||||
|
showErrorDialog("Error Opening Wallet", exception.getMessage());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
loadWalletService.start();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
TextInputDialogBuilder builder = new TextInputDialogBuilder().setTitle("Wallet Password");
|
||||||
|
builder.setDescription("Enter the password for\n" + storage.getWalletName(null));
|
||||||
|
builder.setPasswordInput(true);
|
||||||
|
|
||||||
|
String password = builder.build().showDialog(SparrowTerminal.get().getGui());
|
||||||
|
if(password == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
Storage.LoadWalletService loadWalletService = new Storage.LoadWalletService(storage, new SecureString(password));
|
||||||
|
loadWalletService.setOnSucceeded(workerStateEvent -> {
|
||||||
|
EventManager.get().post(new StorageEvent(storage.getWalletId(null), TimedEvent.Action.END, "Done"));
|
||||||
|
WalletAndKey walletAndKey = loadWalletService.getValue();
|
||||||
|
SparrowTerminal.get().getGui().getGUIThread().invokeLater(() -> openWallet(storage, walletAndKey));
|
||||||
|
});
|
||||||
|
loadWalletService.setOnFailed(workerStateEvent -> {
|
||||||
|
EventManager.get().post(new StorageEvent(storage.getWalletId(null), TimedEvent.Action.END, "Failed"));
|
||||||
|
Throwable exception = loadWalletService.getException();
|
||||||
|
if(exception instanceof InvalidPasswordException) {
|
||||||
|
Optional<ButtonType> optResponse = showErrorDialog("Invalid Password", "The wallet password was invalid. Try again?", ButtonType.CANCEL, ButtonType.OK);
|
||||||
|
if(optResponse.isPresent() && optResponse.get().equals(ButtonType.OK)) {
|
||||||
|
run();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if(exception instanceof StorageException) {
|
||||||
|
showErrorDialog("Error Opening Wallet", exception.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
EventManager.get().post(new StorageEvent(storage.getWalletId(null), TimedEvent.Action.START, "Decrypting wallet..."));
|
||||||
|
loadWalletService.start();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch(Exception e) {
|
||||||
|
if(e instanceof IOException && e.getMessage().startsWith("The process cannot access the file because another process has locked")) {
|
||||||
|
showErrorDialog("Error Opening Wallet", "The wallet file is locked. Is another instance of " + MainApp.APP_NAME + " already running?");
|
||||||
|
} else {
|
||||||
|
log.error("Error opening wallet", e);
|
||||||
|
showErrorDialog("Error Opening Wallet", e.getMessage() == null ? "Unsupported file format" : e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openWallet(Storage storage, WalletAndKey walletAndKey) {
|
||||||
|
SparrowTerminal.get().getGui().removeWindow(loadingDialog);
|
||||||
|
|
||||||
|
try {
|
||||||
|
storage.restorePublicKeysFromSeed(walletAndKey.getWallet(), walletAndKey.getKey());
|
||||||
|
if(!walletAndKey.getWallet().isValid()) {
|
||||||
|
throw new IllegalStateException("Wallet file is not valid.");
|
||||||
|
}
|
||||||
|
addWallet(storage, walletAndKey.getWallet());
|
||||||
|
for(Map.Entry<WalletAndKey, Storage> entry : walletAndKey.getChildWallets().entrySet()) {
|
||||||
|
openWallet(entry.getValue(), entry.getKey());
|
||||||
|
}
|
||||||
|
if(walletAndKey.getWallet().isMasterWallet()) {
|
||||||
|
getOpeningDialog(walletAndKey.getWallet()).showDialog(SparrowTerminal.get().getGui());
|
||||||
|
}
|
||||||
|
} catch(Exception e) {
|
||||||
|
log.error("Wallet Error", e);
|
||||||
|
showErrorDialog("Wallet Error", e.getMessage());
|
||||||
|
} finally {
|
||||||
|
walletAndKey.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addWallet(Storage storage, Wallet wallet) {
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
if(wallet.isNested()) {
|
||||||
|
WalletData walletData = SparrowTerminal.get().getWalletData().get(wallet.getMasterWallet());
|
||||||
|
WalletForm walletForm = new WalletForm(storage, wallet);
|
||||||
|
EventManager.get().register(walletForm);
|
||||||
|
walletData.getWalletForm().getNestedWalletForms().add(walletForm);
|
||||||
|
} else {
|
||||||
|
EventManager.get().post(new WalletOpeningEvent(storage, wallet));
|
||||||
|
|
||||||
|
WalletForm walletForm = new WalletForm(storage, wallet);
|
||||||
|
EventManager.get().register(walletForm);
|
||||||
|
SparrowTerminal.get().getWalletData().put(wallet, new WalletData(walletForm));
|
||||||
|
|
||||||
|
List<WalletTabData> walletTabDataList = SparrowTerminal.get().getWalletData().values().stream()
|
||||||
|
.map(data -> new WalletTabData(TabData.TabType.WALLET, data.getWalletForm())).collect(Collectors.toList());
|
||||||
|
EventManager.get().post(new OpenWalletsEvent(DEFAULT_WINDOW, walletTabDataList));
|
||||||
|
|
||||||
|
Set<File> walletFiles = new LinkedHashSet<>();
|
||||||
|
walletFiles.add(storage.getWalletFile());
|
||||||
|
walletFiles.addAll(Config.get().getRecentWalletFiles().stream().limit(MAX_RECENT_WALLETS - 1).collect(Collectors.toList()));
|
||||||
|
Config.get().setRecentWalletFiles(Config.get().isLoadRecentWallets() ? new ArrayList<>(walletFiles) : Collections.emptyList());
|
||||||
|
}
|
||||||
|
|
||||||
|
EventManager.get().post(new WalletOpenedEvent(storage, wallet));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DialogWindow getOpeningDialog(Wallet masterWallet) {
|
||||||
|
if(masterWallet.getChildWallets().stream().anyMatch(childWallet -> !childWallet.isNested())) {
|
||||||
|
return new WalletAccountsDialog(masterWallet);
|
||||||
|
} else {
|
||||||
|
return new WalletActionsDialog(masterWallet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final javafx.stage.Window DEFAULT_WINDOW = new Window() { };
|
||||||
|
|
||||||
|
private static final class LoadingDialog extends DialogWindow {
|
||||||
|
public LoadingDialog(Storage storage) {
|
||||||
|
super(storage.getWalletName(null));
|
||||||
|
|
||||||
|
setHints(List.of(Hint.CENTERED));
|
||||||
|
setFixedSize(new TerminalSize(30, 5));
|
||||||
|
|
||||||
|
Panel mainPanel = new Panel();
|
||||||
|
mainPanel.setLayoutManager(new LinearLayout());
|
||||||
|
mainPanel.addComponent(new EmptySpace(), LinearLayout.createLayoutData(LinearLayout.Alignment.Beginning, LinearLayout.GrowPolicy.CanGrow));
|
||||||
|
|
||||||
|
Label label = new Label("Loading...");
|
||||||
|
mainPanel.addComponent(label, LinearLayout.createLayoutData(LinearLayout.Alignment.Center));
|
||||||
|
|
||||||
|
mainPanel.addComponent(new EmptySpace(), LinearLayout.createLayoutData(LinearLayout.Alignment.Beginning, LinearLayout.GrowPolicy.CanGrow));
|
||||||
|
|
||||||
|
setComponent(mainPanel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,222 @@
|
||||||
|
package com.sparrowwallet.sparrow.terminal.wallet;
|
||||||
|
|
||||||
|
import com.googlecode.lanterna.TerminalSize;
|
||||||
|
import com.googlecode.lanterna.gui2.*;
|
||||||
|
import com.samourai.whirlpool.client.wallet.beans.IndexRange;
|
||||||
|
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||||
|
import com.sparrowwallet.drongo.wallet.MixConfig;
|
||||||
|
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||||
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
|
import com.sparrowwallet.sparrow.wallet.WalletForm;
|
||||||
|
import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import static com.sparrowwallet.drongo.wallet.StandardAccount.WHIRLPOOL_BADBANK;
|
||||||
|
import static com.sparrowwallet.drongo.wallet.StandardAccount.WHIRLPOOL_PREMIX;
|
||||||
|
|
||||||
|
public class MixToDialog extends WalletDialog {
|
||||||
|
private static final DisplayWallet NONE_DISPLAY_WALLET = new DisplayWallet(null);
|
||||||
|
|
||||||
|
private MixConfig mixConfig;
|
||||||
|
|
||||||
|
private final ComboBox<DisplayWallet> mixToWallet;
|
||||||
|
private final TextBox minimumMixes;
|
||||||
|
private final ComboBox<DisplayIndexRange> indexRange;
|
||||||
|
private final Button apply;
|
||||||
|
|
||||||
|
public MixToDialog(WalletForm walletForm) {
|
||||||
|
super(walletForm.getWallet().getFullDisplayName() + " Mix To", walletForm);
|
||||||
|
|
||||||
|
setHints(List.of(Hint.CENTERED));
|
||||||
|
|
||||||
|
Wallet wallet = getWalletForm().getWallet();
|
||||||
|
this.mixConfig = wallet.getMasterMixConfig().copy();
|
||||||
|
|
||||||
|
Panel mainPanel = new Panel();
|
||||||
|
mainPanel.setLayoutManager(new GridLayout(2).setHorizontalSpacing(5).setVerticalSpacing(1));
|
||||||
|
|
||||||
|
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
|
||||||
|
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
|
||||||
|
|
||||||
|
mainPanel.addComponent(new Label("Mix to wallet"));
|
||||||
|
mixToWallet = new ComboBox<>();
|
||||||
|
mainPanel.addComponent(mixToWallet);
|
||||||
|
|
||||||
|
mainPanel.addComponent(new Label("Minimum mixes"));
|
||||||
|
minimumMixes = new TextBox().setValidationPattern(Pattern.compile("[0-9]*"));
|
||||||
|
mainPanel.addComponent(minimumMixes);
|
||||||
|
|
||||||
|
mainPanel.addComponent(new Label("Postmix index range"));
|
||||||
|
indexRange = new ComboBox<>();
|
||||||
|
mainPanel.addComponent(indexRange);
|
||||||
|
|
||||||
|
Panel buttonPanel = new Panel();
|
||||||
|
buttonPanel.setLayoutManager(new GridLayout(2).setHorizontalSpacing(1));
|
||||||
|
buttonPanel.addComponent(new Button("Cancel", this::onCancel));
|
||||||
|
apply = new Button("Apply", this::onApply).setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.CENTER, GridLayout.Alignment.CENTER, true, false));
|
||||||
|
apply.setEnabled(false);
|
||||||
|
buttonPanel.addComponent(apply);
|
||||||
|
|
||||||
|
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
|
||||||
|
|
||||||
|
buttonPanel.setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.END, GridLayout.Alignment.CENTER,false,false)).addTo(mainPanel);
|
||||||
|
setComponent(mainPanel);
|
||||||
|
|
||||||
|
List<DisplayWallet> allWallets = new ArrayList<>();
|
||||||
|
allWallets.add(NONE_DISPLAY_WALLET);
|
||||||
|
List<Wallet> destinationWallets = AppServices.get().getOpenWallets().keySet().stream().filter(openWallet -> openWallet.isValid()
|
||||||
|
&& (openWallet.getScriptType() == ScriptType.P2WPKH || openWallet.getScriptType() == ScriptType.P2WSH)
|
||||||
|
&& openWallet != wallet && openWallet != wallet.getMasterWallet()
|
||||||
|
&& (openWallet.getStandardAccountType() == null || !List.of(WHIRLPOOL_PREMIX, WHIRLPOOL_BADBANK).contains(openWallet.getStandardAccountType()))).collect(Collectors.toList());
|
||||||
|
allWallets.addAll(destinationWallets.stream().map(DisplayWallet::new).collect(Collectors.toList()));
|
||||||
|
allWallets.forEach(mixToWallet::addItem);
|
||||||
|
|
||||||
|
String mixToWalletId = null;
|
||||||
|
try {
|
||||||
|
mixToWalletId = AppServices.getWhirlpoolServices().getWhirlpoolMixToWalletId(mixConfig);
|
||||||
|
} catch(NoSuchElementException e) {
|
||||||
|
//ignore, mix to wallet is not open
|
||||||
|
}
|
||||||
|
|
||||||
|
if(mixToWalletId != null) {
|
||||||
|
mixToWallet.setSelectedItem(new DisplayWallet(AppServices.get().getWallet(mixToWalletId)));
|
||||||
|
} else {
|
||||||
|
mixToWallet.setSelectedItem(NONE_DISPLAY_WALLET);
|
||||||
|
}
|
||||||
|
|
||||||
|
int initialMinMixes = mixConfig.getMinMixes() == null ? Whirlpool.DEFAULT_MIXTO_MIN_MIXES : mixConfig.getMinMixes();
|
||||||
|
minimumMixes.setText(Integer.toString(initialMinMixes));
|
||||||
|
|
||||||
|
List<DisplayIndexRange> indexRanges = Arrays.stream(IndexRange.values()).map(DisplayIndexRange::new).collect(Collectors.toList());
|
||||||
|
indexRanges.forEach(indexRange::addItem);
|
||||||
|
|
||||||
|
indexRange.setSelectedItem(new DisplayIndexRange(IndexRange.FULL));
|
||||||
|
if(mixConfig.getIndexRange() != null) {
|
||||||
|
try {
|
||||||
|
indexRange.setSelectedItem(new DisplayIndexRange(IndexRange.valueOf(mixConfig.getIndexRange())));
|
||||||
|
} catch(Exception e) {
|
||||||
|
//ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mixToWallet.addListener((selectedIndex, previousSelection, changedByUserInteraction) -> {
|
||||||
|
DisplayWallet newValue = mixToWallet.getSelectedItem();
|
||||||
|
if(newValue == NONE_DISPLAY_WALLET) {
|
||||||
|
mixConfig.setMixToWalletName(null);
|
||||||
|
mixConfig.setMixToWalletFile(null);
|
||||||
|
} else {
|
||||||
|
mixConfig.setMixToWalletName(newValue.getWallet().getName());
|
||||||
|
mixConfig.setMixToWalletFile(AppServices.get().getOpenWallets().get(newValue.getWallet()).getWalletFile());
|
||||||
|
}
|
||||||
|
apply.setEnabled(apply.isEnabled() || selectedIndex != previousSelection);
|
||||||
|
});
|
||||||
|
|
||||||
|
minimumMixes.setTextChangeListener((newText, changedByUserInteraction) -> {
|
||||||
|
try {
|
||||||
|
int newValue = Integer.parseInt(newText);
|
||||||
|
if(newValue < 2 || newValue > 10000) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mixConfig.setMinMixes(newValue);
|
||||||
|
apply.setEnabled(true);
|
||||||
|
} catch(NumberFormatException e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
indexRange.addListener((selectedIndex, previousSelection, changedByUserInteraction) -> {
|
||||||
|
DisplayIndexRange newValue = indexRange.getSelectedItem();
|
||||||
|
mixConfig.setIndexRange(newValue.getIndexRange().toString());
|
||||||
|
apply.setEnabled(apply.isEnabled() || selectedIndex != previousSelection);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onCancel() {
|
||||||
|
mixConfig = null;
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onApply() {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object showDialog(WindowBasedTextGUI textGUI) {
|
||||||
|
super.showDialog(textGUI);
|
||||||
|
return mixConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class DisplayWallet {
|
||||||
|
private final Wallet wallet;
|
||||||
|
|
||||||
|
public DisplayWallet(Wallet wallet) {
|
||||||
|
this.wallet = wallet;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Wallet getWallet() {
|
||||||
|
return wallet;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return wallet == null ? "None" : wallet.getFullDisplayName();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if(this == o) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if(o == null || getClass() != o.getClass()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
DisplayWallet that = (DisplayWallet) o;
|
||||||
|
return Objects.equals(wallet, that.wallet);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return wallet != null ? wallet.hashCode() : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class DisplayIndexRange {
|
||||||
|
private final IndexRange indexRange;
|
||||||
|
|
||||||
|
public DisplayIndexRange(IndexRange indexRange) {
|
||||||
|
this.indexRange = indexRange;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IndexRange getIndexRange() {
|
||||||
|
return indexRange;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return indexRange.toString().charAt(0) + indexRange.toString().substring(1).toLowerCase(Locale.ROOT);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if(this == o) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if(o == null || getClass() != o.getClass()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
DisplayIndexRange that = (DisplayIndexRange) o;
|
||||||
|
return indexRange == that.indexRange;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return indexRange.hashCode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,138 @@
|
||||||
|
package com.sparrowwallet.sparrow.terminal.wallet;
|
||||||
|
|
||||||
|
import com.google.common.eventbus.Subscribe;
|
||||||
|
import com.googlecode.lanterna.TerminalSize;
|
||||||
|
import com.googlecode.lanterna.gui2.*;
|
||||||
|
import com.sparrowwallet.drongo.KeyDerivation;
|
||||||
|
import com.sparrowwallet.drongo.KeyPurpose;
|
||||||
|
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
|
||||||
|
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||||
|
import com.sparrowwallet.drongo.wallet.WalletNode;
|
||||||
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
|
import com.sparrowwallet.sparrow.event.*;
|
||||||
|
import com.sparrowwallet.sparrow.terminal.SparrowTerminal;
|
||||||
|
import com.sparrowwallet.sparrow.wallet.Function;
|
||||||
|
import com.sparrowwallet.sparrow.wallet.NodeEntry;
|
||||||
|
import com.sparrowwallet.sparrow.wallet.WalletForm;
|
||||||
|
|
||||||
|
import java.text.DateFormat;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public class ReceiveDialog extends WalletDialog {
|
||||||
|
private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm");
|
||||||
|
|
||||||
|
private final Label address;
|
||||||
|
private final Label derivation;
|
||||||
|
private final Label lastUsed;
|
||||||
|
|
||||||
|
private NodeEntry currentEntry;
|
||||||
|
|
||||||
|
public ReceiveDialog(WalletForm walletForm) {
|
||||||
|
super(walletForm.getWallet().getFullDisplayName() + " Receive", walletForm);
|
||||||
|
|
||||||
|
setHints(List.of(Hint.CENTERED));
|
||||||
|
|
||||||
|
Panel mainPanel = new Panel(new GridLayout(2).setHorizontalSpacing(2).setVerticalSpacing(1).setTopMarginSize(1));
|
||||||
|
|
||||||
|
mainPanel.addComponent(new Label("Address"));
|
||||||
|
address = new Label("").addTo(mainPanel);
|
||||||
|
|
||||||
|
mainPanel.addComponent(new Label("Derivation"));
|
||||||
|
derivation = new Label("").addTo(mainPanel);
|
||||||
|
|
||||||
|
mainPanel.addComponent(new Label("Last Used"));
|
||||||
|
lastUsed = new Label("").addTo(mainPanel);
|
||||||
|
|
||||||
|
Panel buttonPanel = new Panel();
|
||||||
|
buttonPanel.setLayoutManager(new GridLayout(2).setHorizontalSpacing(1));
|
||||||
|
buttonPanel.addComponent(new Button("Back", () -> onBack(Function.RECEIVE)));
|
||||||
|
buttonPanel.addComponent(new Button("Get Fresh Address", this::refreshAddress).setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.CENTER, GridLayout.Alignment.CENTER, true, false)));
|
||||||
|
|
||||||
|
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
|
||||||
|
|
||||||
|
buttonPanel.setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.END, GridLayout.Alignment.CENTER,false,false)).addTo(mainPanel);
|
||||||
|
setComponent(mainPanel);
|
||||||
|
|
||||||
|
refreshAddress();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void refreshAddress() {
|
||||||
|
SparrowTerminal.get().getGui().getGUIThread().invokeLater(() -> {
|
||||||
|
NodeEntry freshEntry = getWalletForm().getFreshNodeEntry(KeyPurpose.RECEIVE, currentEntry);
|
||||||
|
setNodeEntry(freshEntry);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setNodeEntry(NodeEntry nodeEntry) {
|
||||||
|
this.currentEntry = nodeEntry;
|
||||||
|
address.setText(nodeEntry.getAddress().toString());
|
||||||
|
derivation.setText(getDerivationPath(nodeEntry.getNode()));
|
||||||
|
updateLastUsed();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String getDerivationPath(WalletNode node) {
|
||||||
|
if(isSingleDerivationPath()) {
|
||||||
|
KeyDerivation firstDerivation = getWalletForm().getWallet().getKeystores().get(0).getKeyDerivation();
|
||||||
|
return firstDerivation.extend(node.getDerivation()).getDerivationPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
return node.getDerivationPath().replace("m", "multi");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected boolean isSingleDerivationPath() {
|
||||||
|
KeyDerivation firstDerivation = getWalletForm().getWallet().getKeystores().get(0).getKeyDerivation();
|
||||||
|
for(Keystore keystore : getWalletForm().getWallet().getKeystores()) {
|
||||||
|
if(!keystore.getKeyDerivation().getDerivationPath().equals(firstDerivation.getDerivationPath())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateLastUsed() {
|
||||||
|
SparrowTerminal.get().getGui().getGUIThread().invokeLater(() -> {
|
||||||
|
Set<BlockTransactionHashIndex> currentOutputs = currentEntry.getNode().getTransactionOutputs();
|
||||||
|
if(AppServices.onlineProperty().get() && currentOutputs.isEmpty()) {
|
||||||
|
lastUsed.setText("Never");
|
||||||
|
} else if(!currentOutputs.isEmpty()) {
|
||||||
|
long count = currentOutputs.size();
|
||||||
|
BlockTransactionHashIndex lastUsedReference = currentOutputs.stream().skip(count - 1).findFirst().get();
|
||||||
|
lastUsed.setText(lastUsedReference.getHeight() <= 0 ? "Unconfirmed Transaction" : (lastUsedReference.getDate() == null ? "Unknown" : DATE_FORMAT.format(lastUsedReference.getDate())));
|
||||||
|
} else {
|
||||||
|
lastUsed.setText("Unknown");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void walletNodesChanged(WalletNodesChangedEvent event) {
|
||||||
|
if(event.getWallet().equals(getWalletForm().getWallet())) {
|
||||||
|
if(currentEntry != null) {
|
||||||
|
currentEntry = null;
|
||||||
|
}
|
||||||
|
refreshAddress();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void walletHistoryChanged(WalletHistoryChangedEvent event) {
|
||||||
|
if(event.getWallet().equals(getWalletForm().getWallet())) {
|
||||||
|
if(currentEntry != null && event.getHistoryChangedNodes().contains(currentEntry.getNode())) {
|
||||||
|
refreshAddress();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void connection(ConnectionEvent event) {
|
||||||
|
updateLastUsed();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void disconnection(DisconnectionEvent event) {
|
||||||
|
updateLastUsed();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,155 @@
|
||||||
|
package com.sparrowwallet.sparrow.terminal.wallet;
|
||||||
|
|
||||||
|
import com.google.common.collect.Lists;
|
||||||
|
import com.google.common.eventbus.Subscribe;
|
||||||
|
import com.googlecode.lanterna.TerminalSize;
|
||||||
|
import com.googlecode.lanterna.gui2.*;
|
||||||
|
import com.googlecode.lanterna.gui2.table.Table;
|
||||||
|
import com.googlecode.lanterna.gui2.table.TableModel;
|
||||||
|
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||||
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
|
import com.sparrowwallet.sparrow.event.*;
|
||||||
|
import com.sparrowwallet.sparrow.net.ExchangeSource;
|
||||||
|
import com.sparrowwallet.sparrow.terminal.SparrowTerminal;
|
||||||
|
import com.sparrowwallet.sparrow.terminal.wallet.table.CoinTableCell;
|
||||||
|
import com.sparrowwallet.sparrow.terminal.wallet.table.DateTableCell;
|
||||||
|
import com.sparrowwallet.sparrow.terminal.wallet.table.TableCell;
|
||||||
|
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||||
|
import com.sparrowwallet.sparrow.wallet.Function;
|
||||||
|
import com.sparrowwallet.sparrow.wallet.WalletForm;
|
||||||
|
import com.sparrowwallet.sparrow.wallet.WalletTransactionsEntry;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class TransactionsDialog extends WalletDialog {
|
||||||
|
private final Label balance;
|
||||||
|
private final Label fiatBalance;
|
||||||
|
private final Label mempoolBalance;
|
||||||
|
private final Label fiatMempoolBalance;
|
||||||
|
private final Label transactionCount;
|
||||||
|
private final Table<TableCell> transactions;
|
||||||
|
|
||||||
|
private final String[] tableColumns = {centerPad("Date", DateTableCell.TRANSACTION_WIDTH), centerPad("Value", CoinTableCell.TRANSACTION_WIDTH), centerPad("Balance", CoinTableCell.TRANSACTION_WIDTH)};
|
||||||
|
|
||||||
|
public TransactionsDialog(WalletForm walletForm) {
|
||||||
|
super(walletForm.getWallet().getFullDisplayName() + " Transactions", walletForm);
|
||||||
|
|
||||||
|
setHints(List.of(Hint.CENTERED, Hint.EXPANDED));
|
||||||
|
Panel labelPanel = new Panel(new GridLayout(3).setHorizontalSpacing(5).setVerticalSpacing(0));
|
||||||
|
|
||||||
|
WalletTransactionsEntry walletTransactionsEntry = getWalletForm().getWalletTransactionsEntry();
|
||||||
|
|
||||||
|
labelPanel.addComponent(new Label("Balance"));
|
||||||
|
balance = new Label("").addTo(labelPanel);
|
||||||
|
fiatBalance = new Label("").addTo(labelPanel);
|
||||||
|
|
||||||
|
labelPanel.addComponent(new Label("Mempool"));
|
||||||
|
mempoolBalance = new Label("").addTo(labelPanel);
|
||||||
|
fiatMempoolBalance = new Label("").addTo(labelPanel);
|
||||||
|
|
||||||
|
labelPanel.addComponent(new Label("Transactions"));
|
||||||
|
transactionCount = new Label("").addTo(labelPanel);
|
||||||
|
labelPanel.addComponent(new EmptySpace(TerminalSize.ONE));
|
||||||
|
|
||||||
|
transactions = new Table<>(tableColumns);
|
||||||
|
transactions.setTableCellRenderer(new EntryTableCellRenderer());
|
||||||
|
|
||||||
|
updateLabels(walletTransactionsEntry);
|
||||||
|
updateHistory(getWalletForm().getWalletTransactionsEntry());
|
||||||
|
|
||||||
|
Panel buttonPanel = new Panel(new GridLayout(4).setHorizontalSpacing(2).setVerticalSpacing(0));
|
||||||
|
buttonPanel.addComponent(new EmptySpace(new TerminalSize(15, 1)));
|
||||||
|
buttonPanel.addComponent(new EmptySpace(new TerminalSize(15, 1)));
|
||||||
|
buttonPanel.addComponent(new Button("Back", () -> onBack(Function.TRANSACTIONS)));
|
||||||
|
buttonPanel.addComponent(new Button("Refresh", this::onRefresh));
|
||||||
|
|
||||||
|
Panel mainPanel = new Panel();
|
||||||
|
mainPanel.setLayoutManager(new LinearLayout(Direction.VERTICAL).setSpacing(1));
|
||||||
|
mainPanel.addComponent(labelPanel);
|
||||||
|
mainPanel.addComponent(transactions);
|
||||||
|
mainPanel.addComponent(buttonPanel);
|
||||||
|
|
||||||
|
setComponent(mainPanel);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateHistory(WalletTransactionsEntry walletTransactionsEntry) {
|
||||||
|
SparrowTerminal.get().getGui().getGUIThread().invokeLater(() -> {
|
||||||
|
TableModel<TableCell> tableModel = getTableModel(walletTransactionsEntry);
|
||||||
|
transactions.setTableModel(tableModel);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private TableModel<TableCell> getTableModel(WalletTransactionsEntry walletTransactionsEntry) {
|
||||||
|
TableModel<TableCell> tableModel = new TableModel<>(tableColumns);
|
||||||
|
for(Entry entry : Lists.reverse(walletTransactionsEntry.getChildren())) {
|
||||||
|
tableModel.addRow(new DateTableCell(entry), new CoinTableCell(entry, false), new CoinTableCell(entry, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
return tableModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateLabels(WalletTransactionsEntry walletTransactionsEntry) {
|
||||||
|
SparrowTerminal.get().getGui().getGUIThread().invokeLater(() -> {
|
||||||
|
balance.setText(formatBitcoinValue(walletTransactionsEntry.getBalance(), true));
|
||||||
|
mempoolBalance.setText(formatBitcoinValue(walletTransactionsEntry.getMempoolBalance(), true));
|
||||||
|
|
||||||
|
if(AppServices.getFiatCurrencyExchangeRate() != null) {
|
||||||
|
fiatBalance.setText(formatFiatValue(getFiatValue(walletTransactionsEntry.getBalance(), AppServices.getFiatCurrencyExchangeRate().getBtcRate())));
|
||||||
|
fiatMempoolBalance.setText(formatFiatValue(getFiatValue(walletTransactionsEntry.getMempoolBalance(), AppServices.getFiatCurrencyExchangeRate().getBtcRate())));
|
||||||
|
} else {
|
||||||
|
fiatBalance.setText("");
|
||||||
|
fiatMempoolBalance.setText("");
|
||||||
|
}
|
||||||
|
|
||||||
|
setTransactionCount(walletTransactionsEntry);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setTransactionCount(WalletTransactionsEntry walletTransactionsEntry) {
|
||||||
|
transactionCount.setText(walletTransactionsEntry.getChildren() != null ? Integer.toString(walletTransactionsEntry.getChildren().size()) : "0");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void walletNodesChanged(WalletNodesChangedEvent event) {
|
||||||
|
if(event.getWallet().equals(getWalletForm().getWallet())) {
|
||||||
|
WalletTransactionsEntry walletTransactionsEntry = getWalletForm().getWalletTransactionsEntry();
|
||||||
|
updateHistory(walletTransactionsEntry);
|
||||||
|
updateLabels(walletTransactionsEntry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void walletHistoryChanged(WalletHistoryChangedEvent event) {
|
||||||
|
if(event.getWallet().equals(getWalletForm().getWallet())) {
|
||||||
|
WalletTransactionsEntry walletTransactionsEntry = getWalletForm().getWalletTransactionsEntry();
|
||||||
|
walletTransactionsEntry.updateTransactions();
|
||||||
|
updateHistory(walletTransactionsEntry);
|
||||||
|
updateLabels(walletTransactionsEntry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void unitFormatChanged(UnitFormatChangedEvent event) {
|
||||||
|
updateHistory(getWalletForm().getWalletTransactionsEntry());
|
||||||
|
updateLabels(getWalletForm().getWalletTransactionsEntry());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void fiatCurrencySelected(FiatCurrencySelectedEvent event) {
|
||||||
|
if(event.getExchangeSource() == ExchangeSource.NONE) {
|
||||||
|
SparrowTerminal.get().getGui().getGUIThread().invokeLater(() -> {
|
||||||
|
fiatBalance.setText("");
|
||||||
|
fiatMempoolBalance.setText("");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void exchangeRatesUpdated(ExchangeRatesUpdatedEvent event) {
|
||||||
|
SparrowTerminal.get().getGui().getGUIThread().invokeLater(() -> {
|
||||||
|
WalletTransactionsEntry walletTransactionsEntry = getWalletForm().getWalletTransactionsEntry();
|
||||||
|
fiatBalance.setText(formatFiatValue(getFiatValue(walletTransactionsEntry.getBalance(), event.getBtcRate())));
|
||||||
|
fiatMempoolBalance.setText(formatFiatValue(getFiatValue(walletTransactionsEntry.getMempoolBalance(), event.getBtcRate())));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,410 @@
|
||||||
|
package com.sparrowwallet.sparrow.terminal.wallet;
|
||||||
|
|
||||||
|
import com.google.common.eventbus.Subscribe;
|
||||||
|
import com.googlecode.lanterna.TerminalSize;
|
||||||
|
import com.googlecode.lanterna.gui2.*;
|
||||||
|
import com.googlecode.lanterna.gui2.table.Table;
|
||||||
|
import com.googlecode.lanterna.gui2.table.TableModel;
|
||||||
|
import com.samourai.whirlpool.client.wallet.beans.MixProgress;
|
||||||
|
import com.sparrowwallet.drongo.wallet.MixConfig;
|
||||||
|
import com.sparrowwallet.drongo.wallet.StandardAccount;
|
||||||
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
|
import com.sparrowwallet.sparrow.EventManager;
|
||||||
|
import com.sparrowwallet.sparrow.event.*;
|
||||||
|
import com.sparrowwallet.sparrow.net.ExchangeSource;
|
||||||
|
import com.sparrowwallet.sparrow.terminal.SparrowTerminal;
|
||||||
|
import com.sparrowwallet.sparrow.terminal.wallet.table.*;
|
||||||
|
import com.sparrowwallet.sparrow.wallet.*;
|
||||||
|
import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import javafx.beans.value.ChangeListener;
|
||||||
|
import javafx.beans.value.WeakChangeListener;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.NoSuchElementException;
|
||||||
|
|
||||||
|
public class UtxosDialog extends WalletDialog {
|
||||||
|
private final Label balance;
|
||||||
|
private final Label fiatBalance;
|
||||||
|
private final Label mempoolBalance;
|
||||||
|
private final Label fiatMempoolBalance;
|
||||||
|
private final Label utxoCount;
|
||||||
|
private final Table<TableCell> utxos;
|
||||||
|
|
||||||
|
private Button startMix;
|
||||||
|
private Button mixTo;
|
||||||
|
|
||||||
|
private final ChangeListener<Boolean> mixingOnlineListener = (observable, oldValue, newValue) -> {
|
||||||
|
SparrowTerminal.get().getGui().getGUIThread().invokeLater(() -> startMix.setEnabled(newValue));
|
||||||
|
};
|
||||||
|
|
||||||
|
private final ChangeListener<Boolean> mixingStartingListener = (observable, oldValue, newValue) -> {
|
||||||
|
try {
|
||||||
|
SparrowTerminal.get().getGui().getGUIThread().invokeAndWait(() -> {
|
||||||
|
startMix.setEnabled(!newValue && AppServices.onlineProperty().get());
|
||||||
|
startMix.setLabel(newValue && AppServices.onlineProperty().get() ? "Starting Mixing..." : isMixing() ? "Stop Mixing" : "Start Mixing");
|
||||||
|
mixTo.setEnabled(!newValue);
|
||||||
|
});
|
||||||
|
} catch(InterruptedException e) {
|
||||||
|
//ignore
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private final ChangeListener<Boolean> mixingStoppingListener = (observable, oldValue, newValue) -> {
|
||||||
|
try {
|
||||||
|
SparrowTerminal.get().getGui().getGUIThread().invokeAndWait(() -> {
|
||||||
|
startMix.setEnabled(!newValue && AppServices.onlineProperty().get());
|
||||||
|
startMix.setLabel(newValue ? "Stopping Mixing..." : isMixing() ? "Stop Mixing" : "Start Mixing");
|
||||||
|
mixTo.setEnabled(!newValue);
|
||||||
|
});
|
||||||
|
} catch(InterruptedException e) {
|
||||||
|
//ignore
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private final ChangeListener<Boolean> mixingListener = (observable, oldValue, newValue) -> {
|
||||||
|
if(!newValue) {
|
||||||
|
WalletUtxosEntry walletUtxosEntry = getWalletForm().getWalletUtxosEntry();
|
||||||
|
for(Entry entry : walletUtxosEntry.getChildren()) {
|
||||||
|
UtxoEntry utxoEntry = (UtxoEntry)entry;
|
||||||
|
if(utxoEntry.getMixStatus() != null && utxoEntry.getMixStatus().getMixProgress() != null
|
||||||
|
&& utxoEntry.getMixStatus().getMixProgress().getMixStep() != null
|
||||||
|
&& utxoEntry.getMixStatus().getMixProgress().getMixStep().isInterruptable()) {
|
||||||
|
whirlpoolMix(new WhirlpoolMixEvent(getWalletForm().getWallet(), utxoEntry.getHashIndex(), (MixProgress)null));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public UtxosDialog(WalletForm walletForm) {
|
||||||
|
super(walletForm.getWallet().getFullDisplayName() + " UTXOs", walletForm);
|
||||||
|
|
||||||
|
setHints(List.of(Hint.CENTERED, Hint.EXPANDED));
|
||||||
|
Panel labelPanel = new Panel(new GridLayout(3).setHorizontalSpacing(5).setVerticalSpacing(0));
|
||||||
|
|
||||||
|
WalletUtxosEntry walletUtxosEntry = getWalletForm().getWalletUtxosEntry();
|
||||||
|
|
||||||
|
labelPanel.addComponent(new Label("Balance"));
|
||||||
|
balance = new Label("").addTo(labelPanel);
|
||||||
|
fiatBalance = new Label("").addTo(labelPanel);
|
||||||
|
|
||||||
|
labelPanel.addComponent(new Label("Mempool"));
|
||||||
|
mempoolBalance = new Label("").addTo(labelPanel);
|
||||||
|
fiatMempoolBalance = new Label("").addTo(labelPanel);
|
||||||
|
|
||||||
|
labelPanel.addComponent(new Label("UTXOs"));
|
||||||
|
utxoCount = new Label("").addTo(labelPanel);
|
||||||
|
labelPanel.addComponent(new EmptySpace(TerminalSize.ONE));
|
||||||
|
|
||||||
|
utxos = new Table<>(getTableColumns());
|
||||||
|
utxos.setTableCellRenderer(new EntryTableCellRenderer());
|
||||||
|
|
||||||
|
updateLabels(walletUtxosEntry);
|
||||||
|
updateHistory(getWalletForm().getWalletUtxosEntry());
|
||||||
|
|
||||||
|
Panel buttonPanel = new Panel(new GridLayout(4).setHorizontalSpacing(2).setVerticalSpacing(0));
|
||||||
|
if(getWalletForm().getWallet().isWhirlpoolMixWallet()) {
|
||||||
|
startMix = new Button("Start Mixing", this::toggleMixing).setSize(new TerminalSize(20, 1)).addTo(buttonPanel);
|
||||||
|
startMix.setEnabled(AppServices.onlineProperty().get());
|
||||||
|
|
||||||
|
mixTo = new Button("Mix to...", this::showMixToDialog).addTo(buttonPanel);
|
||||||
|
if(getWalletForm().getWallet().getStandardAccountType() == StandardAccount.WHIRLPOOL_POSTMIX) {
|
||||||
|
buttonPanel.addComponent(mixTo);
|
||||||
|
} else {
|
||||||
|
buttonPanel.addComponent(new EmptySpace(new TerminalSize(15, 1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(getWalletForm().getWallet());
|
||||||
|
if(whirlpool != null) {
|
||||||
|
startMix.setLabel(whirlpool.isMixing() ? "Stop Mixing" : "Start Mixing");
|
||||||
|
if(whirlpool.startingProperty().getValue()) {
|
||||||
|
mixingStartingListener.changed(whirlpool.startingProperty(), null, whirlpool.startingProperty().getValue());
|
||||||
|
}
|
||||||
|
whirlpool.startingProperty().addListener(new WeakChangeListener<>(mixingStartingListener));
|
||||||
|
if(whirlpool.stoppingProperty().getValue()) {
|
||||||
|
mixingStoppingListener.changed(whirlpool.stoppingProperty(), null, whirlpool.stoppingProperty().getValue());
|
||||||
|
}
|
||||||
|
whirlpool.stoppingProperty().addListener(new WeakChangeListener<>(mixingStoppingListener));
|
||||||
|
whirlpool.mixingProperty().addListener(new WeakChangeListener<>(mixingListener));
|
||||||
|
updateMixToButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
AppServices.onlineProperty().addListener(new WeakChangeListener<>(mixingOnlineListener));
|
||||||
|
|
||||||
|
buttonPanel.addComponent(new Button("Back", () -> onBack(Function.UTXOS)));
|
||||||
|
buttonPanel.addComponent(new Button("Refresh", this::onRefresh));
|
||||||
|
} else {
|
||||||
|
buttonPanel.addComponent(new EmptySpace(new TerminalSize(15, 1)));
|
||||||
|
buttonPanel.addComponent(new EmptySpace(new TerminalSize(15, 1)));
|
||||||
|
buttonPanel.addComponent(new Button("Back", () -> onBack(Function.UTXOS)));
|
||||||
|
buttonPanel.addComponent(new Button("Refresh", this::onRefresh));
|
||||||
|
}
|
||||||
|
|
||||||
|
Panel mainPanel = new Panel();
|
||||||
|
mainPanel.setLayoutManager(new LinearLayout(Direction.VERTICAL).setSpacing(1));
|
||||||
|
mainPanel.addComponent(labelPanel);
|
||||||
|
mainPanel.addComponent(utxos);
|
||||||
|
mainPanel.addComponent(buttonPanel);
|
||||||
|
|
||||||
|
setComponent(mainPanel);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String[] getTableColumns() {
|
||||||
|
if(getWalletForm().getWallet().isWhirlpoolMixWallet()) {
|
||||||
|
return new String[] {centerPad("Date", DateTableCell.UTXO_WIDTH), centerPad("Output", OutputTableCell.WIDTH),
|
||||||
|
centerPad("Mixes", MixTableCell.WIDTH), centerPad("Value", CoinTableCell.UTXO_WIDTH)};
|
||||||
|
}
|
||||||
|
|
||||||
|
return new String[] {centerPad("Date", DateTableCell.UTXO_WIDTH), centerPad("Output", OutputTableCell.WIDTH),
|
||||||
|
centerPad("Address", AddressTableCell.UTXO_WIDTH), centerPad("Value", CoinTableCell.UTXO_WIDTH)};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateHistory(WalletUtxosEntry walletUtxosEntry) {
|
||||||
|
SparrowTerminal.get().getGui().getGUIThread().invokeLater(() -> {
|
||||||
|
TableModel<TableCell> tableModel = getTableModel(walletUtxosEntry);
|
||||||
|
utxos.setTableModel(tableModel);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private TableModel<TableCell> getTableModel(WalletUtxosEntry walletUtxosEntry) {
|
||||||
|
TableModel<TableCell> tableModel = new TableModel<>(getTableColumns());
|
||||||
|
List<Entry> utxoList = new ArrayList<>(walletUtxosEntry.getChildren());
|
||||||
|
utxoList.sort((o1, o2) -> Long.compare(o2.getValue(), o1.getValue()));
|
||||||
|
|
||||||
|
for(Entry entry : utxoList) {
|
||||||
|
if(walletUtxosEntry.getWallet().isWhirlpoolMixWallet()) {
|
||||||
|
tableModel.addRow(new DateTableCell(entry), new OutputTableCell(entry), new MixTableCell(entry), new CoinTableCell(entry, false));
|
||||||
|
} else {
|
||||||
|
tableModel.addRow(new DateTableCell(entry), new OutputTableCell(entry), new AddressTableCell(entry), new CoinTableCell(entry, false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tableModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateLabels(WalletUtxosEntry walletUtxosEntry) {
|
||||||
|
SparrowTerminal.get().getGui().getGUIThread().invokeLater(() -> {
|
||||||
|
balance.setText(formatBitcoinValue(walletUtxosEntry.getBalance(), true));
|
||||||
|
mempoolBalance.setText(formatBitcoinValue(walletUtxosEntry.getMempoolBalance(), true));
|
||||||
|
|
||||||
|
if(AppServices.getFiatCurrencyExchangeRate() != null) {
|
||||||
|
fiatBalance.setText(formatFiatValue(getFiatValue(walletUtxosEntry.getBalance(), AppServices.getFiatCurrencyExchangeRate().getBtcRate())));
|
||||||
|
fiatMempoolBalance.setText(formatFiatValue(getFiatValue(walletUtxosEntry.getMempoolBalance(), AppServices.getFiatCurrencyExchangeRate().getBtcRate())));
|
||||||
|
} else {
|
||||||
|
fiatBalance.setText("");
|
||||||
|
fiatMempoolBalance.setText("");
|
||||||
|
}
|
||||||
|
|
||||||
|
setUtxoCount(walletUtxosEntry);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setUtxoCount(WalletUtxosEntry walletUtxosEntry) {
|
||||||
|
utxoCount.setText(walletUtxosEntry.getChildren() != null ? Integer.toString(walletUtxosEntry.getChildren().size()) : "0");
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isMixing() {
|
||||||
|
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(getWalletForm().getWallet());
|
||||||
|
return whirlpool != null && whirlpool.isMixing();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void toggleMixing() {
|
||||||
|
if(isMixing()) {
|
||||||
|
stopMixing();
|
||||||
|
} else {
|
||||||
|
startMixing();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void startMixing() {
|
||||||
|
startMix.setEnabled(false);
|
||||||
|
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
getWalletForm().getWallet().getMasterMixConfig().setMixOnStartup(Boolean.TRUE);
|
||||||
|
EventManager.get().post(new WalletMasterMixConfigChangedEvent(getWalletForm().getWallet()));
|
||||||
|
|
||||||
|
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(getWalletForm().getWallet());
|
||||||
|
if(whirlpool != null && !whirlpool.isStarted() && AppServices.isConnected()) {
|
||||||
|
AppServices.getWhirlpoolServices().startWhirlpool(getWalletForm().getWallet(), whirlpool, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stopMixing() {
|
||||||
|
startMix.setEnabled(AppServices.onlineProperty().get());
|
||||||
|
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
getWalletForm().getWallet().getMasterMixConfig().setMixOnStartup(Boolean.FALSE);
|
||||||
|
EventManager.get().post(new WalletMasterMixConfigChangedEvent(getWalletForm().getWallet()));
|
||||||
|
|
||||||
|
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(getWalletForm().getWallet());
|
||||||
|
if(whirlpool.isStarted()) {
|
||||||
|
AppServices.getWhirlpoolServices().stopWhirlpool(whirlpool, true);
|
||||||
|
} else {
|
||||||
|
//Ensure http clients are shutdown
|
||||||
|
whirlpool.shutdown();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void showMixToDialog() {
|
||||||
|
MixToDialog mixToDialog = new MixToDialog(getWalletForm());
|
||||||
|
MixConfig changedMixConfig = (MixConfig)mixToDialog.showDialog(SparrowTerminal.get().getGui());
|
||||||
|
|
||||||
|
if(changedMixConfig != null) {
|
||||||
|
MixConfig mixConfig = getWalletForm().getWallet().getMasterMixConfig();
|
||||||
|
|
||||||
|
mixConfig.setMixToWalletName(changedMixConfig.getMixToWalletName());
|
||||||
|
mixConfig.setMixToWalletFile(changedMixConfig.getMixToWalletFile());
|
||||||
|
mixConfig.setMinMixes(changedMixConfig.getMinMixes());
|
||||||
|
mixConfig.setIndexRange(changedMixConfig.getIndexRange());
|
||||||
|
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
EventManager.get().post(new WalletMasterMixConfigChangedEvent(getWalletForm().getWallet()));
|
||||||
|
|
||||||
|
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(getWalletForm().getWallet());
|
||||||
|
whirlpool.setPostmixIndexRange(mixConfig.getIndexRange());
|
||||||
|
try {
|
||||||
|
String mixToWalletId = AppServices.getWhirlpoolServices().getWhirlpoolMixToWalletId(mixConfig);
|
||||||
|
whirlpool.setMixToWallet(mixToWalletId, mixConfig.getMinMixes());
|
||||||
|
} catch(NoSuchElementException e) {
|
||||||
|
mixConfig.setMixToWalletName(null);
|
||||||
|
mixConfig.setMixToWalletFile(null);
|
||||||
|
EventManager.get().post(new WalletMasterMixConfigChangedEvent(getWalletForm().getWallet()));
|
||||||
|
whirlpool.setMixToWallet(null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
SparrowTerminal.get().getGui().getGUIThread().invokeLater(this::updateMixToButton);
|
||||||
|
if(whirlpool.isStarted()) {
|
||||||
|
//Will automatically restart
|
||||||
|
AppServices.getWhirlpoolServices().stopWhirlpool(whirlpool, false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateMixToButton() {
|
||||||
|
if(mixTo == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
MixConfig mixConfig = getWalletForm().getWallet().getMasterMixConfig();
|
||||||
|
if(mixConfig != null && mixConfig.getMixToWalletName() != null) {
|
||||||
|
mixTo.setLabel("Mixing to " + mixConfig.getMixToWalletName());
|
||||||
|
try {
|
||||||
|
String mixToWalletId = AppServices.getWhirlpoolServices().getWhirlpoolMixToWalletId(mixConfig);
|
||||||
|
String mixToName = AppServices.get().getWallet(mixToWalletId).getFullDisplayName();
|
||||||
|
mixTo.setLabel("Mixing to " + mixToName);
|
||||||
|
} catch(NoSuchElementException e) {
|
||||||
|
mixTo.setLabel("! Not Open");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mixTo.setLabel("Mix to...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void walletNodesChanged(WalletNodesChangedEvent event) {
|
||||||
|
if(event.getWallet().equals(getWalletForm().getWallet())) {
|
||||||
|
WalletUtxosEntry walletUtxosEntry = getWalletForm().getWalletUtxosEntry();
|
||||||
|
updateHistory(walletUtxosEntry);
|
||||||
|
updateLabels(walletUtxosEntry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void walletHistoryChanged(WalletHistoryChangedEvent event) {
|
||||||
|
if(event.getWallet().equals(getWalletForm().getWallet())) {
|
||||||
|
WalletUtxosEntry walletUtxosEntry = getWalletForm().getWalletUtxosEntry();
|
||||||
|
walletUtxosEntry.updateUtxos();
|
||||||
|
updateHistory(walletUtxosEntry);
|
||||||
|
updateLabels(walletUtxosEntry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void whirlpoolMix(WhirlpoolMixEvent event) {
|
||||||
|
if(event.getWallet().equals(getWalletForm().getWallet())) {
|
||||||
|
WalletUtxosEntry walletUtxosEntry = getWalletForm().getWalletUtxosEntry();
|
||||||
|
for(Entry entry : walletUtxosEntry.getChildren()) {
|
||||||
|
UtxoEntry utxoEntry = (UtxoEntry)entry;
|
||||||
|
if(utxoEntry.getHashIndex().equals(event.getUtxo())) {
|
||||||
|
if(event.getNextUtxo() != null) {
|
||||||
|
utxoEntry.setNextMixUtxo(event.getNextUtxo());
|
||||||
|
} else if(event.getMixFailReason() != null) {
|
||||||
|
utxoEntry.setMixFailReason(event.getMixFailReason(), event.getMixError());
|
||||||
|
} else {
|
||||||
|
utxoEntry.setMixProgress(event.getMixProgress());
|
||||||
|
}
|
||||||
|
|
||||||
|
TableModel<TableCell> tableModel = utxos.getTableModel();
|
||||||
|
for(int row = 0; row < tableModel.getRowCount(); row++) {
|
||||||
|
UtxoEntry tableEntry = (UtxoEntry)tableModel.getRow(row).get(0).getEntry();
|
||||||
|
if(tableEntry.getHashIndex().equals(event.getUtxo())) {
|
||||||
|
final int utxoRow = row;
|
||||||
|
SparrowTerminal.get().getGui().getGUIThread().invokeLater(() -> {
|
||||||
|
tableModel.setCell(2, utxoRow, new MixTableCell(utxoEntry));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void newBlock(NewBlockEvent event) {
|
||||||
|
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(getWalletForm().getWallet());
|
||||||
|
if(whirlpool != null) {
|
||||||
|
for(Entry entry : getWalletForm().getWalletUtxosEntry().getChildren()) {
|
||||||
|
UtxoEntry utxoEntry = (UtxoEntry)entry;
|
||||||
|
MixProgress mixProgress = whirlpool.getMixProgress(utxoEntry.getHashIndex());
|
||||||
|
if(mixProgress != null || utxoEntry.getMixStatus() == null || (utxoEntry.getMixStatus().getMixFailReason() == null && utxoEntry.getMixStatus().getNextMixUtxo() == null)) {
|
||||||
|
whirlpoolMix(new WhirlpoolMixEvent(getWalletForm().getWallet(), utxoEntry.getHashIndex(), mixProgress));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void openWallets(OpenWalletsEvent event) {
|
||||||
|
SparrowTerminal.get().getGui().getGUIThread().invokeLater(this::updateMixToButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void walletLabelChanged(WalletLabelChangedEvent event) {
|
||||||
|
SparrowTerminal.get().getGui().getGUIThread().invokeLater(this::updateMixToButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void includeMempoolOutputsChangedEvent(IncludeMempoolOutputsChangedEvent event) {
|
||||||
|
updateHistory(getWalletForm().getWalletUtxosEntry());
|
||||||
|
updateLabels(getWalletForm().getWalletUtxosEntry());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void unitFormatChanged(UnitFormatChangedEvent event) {
|
||||||
|
updateHistory(getWalletForm().getWalletUtxosEntry());
|
||||||
|
updateLabels(getWalletForm().getWalletUtxosEntry());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void fiatCurrencySelected(FiatCurrencySelectedEvent event) {
|
||||||
|
if(event.getExchangeSource() == ExchangeSource.NONE) {
|
||||||
|
SparrowTerminal.get().getGui().getGUIThread().invokeLater(() -> {
|
||||||
|
fiatBalance.setText("");
|
||||||
|
fiatMempoolBalance.setText("");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void exchangeRatesUpdated(ExchangeRatesUpdatedEvent event) {
|
||||||
|
SparrowTerminal.get().getGui().getGUIThread().invokeLater(() -> {
|
||||||
|
WalletUtxosEntry walletUtxosEntry = getWalletForm().getWalletUtxosEntry();
|
||||||
|
fiatBalance.setText(formatFiatValue(getFiatValue(walletUtxosEntry.getBalance(), event.getBtcRate())));
|
||||||
|
fiatMempoolBalance.setText(formatFiatValue(getFiatValue(walletUtxosEntry.getMempoolBalance(), event.getBtcRate())));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
package com.sparrowwallet.sparrow.terminal.wallet;
|
||||||
|
|
||||||
|
import com.googlecode.lanterna.TerminalSize;
|
||||||
|
import com.googlecode.lanterna.gui2.*;
|
||||||
|
import com.googlecode.lanterna.gui2.dialogs.DialogWindow;
|
||||||
|
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||||
|
import com.sparrowwallet.sparrow.terminal.SparrowTerminal;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class WalletAccountsDialog extends DialogWindow {
|
||||||
|
private final Wallet masterWallet;
|
||||||
|
private final ActionListBox actions;
|
||||||
|
|
||||||
|
public WalletAccountsDialog(Wallet masterWallet) {
|
||||||
|
super(masterWallet.getFullDisplayName());
|
||||||
|
|
||||||
|
setHints(List.of(Hint.CENTERED));
|
||||||
|
|
||||||
|
this.masterWallet = masterWallet;
|
||||||
|
|
||||||
|
actions = new ActionListBox();
|
||||||
|
|
||||||
|
for(Wallet wallet : masterWallet.getAllWallets()) {
|
||||||
|
actions.addItem(wallet.getDisplayName(), () -> {
|
||||||
|
close();
|
||||||
|
SparrowTerminal.get().getGui().getGUIThread().invokeLater(() -> {
|
||||||
|
WalletActionsDialog walletActionsDialog = new WalletActionsDialog(wallet);
|
||||||
|
walletActionsDialog.showDialog(SparrowTerminal.get().getGui());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Panel mainPanel = new Panel();
|
||||||
|
mainPanel.setLayoutManager(new GridLayout(1).setLeftMarginSize(1).setRightMarginSize(1));
|
||||||
|
actions.setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.FILL, GridLayout.Alignment.CENTER, true, false)).addTo(mainPanel);
|
||||||
|
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
|
||||||
|
|
||||||
|
Panel buttonPanel = new Panel();
|
||||||
|
buttonPanel.setLayoutManager(new GridLayout(2).setHorizontalSpacing(1));
|
||||||
|
buttonPanel.addComponent(new Button("Cancel", this::onCancel).setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.CENTER, GridLayout.Alignment.CENTER, true, false)));
|
||||||
|
buttonPanel.setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.END, GridLayout.Alignment.CENTER, false, false)).addTo(mainPanel);
|
||||||
|
|
||||||
|
setComponent(mainPanel);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onCancel() {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setWalletAccount(Wallet wallet) {
|
||||||
|
Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet();
|
||||||
|
actions.setSelectedIndex(masterWallet.getAllWallets().indexOf(wallet));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,96 @@
|
||||||
|
package com.sparrowwallet.sparrow.terminal.wallet;
|
||||||
|
|
||||||
|
import com.googlecode.lanterna.TerminalSize;
|
||||||
|
import com.googlecode.lanterna.gui2.*;
|
||||||
|
import com.googlecode.lanterna.gui2.dialogs.DialogWindow;
|
||||||
|
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||||
|
import com.sparrowwallet.sparrow.terminal.SparrowTerminal;
|
||||||
|
import com.sparrowwallet.sparrow.wallet.Function;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class WalletActionsDialog extends DialogWindow {
|
||||||
|
private final Wallet wallet;
|
||||||
|
private final ActionListBox actions;
|
||||||
|
|
||||||
|
public WalletActionsDialog(Wallet wallet) {
|
||||||
|
super(wallet.getFullDisplayName());
|
||||||
|
|
||||||
|
setHints(List.of(Hint.CENTERED));
|
||||||
|
|
||||||
|
this.wallet = wallet;
|
||||||
|
|
||||||
|
actions = new ActionListBox();
|
||||||
|
actions.addItem("Transactions", () -> {
|
||||||
|
close();
|
||||||
|
TransactionsDialog transactionsDialog = getWalletData().getTransactionsDialog();
|
||||||
|
transactionsDialog.showDialog(SparrowTerminal.get().getGui());
|
||||||
|
});
|
||||||
|
if(!getWalletData().getWalletForm().getWallet().isWhirlpoolChildWallet()) {
|
||||||
|
actions.addItem("Receive", () -> {
|
||||||
|
close();
|
||||||
|
ReceiveDialog receiveDialog = getWalletData().getReceiveDialog();
|
||||||
|
receiveDialog.showDialog(SparrowTerminal.get().getGui());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
actions.addItem("Addresses", () -> {
|
||||||
|
close();
|
||||||
|
AddressesDialog addressesDialog = getWalletData().getAddressesDialog();
|
||||||
|
addressesDialog.showDialog(SparrowTerminal.get().getGui());
|
||||||
|
});
|
||||||
|
actions.addItem("UTXOs", () -> {
|
||||||
|
close();
|
||||||
|
UtxosDialog utxosDialog = getWalletData().getUtxosDialog();
|
||||||
|
utxosDialog.showDialog(SparrowTerminal.get().getGui());
|
||||||
|
});
|
||||||
|
|
||||||
|
Panel mainPanel = new Panel();
|
||||||
|
mainPanel.setLayoutManager(new GridLayout(1).setLeftMarginSize(1).setRightMarginSize(1));
|
||||||
|
actions.setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.FILL, GridLayout.Alignment.CENTER, true, false)).addTo(mainPanel);
|
||||||
|
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
|
||||||
|
|
||||||
|
Panel buttonPanel = new Panel();
|
||||||
|
buttonPanel.setLayoutManager(new GridLayout(2).setHorizontalSpacing(1));
|
||||||
|
Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet();
|
||||||
|
if(masterWallet.getChildWallets().stream().anyMatch(childWallet -> !childWallet.isNested())) {
|
||||||
|
buttonPanel.addComponent(new Button("Accounts", this::onAccounts).setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.CENTER, GridLayout.Alignment.CENTER, true, false)));
|
||||||
|
}
|
||||||
|
buttonPanel.addComponent(new Button("Cancel", this::onCancel).setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.CENTER, GridLayout.Alignment.CENTER, true, false)));
|
||||||
|
buttonPanel.setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.END, GridLayout.Alignment.CENTER, false, false)).addTo(mainPanel);
|
||||||
|
|
||||||
|
setComponent(mainPanel);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFunction(Function function) {
|
||||||
|
int isWhirlpoolWallet = getWalletData().getWalletForm().getWallet().isWhirlpoolChildWallet() ? 1 : 0;
|
||||||
|
if(function == Function.TRANSACTIONS) {
|
||||||
|
actions.setSelectedIndex(0);
|
||||||
|
} else if(function == Function.RECEIVE) {
|
||||||
|
actions.setSelectedIndex(1);
|
||||||
|
} else if(function == Function.ADDRESSES) {
|
||||||
|
actions.setSelectedIndex(2 - isWhirlpoolWallet);
|
||||||
|
} else if(function == Function.UTXOS) {
|
||||||
|
actions.setSelectedIndex(3 - isWhirlpoolWallet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onCancel() {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onAccounts() {
|
||||||
|
close();
|
||||||
|
WalletAccountsDialog walletAccountsDialog = new WalletAccountsDialog(wallet.isMasterWallet() ? wallet : wallet.getMasterWallet());
|
||||||
|
walletAccountsDialog.setWalletAccount(wallet);
|
||||||
|
walletAccountsDialog.showDialog(SparrowTerminal.get().getGui());
|
||||||
|
}
|
||||||
|
|
||||||
|
private WalletData getWalletData() {
|
||||||
|
WalletData walletData = SparrowTerminal.get().getWalletData().get(wallet);
|
||||||
|
if(walletData == null) {
|
||||||
|
throw new IllegalStateException("Wallet data is null for " + wallet.getFullDisplayName());
|
||||||
|
}
|
||||||
|
|
||||||
|
return walletData;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
package com.sparrowwallet.sparrow.terminal.wallet;
|
||||||
|
|
||||||
|
import com.sparrowwallet.sparrow.EventManager;
|
||||||
|
import com.sparrowwallet.sparrow.wallet.WalletForm;
|
||||||
|
|
||||||
|
public class WalletData {
|
||||||
|
private final WalletForm walletForm;
|
||||||
|
private TransactionsDialog transactionsDialog;
|
||||||
|
private ReceiveDialog receiveDialog;
|
||||||
|
private AddressesDialog addressesDialog;
|
||||||
|
private UtxosDialog utxosDialog;
|
||||||
|
|
||||||
|
public WalletData(WalletForm walletForm) {
|
||||||
|
this.walletForm = walletForm;
|
||||||
|
}
|
||||||
|
|
||||||
|
public WalletForm getWalletForm() {
|
||||||
|
return walletForm;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TransactionsDialog getTransactionsDialog() {
|
||||||
|
if(transactionsDialog == null) {
|
||||||
|
transactionsDialog = new TransactionsDialog(walletForm);
|
||||||
|
EventManager.get().register(transactionsDialog);
|
||||||
|
}
|
||||||
|
|
||||||
|
return transactionsDialog;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReceiveDialog getReceiveDialog() {
|
||||||
|
if(receiveDialog == null) {
|
||||||
|
receiveDialog = new ReceiveDialog(walletForm);
|
||||||
|
EventManager.get().register(receiveDialog);
|
||||||
|
}
|
||||||
|
|
||||||
|
return receiveDialog;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AddressesDialog getAddressesDialog() {
|
||||||
|
if(addressesDialog == null) {
|
||||||
|
addressesDialog = new AddressesDialog(walletForm);
|
||||||
|
EventManager.get().register(addressesDialog);
|
||||||
|
}
|
||||||
|
|
||||||
|
return addressesDialog;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UtxosDialog getUtxosDialog() {
|
||||||
|
if(utxosDialog == null) {
|
||||||
|
utxosDialog = new UtxosDialog(walletForm);
|
||||||
|
EventManager.get().register(utxosDialog);
|
||||||
|
}
|
||||||
|
|
||||||
|
return utxosDialog;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,99 @@
|
||||||
|
package com.sparrowwallet.sparrow.terminal.wallet;
|
||||||
|
|
||||||
|
import com.google.common.base.Strings;
|
||||||
|
import com.googlecode.lanterna.gui2.dialogs.DialogWindow;
|
||||||
|
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||||
|
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||||
|
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||||
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
|
import com.sparrowwallet.sparrow.CurrencyRate;
|
||||||
|
import com.sparrowwallet.sparrow.EventManager;
|
||||||
|
import com.sparrowwallet.sparrow.UnitFormat;
|
||||||
|
import com.sparrowwallet.sparrow.event.WalletHistoryClearedEvent;
|
||||||
|
import com.sparrowwallet.sparrow.io.Config;
|
||||||
|
import com.sparrowwallet.sparrow.terminal.SparrowTerminal;
|
||||||
|
import com.sparrowwallet.sparrow.wallet.Function;
|
||||||
|
import com.sparrowwallet.sparrow.wallet.WalletForm;
|
||||||
|
import javafx.application.Platform;
|
||||||
|
|
||||||
|
import java.util.Currency;
|
||||||
|
|
||||||
|
public class WalletDialog extends DialogWindow {
|
||||||
|
private final WalletForm walletForm;
|
||||||
|
|
||||||
|
public WalletDialog(String title, WalletForm walletForm) {
|
||||||
|
super(title);
|
||||||
|
this.walletForm = walletForm;
|
||||||
|
}
|
||||||
|
|
||||||
|
public WalletForm getWalletForm() {
|
||||||
|
return walletForm;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void onBack(Function function) {
|
||||||
|
close();
|
||||||
|
WalletActionsDialog walletActionsDialog = new WalletActionsDialog(getWalletForm().getWallet());
|
||||||
|
walletActionsDialog.setFunction(function);
|
||||||
|
walletActionsDialog.showDialog(SparrowTerminal.get().getGui());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void onRefresh() {
|
||||||
|
Wallet wallet = getWalletForm().getWallet();
|
||||||
|
Wallet pastWallet = wallet.copy();
|
||||||
|
wallet.clearHistory();
|
||||||
|
AppServices.clearTransactionHistoryCache(wallet);
|
||||||
|
Platform.runLater(() -> EventManager.get().post(new WalletHistoryClearedEvent(wallet, pastWallet, getWalletForm().getWalletId())));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
if(getTextGUI() != null) {
|
||||||
|
getTextGUI().removeWindow(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String formatBitcoinValue(long value, boolean appendUnit) {
|
||||||
|
BitcoinUnit unit = Config.get().getBitcoinUnit();
|
||||||
|
if(unit == null || unit.equals(BitcoinUnit.AUTO)) {
|
||||||
|
unit = (value >= BitcoinUnit.getAutoThreshold() ? BitcoinUnit.BTC : BitcoinUnit.SATOSHIS);
|
||||||
|
}
|
||||||
|
|
||||||
|
UnitFormat format = Config.get().getUnitFormat();
|
||||||
|
if(format == null) {
|
||||||
|
format = UnitFormat.DOT;
|
||||||
|
}
|
||||||
|
|
||||||
|
return unit == BitcoinUnit.SATOSHIS ? format.formatSatsValue(value) + (appendUnit ? " sats" : "") : format.formatBtcValue(value) + (appendUnit ? " BTC" : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String formatFiatValue(Double value) {
|
||||||
|
UnitFormat format = Config.get().getUnitFormat();
|
||||||
|
if(format == null) {
|
||||||
|
format = UnitFormat.DOT;
|
||||||
|
}
|
||||||
|
|
||||||
|
CurrencyRate currencyRate = AppServices.getFiatCurrencyExchangeRate();
|
||||||
|
if(currencyRate != null && currencyRate.isAvailable() && value > 0) {
|
||||||
|
Currency currency = currencyRate.getCurrency();
|
||||||
|
return currency.getSymbol() + " " + format.formatCurrencyValue(value);
|
||||||
|
} else {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected double getFiatValue(long satsValue, Double btcRate) {
|
||||||
|
return satsValue * btcRate / Transaction.SATOSHIS_PER_BITCOIN;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static String centerPad(String text, int length) {
|
||||||
|
if(text.length() >= length) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
int excess = length - text.length();
|
||||||
|
int half = excess / 2;
|
||||||
|
int extra = excess % 2;
|
||||||
|
|
||||||
|
return Strings.repeat(" ", half) + text + Strings.repeat(" ", half) + Strings.repeat(" ", extra);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
package com.sparrowwallet.sparrow.terminal.wallet.table;
|
||||||
|
|
||||||
|
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||||
|
import com.sparrowwallet.sparrow.wallet.NodeEntry;
|
||||||
|
import com.sparrowwallet.sparrow.wallet.UtxoEntry;
|
||||||
|
|
||||||
|
public class AddressTableCell extends TableCell {
|
||||||
|
public static final int UTXO_WIDTH = 18;
|
||||||
|
|
||||||
|
public AddressTableCell(Entry entry) {
|
||||||
|
super(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String formatCell() {
|
||||||
|
if(entry instanceof NodeEntry nodeEntry) {
|
||||||
|
return nodeEntry.getAddress().toString();
|
||||||
|
} else if(entry instanceof UtxoEntry utxoEntry) {
|
||||||
|
return utxoEntry.getNode().getAddress().toString().substring(0, 10) + "..";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
package com.sparrowwallet.sparrow.terminal.wallet.table;
|
||||||
|
|
||||||
|
import com.google.common.base.Strings;
|
||||||
|
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||||
|
import com.sparrowwallet.sparrow.UnitFormat;
|
||||||
|
import com.sparrowwallet.sparrow.io.Config;
|
||||||
|
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||||
|
import com.sparrowwallet.sparrow.wallet.TransactionEntry;
|
||||||
|
|
||||||
|
public class CoinTableCell extends TableCell {
|
||||||
|
public static final int TRANSACTION_WIDTH = 20;
|
||||||
|
public static final int UTXO_WIDTH = 18;
|
||||||
|
|
||||||
|
private final boolean balance;
|
||||||
|
|
||||||
|
public CoinTableCell(Entry entry, boolean balance) {
|
||||||
|
super(entry);
|
||||||
|
this.balance = balance;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String formatCell() {
|
||||||
|
Long value = null;
|
||||||
|
if(balance && entry instanceof TransactionEntry transactionEntry) {
|
||||||
|
value = transactionEntry.getBalance();
|
||||||
|
} else {
|
||||||
|
value = entry.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(value == null) {
|
||||||
|
value = 0L;
|
||||||
|
}
|
||||||
|
|
||||||
|
BitcoinUnit unit = Config.get().getBitcoinUnit();
|
||||||
|
if(unit == null || unit.equals(BitcoinUnit.AUTO)) {
|
||||||
|
unit = (value >= BitcoinUnit.getAutoThreshold() ? BitcoinUnit.BTC : BitcoinUnit.SATOSHIS);
|
||||||
|
}
|
||||||
|
|
||||||
|
UnitFormat format = Config.get().getUnitFormat();
|
||||||
|
if(format == null) {
|
||||||
|
format = UnitFormat.DOT;
|
||||||
|
}
|
||||||
|
|
||||||
|
String formattedValue = unit == BitcoinUnit.SATOSHIS ? format.formatSatsValue(value) : format.formatBtcValue(value);
|
||||||
|
return Strings.padStart(formattedValue, getWidth(entry), ' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getWidth(Entry entry) {
|
||||||
|
return entry instanceof TransactionEntry ? TRANSACTION_WIDTH : UTXO_WIDTH;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
package com.sparrowwallet.sparrow.terminal.wallet.table;
|
||||||
|
|
||||||
|
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||||
|
import com.sparrowwallet.sparrow.wallet.TransactionEntry;
|
||||||
|
import com.sparrowwallet.sparrow.wallet.UtxoEntry;
|
||||||
|
|
||||||
|
import java.text.DateFormat;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
|
||||||
|
public class DateTableCell extends TableCell {
|
||||||
|
private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm");
|
||||||
|
public static final int TRANSACTION_WIDTH = 20;
|
||||||
|
public static final int UTXO_WIDTH = 18;
|
||||||
|
|
||||||
|
public DateTableCell(Entry entry) {
|
||||||
|
super(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String formatCell() {
|
||||||
|
if(entry instanceof TransactionEntry transactionEntry && transactionEntry.getBlockTransaction() != null) {
|
||||||
|
if(transactionEntry.getBlockTransaction().getHeight() == -1) {
|
||||||
|
return "Unconfirmed Parent";
|
||||||
|
} else if(transactionEntry.getBlockTransaction().getHeight() == 0) {
|
||||||
|
return "Unconfirmed";
|
||||||
|
} else {
|
||||||
|
return DATE_FORMAT.format(transactionEntry.getBlockTransaction().getDate());
|
||||||
|
}
|
||||||
|
} else if(entry instanceof UtxoEntry utxoEntry && utxoEntry.getBlockTransaction() != null) {
|
||||||
|
if(utxoEntry.getBlockTransaction().getHeight() == -1) {
|
||||||
|
return "Unconfirmed Parent";
|
||||||
|
} else if(utxoEntry.getBlockTransaction().getHeight() == 0) {
|
||||||
|
return "Unconfirmed";
|
||||||
|
} else {
|
||||||
|
return DATE_FORMAT.format(utxoEntry.getBlockTransaction().getDate());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,64 @@
|
||||||
|
package com.sparrowwallet.sparrow.terminal.wallet.table;
|
||||||
|
|
||||||
|
import com.google.common.base.Strings;
|
||||||
|
import com.googlecode.lanterna.Symbols;
|
||||||
|
import com.samourai.whirlpool.client.mix.listener.MixFailReason;
|
||||||
|
import com.samourai.whirlpool.client.wallet.beans.MixProgress;
|
||||||
|
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||||
|
import com.sparrowwallet.sparrow.wallet.UtxoEntry;
|
||||||
|
|
||||||
|
public class MixTableCell extends TableCell {
|
||||||
|
public static final int WIDTH = 18;
|
||||||
|
|
||||||
|
public MixTableCell(Entry entry) {
|
||||||
|
super(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String formatCell() {
|
||||||
|
if(entry instanceof UtxoEntry utxoEntry) {
|
||||||
|
if(utxoEntry.getMixStatus() != null) {
|
||||||
|
UtxoEntry.MixStatus mixStatus = utxoEntry.getMixStatus();
|
||||||
|
if(mixStatus.getNextMixUtxo() != null) {
|
||||||
|
return getMixSuccess(mixStatus);
|
||||||
|
} else if(mixStatus.getMixFailReason() != null) {
|
||||||
|
return getMixFail(mixStatus);
|
||||||
|
} else if(mixStatus.getMixProgress() != null) {
|
||||||
|
return getMixProgress(mixStatus, mixStatus.getMixProgress());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return getMixCountOnly(utxoEntry.getMixStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getMixSuccess(UtxoEntry.MixStatus mixStatus) {
|
||||||
|
String msg = "Success!";
|
||||||
|
String mixesDone = Strings.padStart(Integer.toString(mixStatus.getMixesDone()), WIDTH - msg.length(), ' ');
|
||||||
|
return msg + mixesDone;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getMixFail(UtxoEntry.MixStatus mixStatus) {
|
||||||
|
if(mixStatus.getMixFailReason() == MixFailReason.CANCEL) {
|
||||||
|
return getMixCountOnly(mixStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
String msg = mixStatus.getMixFailReason().getMessage();
|
||||||
|
msg = msg.length() > 14 ? msg.substring(0, 14) : msg;
|
||||||
|
String mixesDone = Strings.padStart(Integer.toString(mixStatus.getMixesDone()), WIDTH - msg.length(), ' ');
|
||||||
|
return msg + mixesDone;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getMixProgress(UtxoEntry.MixStatus mixStatus, MixProgress mixProgress) {
|
||||||
|
int progress = mixProgress.getMixStep().getProgressPercent();
|
||||||
|
String progressBar = Strings.padEnd(Strings.repeat(Character.toString(Symbols.BLOCK_SOLID), progress / 10), 10, ' ');
|
||||||
|
String mixesDone = Strings.padStart(Integer.toString(mixStatus.getMixesDone()), WIDTH - 10, ' ');
|
||||||
|
return progressBar + mixesDone;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getMixCountOnly(UtxoEntry.MixStatus mixStatus) {
|
||||||
|
return Strings.padStart(Integer.toString(mixStatus == null ? 0 : mixStatus.getMixesDone()), WIDTH, ' ');
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
package com.sparrowwallet.sparrow.terminal.wallet.table;
|
||||||
|
|
||||||
|
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||||
|
import com.sparrowwallet.sparrow.wallet.UtxoEntry;
|
||||||
|
|
||||||
|
public class OutputTableCell extends TableCell {
|
||||||
|
public static final int WIDTH = 16;
|
||||||
|
|
||||||
|
public OutputTableCell(Entry entry) {
|
||||||
|
super(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String formatCell() {
|
||||||
|
if(entry instanceof UtxoEntry utxoEntry) {
|
||||||
|
return utxoEntry.getDescription();
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
package com.sparrowwallet.sparrow.terminal.wallet.table;
|
||||||
|
|
||||||
|
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||||
|
|
||||||
|
public abstract class TableCell {
|
||||||
|
protected final Entry entry;
|
||||||
|
|
||||||
|
public TableCell(Entry entry) {
|
||||||
|
this.entry = entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Entry getEntry() {
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract String formatCell();
|
||||||
|
}
|
|
@ -66,7 +66,7 @@ public class WhirlpoolServices {
|
||||||
List<Window> windows = whirlpoolMap.keySet().stream().map(walletId -> AppServices.get().getWindowForWallet(walletId)).filter(Objects::nonNull).distinct().collect(Collectors.toList());
|
List<Window> windows = whirlpoolMap.keySet().stream().map(walletId -> AppServices.get().getWindowForWallet(walletId)).filter(Objects::nonNull).distinct().collect(Collectors.toList());
|
||||||
for(Window window : windows) {
|
for(Window window : windows) {
|
||||||
KeyCombination keyCombination = new KeyCodeCombination(KeyCode.W, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN, KeyCombination.ALT_DOWN);
|
KeyCombination keyCombination = new KeyCodeCombination(KeyCode.W, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN, KeyCombination.ALT_DOWN);
|
||||||
if(!window.getScene().getAccelerators().containsKey(keyCombination)) {
|
if(window.getScene() != null && !window.getScene().getAccelerators().containsKey(keyCombination)) {
|
||||||
window.getScene().getAccelerators().put(keyCombination, () -> {
|
window.getScene().getAccelerators().put(keyCombination, () -> {
|
||||||
for(Whirlpool whirlpool : whirlpoolMap.values()) {
|
for(Whirlpool whirlpool : whirlpoolMap.values()) {
|
||||||
whirlpool.logDebug();
|
whirlpool.logDebug();
|
||||||
|
|
|
@ -45,4 +45,5 @@ open module com.sparrowwallet.sparrow {
|
||||||
requires net.sourceforge.streamsupport;
|
requires net.sourceforge.streamsupport;
|
||||||
requires co.nstant.in.cbor;
|
requires co.nstant.in.cbor;
|
||||||
requires com.github.librepdf.openpdf;
|
requires com.github.librepdf.openpdf;
|
||||||
|
requires com.googlecode.lanterna;
|
||||||
}
|
}
|
Loading…
Reference in a new issue