implement terminal mode

This commit is contained in:
Craig Raw 2022-10-06 13:10:18 +02:00
parent 52696b014f
commit 19dedfa070
42 changed files with 3647 additions and 141 deletions

View file

@ -104,6 +104,7 @@ dependencies {
implementation('org.apache.commons:commons-lang3:3.7')
implementation('net.sourceforge.streamsupport:streamsupport:1.7.0')
implementation('com.github.librepdf:openpdf:1.3.27')
implementation('com.googlecode.lanterna:lanterna:3.1.1')
testImplementation('junit:junit:4.12')
}

View file

@ -1028,8 +1028,7 @@ public class AppController implements Initializable {
private void openWallet(Storage storage, WalletAndKey walletAndKey, AppController appController, boolean forceSameWindow) {
try {
checkWalletNetwork(walletAndKey.getWallet());
restorePublicKeysFromSeed(storage, walletAndKey.getWallet(), walletAndKey.getKey());
storage.restorePublicKeysFromSeed(walletAndKey.getWallet(), walletAndKey.getKey());
if(!walletAndKey.getWallet().isValid()) {
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) {
WalletImportDialog dlg = new WalletImportDialog();
Optional<Wallet> optionalWallet = dlg.showAndWait();
@ -1234,14 +1142,12 @@ public class AppController implements Initializable {
try {
storage.setEncryptionPubKey(Storage.NO_PASSWORD_KEY);
storage.saveWallet(wallet);
checkWalletNetwork(wallet);
restorePublicKeysFromSeed(storage, wallet, null);
storage.restorePublicKeysFromSeed(wallet, null);
addWalletTabOrWindow(storage, wallet, false);
for(Wallet childWallet : wallet.getChildWallets()) {
storage.saveWallet(childWallet);
checkWalletNetwork(childWallet);
restorePublicKeysFromSeed(storage, childWallet, null);
storage.restorePublicKeysFromSeed(childWallet, null);
addWalletTabOrWindow(storage, childWallet, false);
}
Platform.runLater(() -> selectTab(wallet));
@ -1261,8 +1167,7 @@ public class AppController implements Initializable {
wallet.encrypt(key);
storage.setEncryptionPubKey(encryptionPubKey);
storage.saveWallet(wallet);
checkWalletNetwork(wallet);
restorePublicKeysFromSeed(storage, wallet, key);
storage.restorePublicKeysFromSeed(wallet, key);
addWalletTabOrWindow(storage, wallet, false);
for(Wallet childWallet : wallet.getChildWallets()) {
@ -1270,8 +1175,7 @@ public class AppController implements Initializable {
childWallet.encrypt(key);
}
storage.saveWallet(childWallet);
checkWalletNetwork(childWallet);
restorePublicKeysFromSeed(storage, childWallet, key);
storage.restorePublicKeysFromSeed(childWallet, key);
addWalletTabOrWindow(storage, childWallet, false);
}
Platform.runLater(() -> selectTab(wallet));

View file

@ -97,6 +97,8 @@ public class AppServices {
private final SorobanServices sorobanServices = new SorobanServices();
private InteractionServices interactionServices;
private static PayNymService payNymService;
private final MainApp application;
@ -173,8 +175,9 @@ public class AppServices {
openFiles(event.getFiles(), null);
};
public AppServices(MainApp application) {
private AppServices(MainApp application, InteractionServices interactionServices) {
this.application = application;
this.interactionServices = interactionServices;
EventManager.get().register(this);
EventManager.get().register(whirlpoolServices);
EventManager.get().register(sorobanServices);
@ -500,7 +503,11 @@ public class AppServices {
}
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() {
@ -515,6 +522,10 @@ public class AppServices {
return get().sorobanServices;
}
public static InteractionServices getInteractionServices() {
return get().interactionServices;
}
public static PayNymService getPayNymService() {
if(payNymService == null) {
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) {
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();
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();
return getInteractionServices().showAlert(title, content, alertType, graphic, buttons);
}
public static void setStageIcon(Window window) {
@ -1165,6 +1143,10 @@ public class AppServices {
@Subscribe
public void requestDisconnect(RequestDisconnectEvent event) {
onlineProperty.set(false);
//Ensure services don't try to reconnect
connectionService.cancel();
ratesService.cancel();
versionCheckService.cancel();
}
@Subscribe

View file

@ -17,6 +17,9 @@ public class Args {
@Parameter(names = { "--level", "-l" }, description = "Set log 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)
public boolean help;

View file

@ -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();
}
}

View file

@ -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);
}

View file

@ -14,6 +14,8 @@ import com.sparrowwallet.sparrow.preferences.PreferenceGroup;
import com.sparrowwallet.sparrow.preferences.PreferencesDialog;
import com.sparrowwallet.sparrow.instance.InstanceException;
import com.sparrowwallet.sparrow.instance.InstanceList;
import com.sparrowwallet.sparrow.terminal.SparrowTerminal;
import com.sparrowwallet.sparrow.terminal.TerminalInteractionServices;
import javafx.application.Application;
import javafx.scene.text.Font;
import javafx.stage.Stage;
@ -179,6 +181,13 @@ public class MainApp extends Application {
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();
try {

View file

@ -1,12 +1,15 @@
package com.sparrowwallet.sparrow.io;
import com.sparrowwallet.drongo.Network;
import com.sparrowwallet.drongo.SecureString;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.*;
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.sparrow.AppServices;
import com.sparrowwallet.sparrow.MainApp;
import com.sparrowwallet.sparrow.soroban.Soroban;
import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import org.apache.commons.lang3.concurrent.BasicThreadFactory;
@ -136,6 +139,98 @@ public class Storage {
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 {
if(walletFile.toPath().startsWith(getWalletsDir().toPath())) {
backupWallet(null);

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -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..." : "")));
}
}

View file

@ -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);
}
}

View file

@ -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());
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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));
}
}

View file

@ -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();
}
}

View file

@ -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());
}
});
}
}

View file

@ -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());
}
}
}

View file

@ -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();
}
}
}

View file

@ -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();
}
}

View file

@ -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)));
}
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}
}

View file

@ -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();
}
}
}

View file

@ -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();
}
}

View file

@ -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())));
});
}
}

View file

@ -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())));
});
}
}

View file

@ -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));
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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 "";
}
}

View file

@ -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;
}
}

View file

@ -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 "";
}
}

View file

@ -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, ' ');
}
}

View file

@ -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 "";
}
}

View file

@ -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();
}

View file

@ -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());
for(Window window : windows) {
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, () -> {
for(Whirlpool whirlpool : whirlpoolMap.values()) {
whirlpool.logDebug();

View file

@ -45,4 +45,5 @@ open module com.sparrowwallet.sparrow {
requires net.sourceforge.streamsupport;
requires co.nstant.in.cbor;
requires com.github.librepdf.openpdf;
requires com.googlecode.lanterna;
}