diff --git a/src/main/java/com/sparrowwallet/sparrow/terminal/MasterActionListBox.java b/src/main/java/com/sparrowwallet/sparrow/terminal/MasterActionListBox.java index b3cdeb0e..ff3aec06 100644 --- a/src/main/java/com/sparrowwallet/sparrow/terminal/MasterActionListBox.java +++ b/src/main/java/com/sparrowwallet/sparrow/terminal/MasterActionListBox.java @@ -4,6 +4,7 @@ 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.googlecode.lanterna.gui2.dialogs.TextInputDialogBuilder; import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.io.Config; @@ -11,8 +12,8 @@ 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.Bip39Dialog; import com.sparrowwallet.sparrow.terminal.wallet.LoadWallet; -import javafx.application.Platform; import java.io.File; import java.util.Map; @@ -25,34 +26,32 @@ public class MasterActionListBox extends ActionListBox { super(new TerminalSize(14, 3)); addItem("Wallets", () -> { - ActionListDialogBuilder builder = new ActionListDialogBuilder(); - builder.setTitle("Wallets"); - 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); - Storage storage = new Storage(recentWalletFile); + ActionListDialogBuilder builder = new ActionListDialogBuilder(); + builder.setTitle("Wallets"); + 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); + Storage storage = new Storage(recentWalletFile); - Optional 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(storage, 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().getGuiThread().invokeLater(loadWallet); - } - }); - builder.build().showDialog(SparrowTerminal.get().getGui()); + Optional 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) + "*", () -> { + SparrowTerminal.get().getGuiThread().invokeLater(() -> LoadWallet.getOpeningDialog(storage, optWallet.get()).showDialog(SparrowTerminal.get().getGui())); + }); + } else { + builder.addAction(storage.getWalletName(null), new LoadWallet(storage)); + } + } + } + builder.addAction("Open Wallet...", () -> { + SparrowTerminal.get().getGuiThread().invokeLater(MasterActionListBox::openWallet); + }); + builder.addAction("Create Wallet...", () -> { + SparrowTerminal.get().getGuiThread().invokeLater(MasterActionListBox::createWallet); + }); + builder.build().showDialog(SparrowTerminal.get().getGui()); }); addItem("Preferences", () -> { @@ -77,4 +76,36 @@ public class MasterActionListBox extends ActionListBox { addItem("Quit", () -> sparrowTerminal.getGui().getMainWindow().close()); } + + private static void openWallet() { + 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().getGuiThread().invokeLater(loadWallet); + } + } + + private static void createWallet() { + 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); + String walletName = newWalletNameBuilder.build().showDialog(SparrowTerminal.get().getGui()); + + ActionListDialogBuilder newBuilder = new ActionListDialogBuilder(); + newBuilder.setTitle("Create Wallet"); + newBuilder.setDescription("Choose the type of wallet"); + newBuilder.addAction("Software (BIP39)", () -> { + Bip39Dialog bip39Dialog = new Bip39Dialog(walletName); + bip39Dialog.showDialog(SparrowTerminal.get().getGui()); + }); + newBuilder.addAction("Watch Only", () -> { + //OutputDescriptorDialog outputDescriptorDialog = new OutputDescriptorDialog(walletName); + + }); + newBuilder.build().showDialog(SparrowTerminal.get().getGui()); + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/terminal/SparrowTerminal.java b/src/main/java/com/sparrowwallet/sparrow/terminal/SparrowTerminal.java index 05b404c4..d4431bdc 100644 --- a/src/main/java/com/sparrowwallet/sparrow/terminal/SparrowTerminal.java +++ b/src/main/java/com/sparrowwallet/sparrow/terminal/SparrowTerminal.java @@ -9,18 +9,26 @@ 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.SparrowWallet; +import com.sparrowwallet.sparrow.*; +import com.sparrowwallet.sparrow.event.OpenWalletsEvent; +import com.sparrowwallet.sparrow.event.WalletOpenedEvent; +import com.sparrowwallet.sparrow.event.WalletOpeningEvent; +import com.sparrowwallet.sparrow.io.Config; +import com.sparrowwallet.sparrow.io.Storage; import com.sparrowwallet.sparrow.terminal.wallet.WalletData; +import com.sparrowwallet.sparrow.wallet.WalletForm; import javafx.application.Application; import javafx.application.Platform; import javafx.stage.Stage; +import javafx.stage.Window; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.HashMap; -import java.util.Map; +import java.io.File; +import java.util.*; +import java.util.stream.Collectors; + +import static com.sparrowwallet.sparrow.terminal.MasterActionListBox.MAX_RECENT_WALLETS; public class SparrowTerminal extends Application { private static final Logger log = LoggerFactory.getLogger(SparrowTerminal.class); @@ -33,6 +41,8 @@ public class SparrowTerminal extends Application { private final Map walletData = new HashMap<>(); + private static final javafx.stage.Window DEFAULT_WINDOW = new Window() { }; + @Override public void init() throws Exception { Thread.setDefaultUncaughtExceptionHandler((t, e) -> { @@ -101,4 +111,32 @@ public class SparrowTerminal extends Application { public static SparrowTerminal get() { return sparrowTerminal; } + + public static void addWallet(Storage storage, Wallet wallet) { + if(wallet.isNested()) { + WalletData walletData = SparrowTerminal.get().getWalletData().get(storage.getWalletId(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(walletForm.getWalletId(), new WalletData(walletForm)); + + List 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 walletFiles = new LinkedHashSet<>(); + walletFiles.add(storage.getWalletFile()); + if(Config.get().getRecentWalletFiles() != null) { + 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)); + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/Bip39Dialog.java b/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/Bip39Dialog.java new file mode 100644 index 00000000..f61c671f --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/Bip39Dialog.java @@ -0,0 +1,270 @@ +package com.sparrowwallet.sparrow.terminal.wallet; + +import com.googlecode.lanterna.TerminalPosition; +import com.googlecode.lanterna.TerminalSize; +import com.googlecode.lanterna.gui2.*; +import com.googlecode.lanterna.gui2.dialogs.DialogWindow; +import com.sparrowwallet.drongo.policy.Policy; +import com.sparrowwallet.drongo.policy.PolicyType; +import com.sparrowwallet.drongo.protocol.ScriptType; +import com.sparrowwallet.drongo.wallet.DeterministicSeed; +import com.sparrowwallet.drongo.wallet.Keystore; +import com.sparrowwallet.drongo.wallet.Wallet; +import com.sparrowwallet.sparrow.io.Bip39; +import com.sparrowwallet.sparrow.io.ImportException; +import com.sparrowwallet.sparrow.terminal.SparrowTerminal; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class Bip39Dialog extends NewWalletDialog { + private static final Logger log = LoggerFactory.getLogger(Bip39Dialog.class); + public static final int MAX_COLUMNS = 40; + + 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); + + setHints(List.of(Hint.CENTERED)); + + this.walletName = walletName; + + 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("Script type")); + scriptType = new ComboBox<>(); + mainPanel.addComponent(scriptType); + + mainPanel.addComponent(new Label("Seed words")); + seedWords = new TextBox(new TerminalSize(MAX_COLUMNS, 5)); + mainPanel.addComponent(seedWords); + + mainPanel.addComponent(new Label("Passphrase")); + passphrase = new TextBox(new TerminalSize(25, 1)); + mainPanel.addComponent(passphrase); + + Panel buttonPanel = new Panel(); + buttonPanel.setLayoutManager(new GridLayout(2).setHorizontalSpacing(1)); + buttonPanel.addComponent(new Button("Cancel", this::onCancel)); + createWallet = new Button("Create Wallet", this::createWallet).setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.CENTER, GridLayout.Alignment.CENTER, true, false)); + createWallet.setEnabled(false); + buttonPanel.addComponent(createWallet); + + mainPanel.addComponent(new Button("Generate New", () -> generateNew())); + + buttonPanel.setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.END, GridLayout.Alignment.CENTER,false,false)).addTo(mainPanel); + setComponent(mainPanel); + + ScriptType.getAddressableScriptTypes(PolicyType.SINGLE).stream().map(DisplayScriptType::new).forEach(scriptType::addItem); + scriptType.setSelectedItem(new DisplayScriptType(ScriptType.P2WPKH)); + + seedWords.setTextChangeListener((newText, changedByUserInteraction) -> { + try { + String[] words = newText.split("[ \n]"); + importer.getKeystore(scriptType.getSelectedItem().scriptType.getDefaultDerivation(), Arrays.asList(words), passphrase.getText()); + createWallet.setEnabled(true); + } catch(ImportException e) { + createWallet.setEnabled(false); + } + + if(changedByUserInteraction) { + List lines = splitString(newText, MAX_COLUMNS); + String splitText = lines.stream().reduce((s1, s2) -> s1 + "\n" + s2).get(); + if(!newText.equals(splitText)) { + seedWords.setText(splitText); + + TerminalPosition pos = seedWords.getCaretPosition(); + if(pos.getRow() == lines.size() - 2 && pos.getColumn() == lines.get(lines.size() - 2).length()) { + seedWords.setCaretPosition(lines.size() - 1, lines.get(lines.size() - 1).length()); + } + } + } + }); + } + + private void generateNew() { + WordNumberDialog wordNumberDialog = new WordNumberDialog(); + Integer numberOfWords = wordNumberDialog.showDialog(SparrowTerminal.get().getGui()); + + if(numberOfWords != null) { + int mnemonicSeedLength = numberOfWords * 11; + int entropyLength = mnemonicSeedLength - (mnemonicSeedLength/33); + + SecureRandom secureRandom; + try { + secureRandom = SecureRandom.getInstanceStrong(); + } catch(NoSuchAlgorithmException e) { + secureRandom = new SecureRandom(); + } + + DeterministicSeed deterministicSeed = new DeterministicSeed(secureRandom, entropyLength, ""); + List words = deterministicSeed.getMnemonicCode(); + setWords(words); + } + } + + private List getWords() { + return List.of(seedWords.getText().split("[ \n]")); + } + + private void setWords(List words) { + String text = words.stream().reduce((s1, s2) -> s1 + " " + s2).get(); + List splitText = splitString(text, MAX_COLUMNS); + seedWords.setText(splitText.stream().reduce((s1, s2) -> s1 + "\n" + s2).get()); + } + + public static List splitString(String stringToSplit, int maxLength) { + int splitLength = maxLength - 1; + String text = stringToSplit; + List lines = new ArrayList<>(); + while (text.length() > splitLength) { + int spaceAt = splitLength - 1; + // the text is too long. + // find the last space before the maxLength + for (int i = splitLength - 1; i > 0; i--) { + if (Character.isWhitespace(text.charAt(i))) { + spaceAt = i; + break; + } + } + lines.add(text.substring(0, spaceAt)); + text = text.substring(spaceAt + 1); + } + lines.add(text); + 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 { + 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; + } + + private static final class DisplayScriptType { + private final ScriptType scriptType; + + public DisplayScriptType(ScriptType scriptType) { + this.scriptType = scriptType; + } + + @Override + public String toString() { + return scriptType.getDescription(); + } + + @Override + public boolean equals(Object o) { + if(this == o) { + return true; + } + if(o == null || getClass() != o.getClass()) { + return false; + } + + DisplayScriptType that = (DisplayScriptType) o; + + return scriptType == that.scriptType; + } + + @Override + public int hashCode() { + return scriptType.hashCode(); + } + } + + private static final class WordNumberDialog extends DialogWindow { + ComboBox wordCount; + private Integer numberOfWords; + + public WordNumberDialog() { + super("Generate Seed Words"); + + setHints(List.of(Hint.CENTERED)); + + 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("Number of words")); + wordCount = new ComboBox<>(); + mainPanel.addComponent(wordCount); + + wordCount.addItem(24); + wordCount.addItem(21); + wordCount.addItem(18); + wordCount.addItem(15); + wordCount.addItem(12); + + Panel buttonPanel = new Panel(); + buttonPanel.setLayoutManager(new GridLayout(2).setHorizontalSpacing(1)); + buttonPanel.addComponent(new Button("Cancel", this::onCancel)); + Button okButton = new Button("Ok", this::onOk).setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.CENTER, GridLayout.Alignment.CENTER, true, false)); + buttonPanel.addComponent(okButton); + + mainPanel.addComponent(new EmptySpace(TerminalSize.ONE)); + + buttonPanel.setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.END, GridLayout.Alignment.CENTER,false,false)).addTo(mainPanel); + setComponent(mainPanel); + } + + private void onOk() { + numberOfWords = wordCount.getSelectedItem(); + close(); + } + + private void onCancel() { + close(); + } + + @Override + public Integer showDialog(WindowBasedTextGUI textGUI) { + super.showDialog(textGUI); + return numberOfWords; + } + } +} 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 38dfb0ef..4f6efbde 100644 --- a/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/LoadWallet.java +++ b/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/LoadWallet.java @@ -8,28 +8,20 @@ import com.sparrowwallet.drongo.crypto.InvalidPasswordException; import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.SparrowWallet; -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); @@ -113,7 +105,7 @@ public class LoadWallet implements Runnable { if(!walletAndKey.getWallet().isValid()) { throw new IllegalStateException("Wallet file is not valid."); } - addWallet(storage, walletAndKey.getWallet()); + SparrowTerminal.addWallet(storage, walletAndKey.getWallet()); for(Map.Entry entry : walletAndKey.getChildWallets().entrySet()) { openWallet(entry.getValue(), entry.getKey()); } @@ -131,34 +123,6 @@ public class LoadWallet implements Runnable { } } - private void addWallet(Storage storage, Wallet wallet) { - if(wallet.isNested()) { - WalletData walletData = SparrowTerminal.get().getWalletData().get(storage.getWalletId(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(walletForm.getWalletId(), new WalletData(walletForm)); - - List 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 walletFiles = new LinkedHashSet<>(); - walletFiles.add(storage.getWalletFile()); - if(Config.get().getRecentWalletFiles() != null) { - 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(Storage storage, Wallet masterWallet) { if(masterWallet.getChildWallets().stream().anyMatch(childWallet -> !childWallet.isNested())) { return new WalletAccountsDialog(storage.getWalletId(masterWallet)); @@ -167,8 +131,6 @@ public class LoadWallet implements Runnable { } } - 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)); diff --git a/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/NewWalletDialog.java b/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/NewWalletDialog.java new file mode 100644 index 00000000..24602cdb --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/NewWalletDialog.java @@ -0,0 +1,163 @@ +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.dialogs.DialogWindow; +import com.googlecode.lanterna.gui2.dialogs.TextInputDialogBuilder; +import com.sparrowwallet.drongo.SecureString; +import com.sparrowwallet.drongo.crypto.ECKey; +import com.sparrowwallet.drongo.crypto.EncryptionType; +import com.sparrowwallet.drongo.crypto.Key; +import com.sparrowwallet.drongo.wallet.MnemonicException; +import com.sparrowwallet.drongo.wallet.Wallet; +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.Storage; +import com.sparrowwallet.sparrow.io.StorageException; +import com.sparrowwallet.sparrow.net.ElectrumServer; +import com.sparrowwallet.sparrow.terminal.SparrowTerminal; +import javafx.application.Platform; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.List; + +import static com.sparrowwallet.sparrow.AppServices.showErrorDialog; + +public class NewWalletDialog extends DialogWindow { + private static final Logger log = LoggerFactory.getLogger(NewWalletDialog.class); + + public NewWalletDialog(String title) { + super(title); + } + + protected void discoverAndSaveWallet(Wallet wallet) { + if(AppServices.onlineProperty().get()) { + discoverAccounts(wallet); + } else { + saveWallet(wallet); + } + } + + private void discoverAccounts(Wallet wallet) { + DiscoveringDialog discoveringDialog = new DiscoveringDialog(wallet); + SparrowTerminal.get().getGui().addWindow(discoveringDialog); + + Platform.runLater(() -> { + ElectrumServer.WalletDiscoveryService walletDiscoveryService = new ElectrumServer.WalletDiscoveryService(List.of(wallet)); + walletDiscoveryService.setOnSucceeded(successEvent -> { + SparrowTerminal.get().getGuiThread().invokeLater(() -> { + SparrowTerminal.get().getGui().removeWindow(discoveringDialog); + saveWallet(wallet); + }); + }); + walletDiscoveryService.setOnFailed(failedEvent -> { + SparrowTerminal.get().getGuiThread().invokeLater(() -> { + SparrowTerminal.get().getGui().removeWindow(discoveringDialog); + saveWallet(wallet); + }); + log.error("Failed to discover accounts", failedEvent.getSource().getException()); + }); + walletDiscoveryService.start(); + }); + } + + private void saveWallet(Wallet wallet) { + Storage storage = new Storage(Storage.getWalletFile(wallet.getName())); + + TextInputDialogBuilder builder = new TextInputDialogBuilder().setTitle("Wallet Password"); + builder.setDescription(SettingsDialog.PasswordRequirement.UPDATE_NEW.getDescription()); + builder.setPasswordInput(true); + + String password = builder.build().showDialog(SparrowTerminal.get().getGui()); + if(password != null) { + Platform.runLater(() -> { + if(password.length() == 0) { + try { + storage.setEncryptionPubKey(Storage.NO_PASSWORD_KEY); + storage.saveWallet(wallet); + storage.restorePublicKeysFromSeed(wallet, null); + SparrowTerminal.addWallet(storage, wallet); + + for(Wallet childWallet : wallet.getChildWallets()) { + storage.saveWallet(childWallet); + storage.restorePublicKeysFromSeed(childWallet, null); + SparrowTerminal.addWallet(storage, childWallet); + } + + SparrowTerminal.get().getGuiThread().invokeLater(() -> LoadWallet.getOpeningDialog(storage, wallet).showDialog(SparrowTerminal.get().getGui())); + } catch(IOException | StorageException | MnemonicException e) { + log.error("Error saving imported wallet", e); + } + } else { + Storage.KeyDerivationService keyDerivationService = new Storage.KeyDerivationService(storage, new SecureString(password)); + keyDerivationService.setOnSucceeded(workerStateEvent -> { + EventManager.get().post(new StorageEvent(Storage.getWalletFile(wallet.getName()).getAbsolutePath(), TimedEvent.Action.END, "Done")); + ECKey encryptionFullKey = keyDerivationService.getValue(); + Key key = null; + + try { + ECKey encryptionPubKey = ECKey.fromPublicOnly(encryptionFullKey); + key = new Key(encryptionFullKey.getPrivKeyBytes(), storage.getKeyDeriver().getSalt(), EncryptionType.Deriver.ARGON2); + wallet.encrypt(key); + storage.setEncryptionPubKey(encryptionPubKey); + storage.saveWallet(wallet); + storage.restorePublicKeysFromSeed(wallet, key); + SparrowTerminal.addWallet(storage, wallet); + + for(Wallet childWallet : wallet.getChildWallets()) { + if(!childWallet.isNested()) { + childWallet.encrypt(key); + } + storage.saveWallet(childWallet); + storage.restorePublicKeysFromSeed(childWallet, key); + SparrowTerminal.addWallet(storage, childWallet); + } + + SparrowTerminal.get().getGuiThread().invokeLater(() -> LoadWallet.getOpeningDialog(storage, wallet).showDialog(SparrowTerminal.get().getGui())); + } catch(IOException | StorageException | MnemonicException e) { + log.error("Error saving imported wallet", e); + } finally { + encryptionFullKey.clear(); + if(key != null) { + key.clear(); + } + } + }); + keyDerivationService.setOnFailed(workerStateEvent -> { + EventManager.get().post(new StorageEvent(Storage.getWalletFile(wallet.getName()).getAbsolutePath(), TimedEvent.Action.END, "Failed")); + showErrorDialog("Error encrypting wallet", keyDerivationService.getException().getMessage()); + }); + EventManager.get().post(new StorageEvent(Storage.getWalletFile(wallet.getName()).getAbsolutePath(), TimedEvent.Action.START, "Encrypting wallet...")); + keyDerivationService.start(); + } + }); + } + } + + 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 26b1260c..9bd15a7d 100644 --- a/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/SettingsDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/SettingsDialog.java @@ -204,5 +204,9 @@ public class SettingsDialog extends WalletDialog { this.description = description; this.okButtonText = okButtonText; } + + public String getDescription() { + return description; + } } }