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