diff --git a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java index 13fb4175..62e58645 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java @@ -1634,6 +1634,10 @@ public class ElectrumServer { } private List getStandardAccounts(Wallet wallet) { + if(!wallet.getKeystores().stream().allMatch(Keystore::hasMasterPrivateKey)) { + return Collections.emptyList(); + } + List accounts = new ArrayList<>(); for(StandardAccount account : StandardAccount.values()) { if(account != StandardAccount.ACCOUNT_0 && (!StandardAccount.WHIRLPOOL_ACCOUNTS.contains(account) || wallet.getScriptType() == ScriptType.P2WPKH)) { diff --git a/src/main/java/com/sparrowwallet/sparrow/terminal/BackgroundProgressBarRenderer.java b/src/main/java/com/sparrowwallet/sparrow/terminal/BackgroundProgressBarRenderer.java new file mode 100644 index 00000000..a624b586 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/terminal/BackgroundProgressBarRenderer.java @@ -0,0 +1,38 @@ +package com.sparrowwallet.sparrow.terminal; + +import com.googlecode.lanterna.TerminalSize; +import com.googlecode.lanterna.graphics.ThemeDefinition; +import com.googlecode.lanterna.gui2.ComponentRenderer; +import com.googlecode.lanterna.gui2.ProgressBar; +import com.googlecode.lanterna.gui2.TextGUIGraphics; + +public class BackgroundProgressBarRenderer implements ComponentRenderer { + @Override + public TerminalSize getPreferredSize(ProgressBar component) { + int preferredWidth = component.getPreferredWidth(); + if(preferredWidth > 0) { + return new TerminalSize(preferredWidth, 1); + } else { + return new TerminalSize(10, 1); + } + } + + @Override + public void drawComponent(TextGUIGraphics graphics, ProgressBar component) { + TerminalSize size = graphics.getSize(); + if(size.getRows() == 0 || size.getColumns() == 0) { + return; + } + + ThemeDefinition themeDefinition = component.getThemeDefinition(); + int columnOfProgress = (int)(component.getProgress() * size.getColumns()); + for(int row = 0; row < size.getRows(); row++) { + graphics.applyThemeStyle(themeDefinition.getActive()); + for(int column = 0; column < size.getColumns(); column++) { + if(column < columnOfProgress) { + graphics.setCharacter(column, row, themeDefinition.getCharacter("FILLER", ' ')); + } + } + } + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/terminal/MasterActionListBox.java b/src/main/java/com/sparrowwallet/sparrow/terminal/MasterActionListBox.java index ff3aec06..ca967195 100644 --- a/src/main/java/com/sparrowwallet/sparrow/terminal/MasterActionListBox.java +++ b/src/main/java/com/sparrowwallet/sparrow/terminal/MasterActionListBox.java @@ -14,6 +14,7 @@ import com.sparrowwallet.sparrow.terminal.preferences.ServerStatusDialog; import com.sparrowwallet.sparrow.terminal.preferences.ServerTypeDialog; import com.sparrowwallet.sparrow.terminal.wallet.Bip39Dialog; import com.sparrowwallet.sparrow.terminal.wallet.LoadWallet; +import com.sparrowwallet.sparrow.terminal.wallet.WatchOnlyDialog; import java.io.File; import java.util.Map; @@ -31,6 +32,10 @@ public class MasterActionListBox extends ActionListBox { if(Config.get().getRecentWalletFiles() != null) { for(int i = 0; i < Config.get().getRecentWalletFiles().size() && i < MAX_RECENT_WALLETS; i++) { File recentWalletFile = Config.get().getRecentWalletFiles().get(i); + if(!recentWalletFile.exists()) { + continue; + } + Storage storage = new Storage(recentWalletFile); Optional optWallet = AppServices.get().getOpenWallets().entrySet().stream() @@ -92,7 +97,7 @@ public class MasterActionListBox extends ActionListBox { TextInputDialogBuilder newWalletNameBuilder = new TextInputDialogBuilder(); newWalletNameBuilder.setTitle("Create Wallet"); newWalletNameBuilder.setDescription("Enter a name for the wallet"); - newWalletNameBuilder.setValidator(content -> Storage.walletExists(content) ? "Wallet already exists" : null); + newWalletNameBuilder.setValidator(content -> content.isEmpty() ? "Please enter a name" : (Storage.walletExists(content) ? "Wallet already exists" : null)); String walletName = newWalletNameBuilder.build().showDialog(SparrowTerminal.get().getGui()); ActionListDialogBuilder newBuilder = new ActionListDialogBuilder(); @@ -103,8 +108,8 @@ public class MasterActionListBox extends ActionListBox { bip39Dialog.showDialog(SparrowTerminal.get().getGui()); }); newBuilder.addAction("Watch Only", () -> { - //OutputDescriptorDialog outputDescriptorDialog = new OutputDescriptorDialog(walletName); - + WatchOnlyDialog watchOnlyDialog = new WatchOnlyDialog(walletName); + watchOnlyDialog.showDialog(SparrowTerminal.get().getGui()); }); newBuilder.build().showDialog(SparrowTerminal.get().getGui()); } diff --git a/src/main/java/com/sparrowwallet/sparrow/terminal/ModalDialog.java b/src/main/java/com/sparrowwallet/sparrow/terminal/ModalDialog.java new file mode 100644 index 00000000..c179679d --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/terminal/ModalDialog.java @@ -0,0 +1,30 @@ +package com.sparrowwallet.sparrow.terminal; + +import com.googlecode.lanterna.TerminalSize; +import com.googlecode.lanterna.gui2.EmptySpace; +import com.googlecode.lanterna.gui2.Label; +import com.googlecode.lanterna.gui2.LinearLayout; +import com.googlecode.lanterna.gui2.Panel; +import com.googlecode.lanterna.gui2.dialogs.DialogWindow; + +import java.util.List; + +public final class ModalDialog extends DialogWindow { + public ModalDialog(String walletName, String description) { + super(walletName); + + 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(description); + mainPanel.addComponent(label, LinearLayout.createLayoutData(LinearLayout.Alignment.Center)); + + mainPanel.addComponent(new EmptySpace(), LinearLayout.createLayoutData(LinearLayout.Alignment.Beginning, LinearLayout.GrowPolicy.CanGrow)); + + setComponent(mainPanel); + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/terminal/SparrowTerminal.java b/src/main/java/com/sparrowwallet/sparrow/terminal/SparrowTerminal.java index d4431bdc..a7fc4739 100644 --- a/src/main/java/com/sparrowwallet/sparrow/terminal/SparrowTerminal.java +++ b/src/main/java/com/sparrowwallet/sparrow/terminal/SparrowTerminal.java @@ -103,6 +103,14 @@ public class SparrowTerminal extends Application { if(instance != null) { instance.freeLock(); } + + List recentWalletFiles = Config.get().getRecentWalletFiles(); + if(recentWalletFiles != null && !recentWalletFiles.isEmpty()) { + Set openedWalletFiles = new LinkedHashSet<>(recentWalletFiles); + openedWalletFiles.removeIf(file -> walletData.values().stream().noneMatch(data -> data.getWalletForm().getWalletFile().equals(file))); + openedWalletFiles.addAll(Config.get().getRecentWalletFiles().subList(0, Math.min(3, recentWalletFiles.size()))); + Config.get().setRecentWalletFiles(new ArrayList<>(openedWalletFiles)); + } } catch(Exception e) { log.error("Could not stop terminal screen", e); } diff --git a/src/main/java/com/sparrowwallet/sparrow/terminal/SparrowTextGui.java b/src/main/java/com/sparrowwallet/sparrow/terminal/SparrowTextGui.java index e3f438b9..a6acb56c 100644 --- a/src/main/java/com/sparrowwallet/sparrow/terminal/SparrowTextGui.java +++ b/src/main/java/com/sparrowwallet/sparrow/terminal/SparrowTextGui.java @@ -14,6 +14,8 @@ import javafx.beans.property.DoubleProperty; import javafx.beans.property.SimpleDoubleProperty; import javafx.util.Duration; +import java.util.Objects; + public class SparrowTextGui extends MultiWindowTextGUI { private final BasicWindow mainWindow; @@ -45,7 +47,7 @@ public class SparrowTextGui extends MultiWindowTextGUI { 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.setRenderer(new BackgroundProgressBarRenderer()); statusProgress.setLabelFormat(null); progressProperty.addListener((observable, oldValue, newValue) -> statusProgress.setValue((int) (newValue.doubleValue() * 100))); @@ -55,8 +57,10 @@ public class SparrowTextGui extends MultiWindowTextGUI { getMainWindow().addWindowListener(new WindowListenerAdapter() { @Override public void onResized(Window window, TerminalSize oldSize, TerminalSize newSize) { - titleBar.invalidate(); - statusBar.invalidate(); + if(!Objects.equals(oldSize, newSize)) { + titleBar.invalidate(); + statusBar.invalidate(); + } } }); @@ -119,25 +123,21 @@ public class SparrowTextGui extends MultiWindowTextGUI { 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)) ); diff --git a/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/Bip39Dialog.java b/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/Bip39Dialog.java index f61c671f..66355ee8 100644 --- a/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/Bip39Dialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/Bip39Dialog.java @@ -28,21 +28,16 @@ public class Bip39Dialog extends NewWalletDialog { private final Bip39 importer = new Bip39(); - private Wallet wallet; - - private final String walletName; private final ComboBox scriptType; private final TextBox seedWords; private final TextBox passphrase; private final Button createWallet; public Bip39Dialog(String walletName) { - super("Create BIP39 Wallet - " + walletName); + super("Create BIP39 Wallet - " + walletName, walletName); setHints(List.of(Hint.CENTERED)); - this.walletName = walletName; - Panel mainPanel = new Panel(); mainPanel.setLayoutManager(new GridLayout(2).setHorizontalSpacing(5).setVerticalSpacing(1)); @@ -152,35 +147,15 @@ public class Bip39Dialog extends NewWalletDialog { return lines; } - private void createWallet() { - close(); - - try { - wallet = getWallet(); - discoverAndSaveWallet(wallet); - } catch(ImportException e) { - log.error("Cannot import wallet", e); - } - } - - private Wallet getWallet() throws ImportException { + @Override + protected List getWallets() throws ImportException { Wallet wallet = new Wallet(walletName); wallet.setPolicyType(PolicyType.SINGLE); wallet.setScriptType(scriptType.getSelectedItem().scriptType); Keystore keystore = importer.getKeystore(wallet.getScriptType().getDefaultDerivation(), getWords(), passphrase.getText()); wallet.getKeystores().add(keystore); wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, wallet.getScriptType(), wallet.getKeystores(), 1)); - return wallet; - } - - private void onCancel() { - close(); - } - - @Override - public Wallet showDialog(WindowBasedTextGUI textGUI) { - super.showDialog(textGUI); - return wallet; + return List.of(wallet); } private static final class DisplayScriptType { diff --git a/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/LoadWallet.java b/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/LoadWallet.java index 4f6efbde..bc8adab5 100644 --- a/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/LoadWallet.java +++ b/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/LoadWallet.java @@ -12,6 +12,7 @@ import com.sparrowwallet.sparrow.event.*; import com.sparrowwallet.sparrow.io.Storage; import com.sparrowwallet.sparrow.io.StorageException; import com.sparrowwallet.sparrow.io.WalletAndKey; +import com.sparrowwallet.sparrow.terminal.ModalDialog; import com.sparrowwallet.sparrow.terminal.SparrowTerminal; import javafx.application.Platform; import javafx.scene.control.ButtonType; @@ -26,11 +27,11 @@ import static com.sparrowwallet.sparrow.AppServices.showErrorDialog; public class LoadWallet implements Runnable { private static final Logger log = LoggerFactory.getLogger(LoadWallet.class); private final Storage storage; - private final LoadingDialog loadingDialog; + private final ModalDialog loadingDialog; public LoadWallet(Storage storage) { this.storage = storage; - this.loadingDialog = new LoadingDialog(storage); + this.loadingDialog = new ModalDialog(storage.getWalletName(null), "Loading..."); } @Override @@ -47,6 +48,7 @@ public class LoadWallet implements Runnable { openWallet(storage, walletAndKey); }); loadWalletService.setOnFailed(workerStateEvent -> { + SparrowTerminal.get().getGuiThread().invokeLater(() -> SparrowTerminal.get().getGui().removeWindow(loadingDialog)); Throwable exception = workerStateEvent.getSource().getException(); if(exception instanceof StorageException) { showErrorDialog("Error Opening Wallet", exception.getMessage()); @@ -73,6 +75,7 @@ public class LoadWallet implements Runnable { }); loadWalletService.setOnFailed(workerStateEvent -> { EventManager.get().post(new StorageEvent(storage.getWalletId(null), TimedEvent.Action.END, "Failed")); + SparrowTerminal.get().getGuiThread().invokeLater(() -> SparrowTerminal.get().getGui().removeWindow(loadingDialog)); Throwable exception = loadWalletService.getException(); if(exception instanceof InvalidPasswordException) { Optional optResponse = showErrorDialog("Invalid Password", "The wallet password was invalid. Try again?", ButtonType.CANCEL, ButtonType.OK); @@ -130,24 +133,4 @@ public class LoadWallet implements Runnable { return new WalletActionsDialog(storage.getWalletId(masterWallet)); } } - - 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); - } - } } diff --git a/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/NewWalletDialog.java b/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/NewWalletDialog.java index 24602cdb..953ecb04 100644 --- a/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/NewWalletDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/NewWalletDialog.java @@ -1,10 +1,6 @@ package com.sparrowwallet.sparrow.terminal.wallet; -import com.googlecode.lanterna.TerminalSize; -import com.googlecode.lanterna.gui2.EmptySpace; -import com.googlecode.lanterna.gui2.Label; -import com.googlecode.lanterna.gui2.LinearLayout; -import com.googlecode.lanterna.gui2.Panel; +import com.googlecode.lanterna.gui2.*; import com.googlecode.lanterna.gui2.dialogs.DialogWindow; import com.googlecode.lanterna.gui2.dialogs.TextInputDialogBuilder; import com.sparrowwallet.drongo.SecureString; @@ -17,9 +13,11 @@ import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.event.StorageEvent; import com.sparrowwallet.sparrow.event.TimedEvent; +import com.sparrowwallet.sparrow.io.ImportException; import com.sparrowwallet.sparrow.io.Storage; import com.sparrowwallet.sparrow.io.StorageException; import com.sparrowwallet.sparrow.net.ElectrumServer; +import com.sparrowwallet.sparrow.terminal.ModalDialog; import com.sparrowwallet.sparrow.terminal.SparrowTerminal; import javafx.application.Platform; import org.slf4j.Logger; @@ -27,37 +25,78 @@ import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.List; +import java.util.Optional; import static com.sparrowwallet.sparrow.AppServices.showErrorDialog; -public class NewWalletDialog extends DialogWindow { +public abstract class NewWalletDialog extends DialogWindow { private static final Logger log = LoggerFactory.getLogger(NewWalletDialog.class); - public NewWalletDialog(String title) { + protected Wallet wallet; + + protected final String walletName; + + public NewWalletDialog(String title, String walletName) { super(title); + this.walletName = walletName; } - protected void discoverAndSaveWallet(Wallet wallet) { - if(AppServices.onlineProperty().get()) { - discoverAccounts(wallet); - } else { - saveWallet(wallet); + protected void createWallet() { + close(); + + try { + discoverAndSaveWallet(getWallets()); + } catch(ImportException e) { + log.error("Cannot import wallet", e); } } - private void discoverAccounts(Wallet wallet) { - DiscoveringDialog discoveringDialog = new DiscoveringDialog(wallet); + /** + * Returns a list of wallets for discovery. + * If no existing wallets are discovered, the first wallet is used. + * + * @return a list of wallet candidates + */ + protected abstract List getWallets() throws ImportException; + + protected void onCancel() { + close(); + } + + @Override + public Wallet showDialog(WindowBasedTextGUI textGUI) { + super.showDialog(textGUI); + return wallet; + } + + protected void discoverAndSaveWallet(List wallets) { + if(wallets.isEmpty()) { + return; + } + + if(AppServices.onlineProperty().get()) { + discoverAccounts(wallets); + } else { + saveWallet(wallets.get(0)); + } + } + + private void discoverAccounts(List wallets) { + ModalDialog discoveringDialog = new ModalDialog(walletName, "Discovering accounts..."); SparrowTerminal.get().getGui().addWindow(discoveringDialog); Platform.runLater(() -> { - ElectrumServer.WalletDiscoveryService walletDiscoveryService = new ElectrumServer.WalletDiscoveryService(List.of(wallet)); + ElectrumServer.WalletDiscoveryService walletDiscoveryService = new ElectrumServer.WalletDiscoveryService(wallets); walletDiscoveryService.setOnSucceeded(successEvent -> { + Optional optWallet = walletDiscoveryService.getValue(); + wallet = optWallet.orElseGet(() -> wallets.get(0)); SparrowTerminal.get().getGuiThread().invokeLater(() -> { SparrowTerminal.get().getGui().removeWindow(discoveringDialog); saveWallet(wallet); }); }); walletDiscoveryService.setOnFailed(failedEvent -> { + wallet = wallets.get(0); SparrowTerminal.get().getGuiThread().invokeLater(() -> { SparrowTerminal.get().getGui().removeWindow(discoveringDialog); saveWallet(wallet); @@ -77,6 +116,9 @@ public class NewWalletDialog extends DialogWindow { String password = builder.build().showDialog(SparrowTerminal.get().getGui()); if(password != null) { + ModalDialog savingDialog = new ModalDialog(walletName, "Saving wallet..."); + SparrowTerminal.get().getGui().addWindow(savingDialog); + Platform.runLater(() -> { if(password.length() == 0) { try { @@ -91,7 +133,10 @@ public class NewWalletDialog extends DialogWindow { SparrowTerminal.addWallet(storage, childWallet); } - SparrowTerminal.get().getGuiThread().invokeLater(() -> LoadWallet.getOpeningDialog(storage, wallet).showDialog(SparrowTerminal.get().getGui())); + SparrowTerminal.get().getGuiThread().invokeLater(() -> { + SparrowTerminal.get().getGui().removeWindow(savingDialog); + LoadWallet.getOpeningDialog(storage, wallet).showDialog(SparrowTerminal.get().getGui()); + }); } catch(IOException | StorageException | MnemonicException e) { log.error("Error saving imported wallet", e); } @@ -120,7 +165,10 @@ public class NewWalletDialog extends DialogWindow { SparrowTerminal.addWallet(storage, childWallet); } - SparrowTerminal.get().getGuiThread().invokeLater(() -> LoadWallet.getOpeningDialog(storage, wallet).showDialog(SparrowTerminal.get().getGui())); + SparrowTerminal.get().getGuiThread().invokeLater(() -> { + SparrowTerminal.get().getGui().removeWindow(savingDialog); + LoadWallet.getOpeningDialog(storage, wallet).showDialog(SparrowTerminal.get().getGui()); + }); } catch(IOException | StorageException | MnemonicException e) { log.error("Error saving imported wallet", e); } finally { @@ -131,6 +179,7 @@ public class NewWalletDialog extends DialogWindow { } }); keyDerivationService.setOnFailed(workerStateEvent -> { + SparrowTerminal.get().getGuiThread().invokeLater(() -> SparrowTerminal.get().getGui().removeWindow(savingDialog)); EventManager.get().post(new StorageEvent(Storage.getWalletFile(wallet.getName()).getAbsolutePath(), TimedEvent.Action.END, "Failed")); showErrorDialog("Error encrypting wallet", keyDerivationService.getException().getMessage()); }); @@ -141,23 +190,4 @@ public class NewWalletDialog extends DialogWindow { } } - private static final class DiscoveringDialog extends DialogWindow { - public DiscoveringDialog(Wallet wallet) { - super(wallet.getName()); - - 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("Discovering Accounts..."); - mainPanel.addComponent(label, LinearLayout.createLayoutData(LinearLayout.Alignment.Center)); - - mainPanel.addComponent(new EmptySpace(), LinearLayout.createLayoutData(LinearLayout.Alignment.Beginning, LinearLayout.GrowPolicy.CanGrow)); - - setComponent(mainPanel); - } - } } diff --git a/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/SettingsDialog.java b/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/SettingsDialog.java index 9bd15a7d..1f7304d6 100644 --- a/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/SettingsDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/SettingsDialog.java @@ -181,10 +181,10 @@ public class SettingsDialog extends WalletDialog { public static List splitString(String stringToSplit, int maxLength) { String text = stringToSplit; List lines = new ArrayList<>(); - while(text.length() > maxLength) { + while(text.length() >= maxLength) { int breakAt = maxLength - 1; lines.add(text.substring(0, breakAt)); - text = text.substring(breakAt + 1); + text = text.substring(breakAt); } lines.add(text); diff --git a/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/WatchOnlyDialog.java b/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/WatchOnlyDialog.java new file mode 100644 index 00000000..655858e3 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/WatchOnlyDialog.java @@ -0,0 +1,168 @@ +package com.sparrowwallet.sparrow.terminal.wallet; + +import com.googlecode.lanterna.TerminalPosition; +import com.googlecode.lanterna.TerminalSize; +import com.googlecode.lanterna.gui2.*; +import com.sparrowwallet.drongo.ExtendedKey; +import com.sparrowwallet.drongo.KeyDerivation; +import com.sparrowwallet.drongo.OutputDescriptor; +import com.sparrowwallet.drongo.policy.Policy; +import com.sparrowwallet.drongo.policy.PolicyType; +import com.sparrowwallet.drongo.protocol.ScriptType; +import com.sparrowwallet.drongo.wallet.Keystore; +import com.sparrowwallet.drongo.wallet.KeystoreSource; +import com.sparrowwallet.drongo.wallet.Wallet; +import com.sparrowwallet.drongo.wallet.WalletModel; +import com.sparrowwallet.sparrow.io.ImportException; +import com.sparrowwallet.sparrow.terminal.SparrowTerminal; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; + +import static com.sparrowwallet.sparrow.wallet.KeystoreController.DEFAULT_WATCH_ONLY_FINGERPRINT; + +public class WatchOnlyDialog extends NewWalletDialog { + private static final Logger log = LoggerFactory.getLogger(WatchOnlyDialog.class); + + private final TextBox descriptor; + private final Button importWallet; + + public WatchOnlyDialog(String walletName) { + super("Create Watch Only Wallet - " + walletName, walletName); + + setHints(List.of(Hint.CENTERED)); + + Panel mainPanel = new Panel(); + mainPanel.setLayoutManager(new GridLayout(2).setVerticalSpacing(0)); + + mainPanel.addComponent(new EmptySpace(TerminalSize.ONE)); + mainPanel.addComponent(new EmptySpace(TerminalSize.ZERO)); + + TerminalSize screenSize = SparrowTerminal.get().getScreen().getTerminalSize(); + int descriptorWidth = Math.min(Math.max(20, screenSize.getColumns() - 20), 120); + + mainPanel.addComponent(new Label("Output descriptor or xpub")); + mainPanel.addComponent(new EmptySpace(TerminalSize.ZERO)); + + descriptor = new TextBox(new TerminalSize(descriptorWidth, 10)); + mainPanel.addComponent(descriptor); + mainPanel.addComponent(new EmptySpace(TerminalSize.ZERO)); + + Panel buttonPanel = new Panel(); + buttonPanel.setLayoutManager(new GridLayout(2).setHorizontalSpacing(1)); + buttonPanel.addComponent(new Button("Cancel", this::onCancel)); + importWallet = new Button("Import Wallet", this::createWallet).setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.CENTER, GridLayout.Alignment.CENTER, true, false)); + importWallet.setEnabled(false); + buttonPanel.addComponent(importWallet); + + mainPanel.addComponent(new EmptySpace(TerminalSize.ONE)); + mainPanel.addComponent(new EmptySpace(TerminalSize.ZERO)); + + buttonPanel.setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.END, GridLayout.Alignment.CENTER,false,false)).addTo(mainPanel); + mainPanel.addComponent(new EmptySpace(TerminalSize.ZERO)); + + setComponent(mainPanel); + + descriptor.setTextChangeListener((newText, changedByUserInteraction) -> { + String line = newText.replaceAll("\\s+", ""); + try { + OutputDescriptor.getOutputDescriptor(line); + importWallet.setEnabled(true); + } catch(Exception e1) { + try { + ExtendedKey.fromDescriptor(line); + importWallet.setEnabled(true); + } catch(Exception e2) { + importWallet.setEnabled(false); + } + } + + if(changedByUserInteraction) { + List lines = splitString(newText, descriptorWidth); + String splitText = lines.stream().reduce((s1, s2) -> s1 + "\n" + s2).get(); + if(!newText.equals(splitText)) { + descriptor.setText(splitText); + + TerminalPosition pos = descriptor.getCaretPosition(); + if(pos.getRow() == lines.size() - 2 && pos.getColumn() == lines.get(lines.size() - 2).length()) { + descriptor.setCaretPosition(lines.size() - 1, lines.get(lines.size() - 1).length()); + } + } + } + }); + } + + @Override + protected List getWallets() throws ImportException { + try { + return getWalletFromXpub(); + } catch(Exception e1) { + try { + return getWalletFromOutputDescriptor(); + } catch(Exception e2) { + log.error("Could not determine wallet from descriptor: " + descriptor.getText(), e2); + } + } + + return Collections.emptyList(); + } + + private List getWalletFromXpub() { + ExtendedKey xpub = ExtendedKey.fromDescriptor(descriptor.getText().replaceAll("\\s+", "")); + ExtendedKey.Header header = ExtendedKey.Header.fromExtendedKey(descriptor.getText()); + + Set scriptTypes = new LinkedHashSet<>(); + scriptTypes.add(ScriptType.P2WPKH); + scriptTypes.add(header.getDefaultScriptType()); + scriptTypes.addAll(ScriptType.getAddressableScriptTypes(PolicyType.SINGLE)); + + List wallets = new ArrayList<>(); + for(ScriptType scriptType : scriptTypes) { + Wallet wallet = new Wallet(walletName); + wallet.setPolicyType(PolicyType.SINGLE); + wallet.setScriptType(scriptType); + + Keystore keystore = new Keystore(); + keystore.setSource(KeystoreSource.SW_WATCH); + keystore.setWalletModel(WalletModel.SPARROW); + keystore.setKeyDerivation(new KeyDerivation(DEFAULT_WATCH_ONLY_FINGERPRINT, scriptType.getDefaultDerivationPath())); + keystore.setExtendedPublicKey(xpub); + wallet.makeLabelsUnique(keystore); + wallet.getKeystores().add(keystore); + + wallet.setDefaultPolicy(Policy.getPolicy(wallet.getPolicyType(), wallet.getScriptType(), wallet.getKeystores(), 1)); + wallets.add(wallet); + } + + return wallets; + } + + private List getWalletFromOutputDescriptor() { + OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(descriptor.getText().replaceAll("\\s+", "")); + Wallet wallet = outputDescriptor.toWallet(); + wallet.setName(walletName); + return List.of(wallet); + } + + private List splitString(String stringToSplit, int maxLength) { + String text = stringToSplit.replaceAll("\\s+", ""); + if(stringToSplit.endsWith("\n")) { + text += "\n"; + } + + List lines = new ArrayList<>(); + while(text.length() >= maxLength) { + int breakAt = maxLength - 1; + lines.add(text.substring(0, breakAt)); + text = text.substring(breakAt); + } + + if(text.equals("\n")) { + text = ""; + } + + lines.add(text); + return lines; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/KeystoreController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/KeystoreController.java index f71adf90..eab9d4b9 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/KeystoreController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/KeystoreController.java @@ -36,7 +36,7 @@ import java.util.stream.Collectors; public class KeystoreController extends WalletFormController implements Initializable { private static final Logger log = LoggerFactory.getLogger(KeystoreController.class); - private static final String DEFAULT_WATCH_ONLY_FINGERPRINT = "00000000"; + public static final String DEFAULT_WATCH_ONLY_FINGERPRINT = "00000000"; private Keystore keystore;