diff --git a/src/main/java/com/sparrowwallet/sparrow/terminal/SparrowTextGui.java b/src/main/java/com/sparrowwallet/sparrow/terminal/SparrowTextGui.java index a6acb56c..de3be136 100644 --- a/src/main/java/com/sparrowwallet/sparrow/terminal/SparrowTextGui.java +++ b/src/main/java/com/sparrowwallet/sparrow/terminal/SparrowTextGui.java @@ -5,6 +5,7 @@ import com.googlecode.lanterna.TerminalSize; import com.googlecode.lanterna.TextColor; import com.googlecode.lanterna.gui2.*; import com.googlecode.lanterna.screen.Screen; +import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.event.*; import com.sparrowwallet.sparrow.io.Config; @@ -128,6 +129,7 @@ public class SparrowTextGui extends MultiWindowTextGUI { } else if(event.getTimeMills() < 0) { getGUIThread().invokeLater(() -> { statusLabel.setText(event.getStatus()); + statusProgress.setValue(0); }); } else { getGUIThread().invokeLater(() -> { @@ -138,6 +140,7 @@ public class SparrowTextGui extends MultiWindowTextGUI { new KeyFrame(Duration.millis(event.getTimeMills()), e -> { getGUIThread().invokeLater(() -> { statusLabel.setText(""); + statusProgress.setValue(0); }); }, new KeyValue(progressProperty, 1)) ); @@ -163,4 +166,13 @@ public class SparrowTextGui extends MultiWindowTextGUI { walletHistoryFinished(new WalletHistoryFinishedEvent(event.getWallet())); statusUpdated(new StatusEvent("Error retrieving wallet history" + (Config.get().getServerType() == ServerType.PUBLIC_ELECTRUM_SERVER ? ", trying another server..." : ""))); } + + @Subscribe + public void childWalletsAdded(ChildWalletsAddedEvent event) { + if(!event.getChildWallets().isEmpty()) { + for(Wallet childWallet : event.getChildWallets()) { + SparrowTerminal.addWallet(event.getStorage(), childWallet); + } + } + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/terminal/TerminalInteractionServices.java b/src/main/java/com/sparrowwallet/sparrow/terminal/TerminalInteractionServices.java index 30fc87a3..f8c35c43 100644 --- a/src/main/java/com/sparrowwallet/sparrow/terminal/TerminalInteractionServices.java +++ b/src/main/java/com/sparrowwallet/sparrow/terminal/TerminalInteractionServices.java @@ -36,7 +36,7 @@ public class TerminalInteractionServices implements InteractionServices { private Optional showMessageDialog(String title, String content, ButtonType[] buttons) { String formattedContent = formatLines(content, 50); - MessageDialogBuilder builder = new MessageDialogBuilder().setTitle(title).setText(formattedContent); + MessageDialogBuilder builder = new MessageDialogBuilder().setTitle(title).setText("\n" + formattedContent); for(ButtonType buttonType : buttons) { builder.addButton(getButton(buttonType)); } diff --git a/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/AddAccountDialog.java b/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/AddAccountDialog.java new file mode 100644 index 00000000..a2db2544 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/AddAccountDialog.java @@ -0,0 +1,97 @@ +package com.sparrowwallet.sparrow.terminal.wallet; + +import com.googlecode.lanterna.TerminalSize; +import com.googlecode.lanterna.gui2.*; +import com.googlecode.lanterna.gui2.dialogs.DialogWindow; +import com.sparrowwallet.drongo.wallet.StandardAccount; +import com.sparrowwallet.drongo.wallet.Wallet; +import com.sparrowwallet.sparrow.whirlpool.WhirlpoolServices; + +import java.util.ArrayList; +import java.util.List; + +final class AddAccountDialog extends DialogWindow { + private ComboBox standardAccounts; + private StandardAccount standardAccount; + + public AddAccountDialog(Wallet wallet) { + super("Add Account"); + + 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("Account to add")); + standardAccounts = new ComboBox<>(); + mainPanel.addComponent(standardAccounts); + + List existingIndexes = new ArrayList<>(); + Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet(); + existingIndexes.add(masterWallet.getAccountIndex()); + for(Wallet childWallet : masterWallet.getChildWallets()) { + if(!childWallet.isNested()) { + existingIndexes.add(childWallet.getAccountIndex()); + } + } + + List availableAccounts = new ArrayList<>(); + for(StandardAccount standardAccount : StandardAccount.values()) { + if(!existingIndexes.contains(standardAccount.getAccountNumber()) && !StandardAccount.WHIRLPOOL_ACCOUNTS.contains(standardAccount)) { + availableAccounts.add(standardAccount); + } + } + + if(WhirlpoolServices.canWalletMix(masterWallet) && !masterWallet.isWhirlpoolMasterWallet()) { + availableAccounts.add(StandardAccount.WHIRLPOOL_PREMIX); + } + + availableAccounts.stream().map(DisplayStandardAccount::new).forEach(standardAccounts::addItem); + + Panel buttonPanel = new Panel(); + buttonPanel.setLayoutManager(new GridLayout(2).setHorizontalSpacing(1)); + buttonPanel.addComponent(new Button("Cancel", this::onCancel)); + Button okButton = new Button("Add Account", this::addAccount).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 addAccount() { + standardAccount = standardAccounts.getSelectedItem().account; + close(); + } + + private void onCancel() { + close(); + } + + @Override + public StandardAccount showDialog(WindowBasedTextGUI textGUI) { + super.showDialog(textGUI); + return standardAccount; + } + + private static class DisplayStandardAccount { + private final StandardAccount account; + + public DisplayStandardAccount(StandardAccount standardAccount) { + this.account = standardAccount; + } + + @Override + public String toString() { + if(StandardAccount.WHIRLPOOL_ACCOUNTS.contains(account)) { + return "Whirlpool Accounts"; + } + + return account.getName(); + } + } +} 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 66355ee8..17e6945c 100644 --- a/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/Bip39Dialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/Bip39Dialog.java @@ -191,7 +191,7 @@ public class Bip39Dialog extends NewWalletDialog { } private static final class WordNumberDialog extends DialogWindow { - ComboBox wordCount; + private final ComboBox wordCount; private Integer numberOfWords; public WordNumberDialog() { 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 bc8adab5..52f92d68 100644 --- a/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/LoadWallet.java +++ b/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/LoadWallet.java @@ -63,6 +63,7 @@ public class LoadWallet implements Runnable { String password = builder.build().showDialog(SparrowTerminal.get().getGui()); if(password == null) { + SparrowTerminal.get().getGui().removeWindow(loadingDialog); return; } 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 1f7304d6..5d3fd1e4 100644 --- a/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/SettingsDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/SettingsDialog.java @@ -8,25 +8,29 @@ import com.sparrowwallet.drongo.OutputDescriptor; import com.sparrowwallet.drongo.SecureString; import com.sparrowwallet.drongo.crypto.ECKey; import com.sparrowwallet.drongo.crypto.EncryptionType; +import com.sparrowwallet.drongo.crypto.InvalidPasswordException; import com.sparrowwallet.drongo.crypto.Key; +import com.sparrowwallet.drongo.wallet.KeystoreSource; +import com.sparrowwallet.drongo.wallet.StandardAccount; import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.EventManager; -import com.sparrowwallet.sparrow.event.RequestOpenWalletsEvent; -import com.sparrowwallet.sparrow.event.StorageEvent; -import com.sparrowwallet.sparrow.event.TimedEvent; +import com.sparrowwallet.sparrow.event.*; import com.sparrowwallet.sparrow.io.Storage; import com.sparrowwallet.sparrow.io.StorageException; import com.sparrowwallet.sparrow.terminal.SparrowTerminal; import com.sparrowwallet.sparrow.wallet.Function; import com.sparrowwallet.sparrow.wallet.WalletForm; +import com.sparrowwallet.sparrow.whirlpool.WhirlpoolServices; import javafx.application.Platform; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; -import java.util.ArrayList; -import java.util.List; +import java.util.*; + +import static com.sparrowwallet.sparrow.AppServices.showErrorDialog; +import static com.sparrowwallet.sparrow.AppServices.showSuccessDialog; public class SettingsDialog extends WalletDialog { private static final Logger log = LoggerFactory.getLogger(SettingsDialog.class); @@ -70,7 +74,11 @@ public class SettingsDialog extends WalletDialog { buttonPanel.addComponent(new Button("Back", () -> onBack(Function.SETTINGS))); buttonPanel.addComponent(new Button("Advanced", this::showAdvanced).setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.CENTER, GridLayout.Alignment.CENTER, true, false))); - mainPanel.addComponent(new EmptySpace(TerminalSize.ONE)); + if(getWalletForm().getMasterWallet().getKeystores().stream().allMatch(ks -> ks.getSource() == KeystoreSource.SW_SEED)) { + mainPanel.addComponent(new Button("Add Account", this::showAddAccount)); + } else { + mainPanel.addComponent(new EmptySpace(TerminalSize.ONE)); + } buttonPanel.setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.END, GridLayout.Alignment.CENTER,false,false)).addTo(mainPanel); setComponent(mainPanel); @@ -85,6 +93,18 @@ public class SettingsDialog extends WalletDialog { } } + private void showAddAccount() { + Wallet openWallet = AppServices.get().getOpenWallets().entrySet().stream().filter(entry -> getWalletForm().getWalletFile().equals(entry.getValue().getWalletFile())).map(Map.Entry::getKey).findFirst().orElseThrow(); + Wallet masterWallet = openWallet.isMasterWallet() ? openWallet : openWallet.getMasterWallet(); + + AddAccountDialog addAccountDialog = new AddAccountDialog(masterWallet); + StandardAccount standardAccount = addAccountDialog.showDialog(SparrowTerminal.get().getGui()); + + if(standardAccount != null) { + addAccount(masterWallet, standardAccount); + } + } + private void saveWallet(boolean changePassword, boolean suggestChangePassword) { WalletForm walletForm = getWalletForm(); ECKey existingPubKey = walletForm.getStorage().getEncryptionPubKey(); @@ -178,6 +198,86 @@ public class SettingsDialog extends WalletDialog { } } + private void addAccount(Wallet masterWallet, StandardAccount standardAccount) { + if(masterWallet.isEncrypted()) { + String walletId = getWalletForm().getWalletId(); + + TextInputDialogBuilder builder = new TextInputDialogBuilder().setTitle("Wallet Password"); + builder.setDescription("Enter the wallet password:"); + builder.setPasswordInput(true); + + String password = builder.build().showDialog(SparrowTerminal.get().getGui()); + if(password != null) { + Platform.runLater(() -> { + Storage.KeyDerivationService keyDerivationService = new Storage.KeyDerivationService(getWalletForm().getStorage(), new SecureString(password), true); + keyDerivationService.setOnSucceeded(workerStateEvent -> { + EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Done")); + ECKey encryptionFullKey = keyDerivationService.getValue(); + Key key = new Key(encryptionFullKey.getPrivKeyBytes(), getWalletForm().getStorage().getKeyDeriver().getSalt(), EncryptionType.Deriver.ARGON2); + encryptionFullKey.clear(); + masterWallet.decrypt(key); + addAndEncryptAccount(masterWallet, standardAccount, key); + }); + keyDerivationService.setOnFailed(workerStateEvent -> { + EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Failed")); + if(keyDerivationService.getException() instanceof InvalidPasswordException) { + showErrorDialog("Invalid Password", "The wallet password was invalid."); + } else { + log.error("Error deriving wallet key", keyDerivationService.getException()); + } + }); + EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.START, "Decrypting wallet...")); + keyDerivationService.start(); + }); + } + } else { + Platform.runLater(() -> addAndSaveAccount(masterWallet, standardAccount)); + } + } + + private void addAndEncryptAccount(Wallet masterWallet, StandardAccount standardAccount, Key key) { + try { + addAndSaveAccount(masterWallet, standardAccount); + } finally { + masterWallet.encrypt(key); + for(Wallet childWallet : masterWallet.getChildWallets()) { + if(!childWallet.isNested() && !childWallet.isEncrypted()) { + childWallet.encrypt(key); + } + } + key.clear(); + } + } + + private void addAndSaveAccount(Wallet masterWallet, StandardAccount standardAccount) { + if(StandardAccount.WHIRLPOOL_ACCOUNTS.contains(standardAccount)) { + WhirlpoolServices.prepareWhirlpoolWallet(masterWallet, getWalletForm().getWalletId(), getWalletForm().getStorage()); + SparrowTerminal.get().getGuiThread().invokeLater(() -> showSuccessDialog("Added Accounts", "Whirlpool Accounts have been successfully added.")); + } else { + Wallet childWallet = masterWallet.addChildWallet(standardAccount); + EventManager.get().post(new ChildWalletsAddedEvent(getWalletForm().getStorage(), masterWallet, childWallet)); + SparrowTerminal.get().getGuiThread().invokeLater(() -> showSuccessDialog("Added Account", standardAccount.getName() + " has been successfully added.")); + } + + saveChildWallets(masterWallet); + } + + private void saveChildWallets(Wallet masterWallet) { + for(Wallet childWallet : masterWallet.getChildWallets()) { + if(!childWallet.isNested()) { + Storage storage = getWalletForm().getStorage(); + if(!storage.isPersisted(childWallet)) { + try { + storage.saveWallet(childWallet); + } catch(Exception e) { + log.error("Error saving wallet", e); + showErrorDialog("Error saving wallet " + childWallet.getName(), e.getMessage()); + } + } + } + } + } + public static List splitString(String stringToSplit, int maxLength) { String text = stringToSplit; List lines = new ArrayList<>(); diff --git a/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/WalletAccountsDialog.java b/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/WalletAccountsDialog.java index f8fee1fb..8ee8e1c6 100644 --- a/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/WalletAccountsDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/WalletAccountsDialog.java @@ -8,6 +8,8 @@ import com.sparrowwallet.sparrow.io.Storage; import com.sparrowwallet.sparrow.terminal.SparrowTerminal; import com.sparrowwallet.sparrow.wallet.WalletForm; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; public class WalletAccountsDialog extends DialogWindow { @@ -24,7 +26,9 @@ public class WalletAccountsDialog extends DialogWindow { actions = new ActionListBox(); - for(Wallet wallet : masterWallet.getAllWallets()) { + List allWallets = new ArrayList<>(masterWallet.getAllWallets()); + Collections.sort(allWallets); + for(Wallet wallet : allWallets) { actions.addItem(wallet.getDisplayName(), () -> { close(); SparrowTerminal.get().getGuiThread().invokeLater(() -> {