terminal - create watch only wallet

This commit is contained in:
Craig Raw 2022-10-19 15:37:14 +02:00
parent 8eb092a8d6
commit 8f165b05c7
12 changed files with 341 additions and 100 deletions

View file

@ -1634,6 +1634,10 @@ public class ElectrumServer {
} }
private List<StandardAccount> getStandardAccounts(Wallet wallet) { private List<StandardAccount> getStandardAccounts(Wallet wallet) {
if(!wallet.getKeystores().stream().allMatch(Keystore::hasMasterPrivateKey)) {
return Collections.emptyList();
}
List<StandardAccount> accounts = new ArrayList<>(); List<StandardAccount> accounts = new ArrayList<>();
for(StandardAccount account : StandardAccount.values()) { for(StandardAccount account : StandardAccount.values()) {
if(account != StandardAccount.ACCOUNT_0 && (!StandardAccount.WHIRLPOOL_ACCOUNTS.contains(account) || wallet.getScriptType() == ScriptType.P2WPKH)) { if(account != StandardAccount.ACCOUNT_0 && (!StandardAccount.WHIRLPOOL_ACCOUNTS.contains(account) || wallet.getScriptType() == ScriptType.P2WPKH)) {

View file

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

View file

@ -14,6 +14,7 @@ import com.sparrowwallet.sparrow.terminal.preferences.ServerStatusDialog;
import com.sparrowwallet.sparrow.terminal.preferences.ServerTypeDialog; import com.sparrowwallet.sparrow.terminal.preferences.ServerTypeDialog;
import com.sparrowwallet.sparrow.terminal.wallet.Bip39Dialog; import com.sparrowwallet.sparrow.terminal.wallet.Bip39Dialog;
import com.sparrowwallet.sparrow.terminal.wallet.LoadWallet; import com.sparrowwallet.sparrow.terminal.wallet.LoadWallet;
import com.sparrowwallet.sparrow.terminal.wallet.WatchOnlyDialog;
import java.io.File; import java.io.File;
import java.util.Map; import java.util.Map;
@ -31,6 +32,10 @@ public class MasterActionListBox extends ActionListBox {
if(Config.get().getRecentWalletFiles() != null) { if(Config.get().getRecentWalletFiles() != null) {
for(int i = 0; i < Config.get().getRecentWalletFiles().size() && i < MAX_RECENT_WALLETS; i++) { for(int i = 0; i < Config.get().getRecentWalletFiles().size() && i < MAX_RECENT_WALLETS; i++) {
File recentWalletFile = Config.get().getRecentWalletFiles().get(i); File recentWalletFile = Config.get().getRecentWalletFiles().get(i);
if(!recentWalletFile.exists()) {
continue;
}
Storage storage = new Storage(recentWalletFile); Storage storage = new Storage(recentWalletFile);
Optional<Wallet> optWallet = AppServices.get().getOpenWallets().entrySet().stream() Optional<Wallet> optWallet = AppServices.get().getOpenWallets().entrySet().stream()
@ -92,7 +97,7 @@ public class MasterActionListBox extends ActionListBox {
TextInputDialogBuilder newWalletNameBuilder = new TextInputDialogBuilder(); TextInputDialogBuilder newWalletNameBuilder = new TextInputDialogBuilder();
newWalletNameBuilder.setTitle("Create Wallet"); newWalletNameBuilder.setTitle("Create Wallet");
newWalletNameBuilder.setDescription("Enter a name for the 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()); String walletName = newWalletNameBuilder.build().showDialog(SparrowTerminal.get().getGui());
ActionListDialogBuilder newBuilder = new ActionListDialogBuilder(); ActionListDialogBuilder newBuilder = new ActionListDialogBuilder();
@ -103,8 +108,8 @@ public class MasterActionListBox extends ActionListBox {
bip39Dialog.showDialog(SparrowTerminal.get().getGui()); bip39Dialog.showDialog(SparrowTerminal.get().getGui());
}); });
newBuilder.addAction("Watch Only", () -> { 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()); newBuilder.build().showDialog(SparrowTerminal.get().getGui());
} }

View file

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

View file

@ -103,6 +103,14 @@ public class SparrowTerminal extends Application {
if(instance != null) { if(instance != null) {
instance.freeLock(); instance.freeLock();
} }
List<File> recentWalletFiles = Config.get().getRecentWalletFiles();
if(recentWalletFiles != null && !recentWalletFiles.isEmpty()) {
Set<File> 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) { } catch(Exception e) {
log.error("Could not stop terminal screen", e); log.error("Could not stop terminal screen", e);
} }

View file

@ -14,6 +14,8 @@ import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty; import javafx.beans.property.SimpleDoubleProperty;
import javafx.util.Duration; import javafx.util.Duration;
import java.util.Objects;
public class SparrowTextGui extends MultiWindowTextGUI { public class SparrowTextGui extends MultiWindowTextGUI {
private final BasicWindow mainWindow; private final BasicWindow mainWindow;
@ -45,7 +47,7 @@ public class SparrowTextGui extends MultiWindowTextGUI {
this.statusLabel = new Label("").addTo(statusBar); this.statusLabel = new Label("").addTo(statusBar);
this.statusProgress = new ProgressBar(0, 100, 10); this.statusProgress = new ProgressBar(0, 100, 10);
statusBar.addComponent(statusProgress, GridLayout.createLayoutData(GridLayout.Alignment.END, GridLayout.Alignment.CENTER, true, false)); statusBar.addComponent(statusProgress, GridLayout.createLayoutData(GridLayout.Alignment.END, GridLayout.Alignment.CENTER, true, false));
statusProgress.setVisible(false); statusProgress.setRenderer(new BackgroundProgressBarRenderer());
statusProgress.setLabelFormat(null); statusProgress.setLabelFormat(null);
progressProperty.addListener((observable, oldValue, newValue) -> statusProgress.setValue((int) (newValue.doubleValue() * 100))); progressProperty.addListener((observable, oldValue, newValue) -> statusProgress.setValue((int) (newValue.doubleValue() * 100)));
@ -55,8 +57,10 @@ public class SparrowTextGui extends MultiWindowTextGUI {
getMainWindow().addWindowListener(new WindowListenerAdapter() { getMainWindow().addWindowListener(new WindowListenerAdapter() {
@Override @Override
public void onResized(Window window, TerminalSize oldSize, TerminalSize newSize) { public void onResized(Window window, TerminalSize oldSize, TerminalSize newSize) {
titleBar.invalidate(); if(!Objects.equals(oldSize, newSize)) {
statusBar.invalidate(); titleBar.invalidate();
statusBar.invalidate();
}
} }
}); });
@ -119,25 +123,21 @@ public class SparrowTextGui extends MultiWindowTextGUI {
if(event.getTimeMills() == 0) { if(event.getTimeMills() == 0) {
getGUIThread().invokeLater(() -> { getGUIThread().invokeLater(() -> {
statusLabel.setText(""); statusLabel.setText("");
statusProgress.setVisible(false);
statusProgress.setValue(0); statusProgress.setValue(0);
}); });
} else if(event.getTimeMills() < 0) { } else if(event.getTimeMills() < 0) {
getGUIThread().invokeLater(() -> { getGUIThread().invokeLater(() -> {
statusLabel.setText(event.getStatus()); statusLabel.setText(event.getStatus());
statusProgress.setVisible(false);
}); });
} else { } else {
getGUIThread().invokeLater(() -> { getGUIThread().invokeLater(() -> {
statusLabel.setText(event.getStatus()); statusLabel.setText(event.getStatus());
statusProgress.setVisible(true);
}); });
statusTimeline = new Timeline( statusTimeline = new Timeline(
new KeyFrame(Duration.ZERO, new KeyValue(progressProperty, 0)), new KeyFrame(Duration.ZERO, new KeyValue(progressProperty, 0)),
new KeyFrame(Duration.millis(event.getTimeMills()), e -> { new KeyFrame(Duration.millis(event.getTimeMills()), e -> {
getGUIThread().invokeLater(() -> { getGUIThread().invokeLater(() -> {
statusLabel.setText(""); statusLabel.setText("");
statusProgress.setVisible(false);
}); });
}, new KeyValue(progressProperty, 1)) }, new KeyValue(progressProperty, 1))
); );

View file

@ -28,21 +28,16 @@ public class Bip39Dialog extends NewWalletDialog {
private final Bip39 importer = new Bip39(); private final Bip39 importer = new Bip39();
private Wallet wallet;
private final String walletName;
private final ComboBox<DisplayScriptType> scriptType; private final ComboBox<DisplayScriptType> scriptType;
private final TextBox seedWords; private final TextBox seedWords;
private final TextBox passphrase; private final TextBox passphrase;
private final Button createWallet; private final Button createWallet;
public Bip39Dialog(String walletName) { public Bip39Dialog(String walletName) {
super("Create BIP39 Wallet - " + walletName); super("Create BIP39 Wallet - " + walletName, walletName);
setHints(List.of(Hint.CENTERED)); setHints(List.of(Hint.CENTERED));
this.walletName = walletName;
Panel mainPanel = new Panel(); Panel mainPanel = new Panel();
mainPanel.setLayoutManager(new GridLayout(2).setHorizontalSpacing(5).setVerticalSpacing(1)); mainPanel.setLayoutManager(new GridLayout(2).setHorizontalSpacing(5).setVerticalSpacing(1));
@ -152,35 +147,15 @@ public class Bip39Dialog extends NewWalletDialog {
return lines; return lines;
} }
private void createWallet() { @Override
close(); protected List<Wallet> getWallets() throws ImportException {
try {
wallet = getWallet();
discoverAndSaveWallet(wallet);
} catch(ImportException e) {
log.error("Cannot import wallet", e);
}
}
private Wallet getWallet() throws ImportException {
Wallet wallet = new Wallet(walletName); Wallet wallet = new Wallet(walletName);
wallet.setPolicyType(PolicyType.SINGLE); wallet.setPolicyType(PolicyType.SINGLE);
wallet.setScriptType(scriptType.getSelectedItem().scriptType); wallet.setScriptType(scriptType.getSelectedItem().scriptType);
Keystore keystore = importer.getKeystore(wallet.getScriptType().getDefaultDerivation(), getWords(), passphrase.getText()); Keystore keystore = importer.getKeystore(wallet.getScriptType().getDefaultDerivation(), getWords(), passphrase.getText());
wallet.getKeystores().add(keystore); wallet.getKeystores().add(keystore);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, wallet.getScriptType(), wallet.getKeystores(), 1)); wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, wallet.getScriptType(), wallet.getKeystores(), 1));
return wallet; return List.of(wallet);
}
private void onCancel() {
close();
}
@Override
public Wallet showDialog(WindowBasedTextGUI textGUI) {
super.showDialog(textGUI);
return wallet;
} }
private static final class DisplayScriptType { private static final class DisplayScriptType {

View file

@ -12,6 +12,7 @@ import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.io.Storage; import com.sparrowwallet.sparrow.io.Storage;
import com.sparrowwallet.sparrow.io.StorageException; import com.sparrowwallet.sparrow.io.StorageException;
import com.sparrowwallet.sparrow.io.WalletAndKey; import com.sparrowwallet.sparrow.io.WalletAndKey;
import com.sparrowwallet.sparrow.terminal.ModalDialog;
import com.sparrowwallet.sparrow.terminal.SparrowTerminal; import com.sparrowwallet.sparrow.terminal.SparrowTerminal;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.scene.control.ButtonType; import javafx.scene.control.ButtonType;
@ -26,11 +27,11 @@ import static com.sparrowwallet.sparrow.AppServices.showErrorDialog;
public class LoadWallet implements Runnable { public class LoadWallet implements Runnable {
private static final Logger log = LoggerFactory.getLogger(LoadWallet.class); private static final Logger log = LoggerFactory.getLogger(LoadWallet.class);
private final Storage storage; private final Storage storage;
private final LoadingDialog loadingDialog; private final ModalDialog loadingDialog;
public LoadWallet(Storage storage) { public LoadWallet(Storage storage) {
this.storage = storage; this.storage = storage;
this.loadingDialog = new LoadingDialog(storage); this.loadingDialog = new ModalDialog(storage.getWalletName(null), "Loading...");
} }
@Override @Override
@ -47,6 +48,7 @@ public class LoadWallet implements Runnable {
openWallet(storage, walletAndKey); openWallet(storage, walletAndKey);
}); });
loadWalletService.setOnFailed(workerStateEvent -> { loadWalletService.setOnFailed(workerStateEvent -> {
SparrowTerminal.get().getGuiThread().invokeLater(() -> SparrowTerminal.get().getGui().removeWindow(loadingDialog));
Throwable exception = workerStateEvent.getSource().getException(); Throwable exception = workerStateEvent.getSource().getException();
if(exception instanceof StorageException) { if(exception instanceof StorageException) {
showErrorDialog("Error Opening Wallet", exception.getMessage()); showErrorDialog("Error Opening Wallet", exception.getMessage());
@ -73,6 +75,7 @@ public class LoadWallet implements Runnable {
}); });
loadWalletService.setOnFailed(workerStateEvent -> { loadWalletService.setOnFailed(workerStateEvent -> {
EventManager.get().post(new StorageEvent(storage.getWalletId(null), TimedEvent.Action.END, "Failed")); 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(); Throwable exception = loadWalletService.getException();
if(exception instanceof InvalidPasswordException) { if(exception instanceof InvalidPasswordException) {
Optional<ButtonType> optResponse = showErrorDialog("Invalid Password", "The wallet password was invalid. Try again?", ButtonType.CANCEL, ButtonType.OK); Optional<ButtonType> 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)); 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);
}
}
} }

View file

@ -1,10 +1,6 @@
package com.sparrowwallet.sparrow.terminal.wallet; package com.sparrowwallet.sparrow.terminal.wallet;
import com.googlecode.lanterna.TerminalSize; import com.googlecode.lanterna.gui2.*;
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 com.googlecode.lanterna.gui2.dialogs.DialogWindow;
import com.googlecode.lanterna.gui2.dialogs.TextInputDialogBuilder; import com.googlecode.lanterna.gui2.dialogs.TextInputDialogBuilder;
import com.sparrowwallet.drongo.SecureString; import com.sparrowwallet.drongo.SecureString;
@ -17,9 +13,11 @@ import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.StorageEvent; import com.sparrowwallet.sparrow.event.StorageEvent;
import com.sparrowwallet.sparrow.event.TimedEvent; import com.sparrowwallet.sparrow.event.TimedEvent;
import com.sparrowwallet.sparrow.io.ImportException;
import com.sparrowwallet.sparrow.io.Storage; import com.sparrowwallet.sparrow.io.Storage;
import com.sparrowwallet.sparrow.io.StorageException; import com.sparrowwallet.sparrow.io.StorageException;
import com.sparrowwallet.sparrow.net.ElectrumServer; import com.sparrowwallet.sparrow.net.ElectrumServer;
import com.sparrowwallet.sparrow.terminal.ModalDialog;
import com.sparrowwallet.sparrow.terminal.SparrowTerminal; import com.sparrowwallet.sparrow.terminal.SparrowTerminal;
import javafx.application.Platform; import javafx.application.Platform;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -27,37 +25,78 @@ import org.slf4j.LoggerFactory;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;
import java.util.Optional;
import static com.sparrowwallet.sparrow.AppServices.showErrorDialog; 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); 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); super(title);
this.walletName = walletName;
} }
protected void discoverAndSaveWallet(Wallet wallet) { protected void createWallet() {
if(AppServices.onlineProperty().get()) { close();
discoverAccounts(wallet);
} else { try {
saveWallet(wallet); 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<Wallet> getWallets() throws ImportException;
protected void onCancel() {
close();
}
@Override
public Wallet showDialog(WindowBasedTextGUI textGUI) {
super.showDialog(textGUI);
return wallet;
}
protected void discoverAndSaveWallet(List<Wallet> wallets) {
if(wallets.isEmpty()) {
return;
}
if(AppServices.onlineProperty().get()) {
discoverAccounts(wallets);
} else {
saveWallet(wallets.get(0));
}
}
private void discoverAccounts(List<Wallet> wallets) {
ModalDialog discoveringDialog = new ModalDialog(walletName, "Discovering accounts...");
SparrowTerminal.get().getGui().addWindow(discoveringDialog); SparrowTerminal.get().getGui().addWindow(discoveringDialog);
Platform.runLater(() -> { Platform.runLater(() -> {
ElectrumServer.WalletDiscoveryService walletDiscoveryService = new ElectrumServer.WalletDiscoveryService(List.of(wallet)); ElectrumServer.WalletDiscoveryService walletDiscoveryService = new ElectrumServer.WalletDiscoveryService(wallets);
walletDiscoveryService.setOnSucceeded(successEvent -> { walletDiscoveryService.setOnSucceeded(successEvent -> {
Optional<Wallet> optWallet = walletDiscoveryService.getValue();
wallet = optWallet.orElseGet(() -> wallets.get(0));
SparrowTerminal.get().getGuiThread().invokeLater(() -> { SparrowTerminal.get().getGuiThread().invokeLater(() -> {
SparrowTerminal.get().getGui().removeWindow(discoveringDialog); SparrowTerminal.get().getGui().removeWindow(discoveringDialog);
saveWallet(wallet); saveWallet(wallet);
}); });
}); });
walletDiscoveryService.setOnFailed(failedEvent -> { walletDiscoveryService.setOnFailed(failedEvent -> {
wallet = wallets.get(0);
SparrowTerminal.get().getGuiThread().invokeLater(() -> { SparrowTerminal.get().getGuiThread().invokeLater(() -> {
SparrowTerminal.get().getGui().removeWindow(discoveringDialog); SparrowTerminal.get().getGui().removeWindow(discoveringDialog);
saveWallet(wallet); saveWallet(wallet);
@ -77,6 +116,9 @@ public class NewWalletDialog extends DialogWindow {
String password = builder.build().showDialog(SparrowTerminal.get().getGui()); String password = builder.build().showDialog(SparrowTerminal.get().getGui());
if(password != null) { if(password != null) {
ModalDialog savingDialog = new ModalDialog(walletName, "Saving wallet...");
SparrowTerminal.get().getGui().addWindow(savingDialog);
Platform.runLater(() -> { Platform.runLater(() -> {
if(password.length() == 0) { if(password.length() == 0) {
try { try {
@ -91,7 +133,10 @@ public class NewWalletDialog extends DialogWindow {
SparrowTerminal.addWallet(storage, childWallet); 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) { } catch(IOException | StorageException | MnemonicException e) {
log.error("Error saving imported wallet", e); log.error("Error saving imported wallet", e);
} }
@ -120,7 +165,10 @@ public class NewWalletDialog extends DialogWindow {
SparrowTerminal.addWallet(storage, childWallet); 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) { } catch(IOException | StorageException | MnemonicException e) {
log.error("Error saving imported wallet", e); log.error("Error saving imported wallet", e);
} finally { } finally {
@ -131,6 +179,7 @@ public class NewWalletDialog extends DialogWindow {
} }
}); });
keyDerivationService.setOnFailed(workerStateEvent -> { 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")); EventManager.get().post(new StorageEvent(Storage.getWalletFile(wallet.getName()).getAbsolutePath(), TimedEvent.Action.END, "Failed"));
showErrorDialog("Error encrypting wallet", keyDerivationService.getException().getMessage()); 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);
}
}
} }

View file

@ -181,10 +181,10 @@ public class SettingsDialog extends WalletDialog {
public static List<String> splitString(String stringToSplit, int maxLength) { public static List<String> splitString(String stringToSplit, int maxLength) {
String text = stringToSplit; String text = stringToSplit;
List<String> lines = new ArrayList<>(); List<String> lines = new ArrayList<>();
while(text.length() > maxLength) { while(text.length() >= maxLength) {
int breakAt = maxLength - 1; int breakAt = maxLength - 1;
lines.add(text.substring(0, breakAt)); lines.add(text.substring(0, breakAt));
text = text.substring(breakAt + 1); text = text.substring(breakAt);
} }
lines.add(text); lines.add(text);

View file

@ -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<String> 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<Wallet> 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<Wallet> getWalletFromXpub() {
ExtendedKey xpub = ExtendedKey.fromDescriptor(descriptor.getText().replaceAll("\\s+", ""));
ExtendedKey.Header header = ExtendedKey.Header.fromExtendedKey(descriptor.getText());
Set<ScriptType> scriptTypes = new LinkedHashSet<>();
scriptTypes.add(ScriptType.P2WPKH);
scriptTypes.add(header.getDefaultScriptType());
scriptTypes.addAll(ScriptType.getAddressableScriptTypes(PolicyType.SINGLE));
List<Wallet> 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<Wallet> getWalletFromOutputDescriptor() {
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(descriptor.getText().replaceAll("\\s+", ""));
Wallet wallet = outputDescriptor.toWallet();
wallet.setName(walletName);
return List.of(wallet);
}
private List<String> splitString(String stringToSplit, int maxLength) {
String text = stringToSplit.replaceAll("\\s+", "");
if(stringToSplit.endsWith("\n")) {
text += "\n";
}
List<String> 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;
}
}

View file

@ -36,7 +36,7 @@ import java.util.stream.Collectors;
public class KeystoreController extends WalletFormController implements Initializable { public class KeystoreController extends WalletFormController implements Initializable {
private static final Logger log = LoggerFactory.getLogger(KeystoreController.class); 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; private Keystore keystore;