diff --git a/src/main/java/com/sparrowwallet/sparrow/AppServices.java b/src/main/java/com/sparrowwallet/sparrow/AppServices.java index c5c55c16..9ab973b7 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppServices.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppServices.java @@ -622,7 +622,7 @@ public class AppServices { } public static Optional showErrorDialog(String title, String content, ButtonType... buttons) { - return showAlertDialog(title, content, Alert.AlertType.ERROR, buttons); + return showAlertDialog(title, content == null ? "See log file (Help menu)" : content, Alert.AlertType.ERROR, buttons); } public static Optional showAlertDialog(String title, String content, Alert.AlertType alertType, ButtonType... buttons) { diff --git a/src/main/java/com/sparrowwallet/sparrow/control/AddAccountDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/AddAccountDialog.java index cb297090..0632fd4c 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/AddAccountDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/AddAccountDialog.java @@ -1,5 +1,6 @@ package com.sparrowwallet.sparrow.control; +import com.sparrowwallet.drongo.wallet.KeystoreSource; import com.sparrowwallet.drongo.wallet.StandardAccount; import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.sparrow.AppServices; @@ -14,8 +15,9 @@ import org.controlsfx.glyphfont.Glyph; import java.util.ArrayList; import java.util.List; -public class AddAccountDialog extends Dialog { +public class AddAccountDialog extends Dialog> { private final ComboBox standardAccountCombo; + private boolean discoverAccounts = false; public AddAccountDialog(Wallet wallet) { final DialogPane dialogPane = getDialogPane(); @@ -56,6 +58,16 @@ public class AddAccountDialog extends Dialog { availableAccounts.add(StandardAccount.WHIRLPOOL_PREMIX); } + final ButtonType discoverButtonType = new javafx.scene.control.ButtonType("Discover", ButtonBar.ButtonData.LEFT); + if(!availableAccounts.isEmpty() && masterWallet.getKeystores().stream().allMatch(ks -> ks.getSource() == KeystoreSource.SW_SEED)) { + dialogPane.getButtonTypes().add(discoverButtonType); + Button discoverButton = (Button)dialogPane.lookupButton(discoverButtonType); + discoverButton.disableProperty().bind(AppServices.onlineProperty().not()); + discoverButton.setOnAction(event -> { + discoverAccounts = true; + }); + } + standardAccountCombo.setItems(FXCollections.observableList(availableAccounts)); standardAccountCombo.setConverter(new StringConverter<>() { @Override @@ -86,6 +98,10 @@ public class AddAccountDialog extends Dialog { content.getChildren().add(standardAccountCombo); dialogPane.setContent(content); - setResultConverter(dialogButton -> dialogButton == ButtonType.OK ? standardAccountCombo.getValue() : null); + setResultConverter(dialogButton -> dialogButton == ButtonType.OK ? List.of(standardAccountCombo.getValue()) : (dialogButton == discoverButtonType ? availableAccounts : null)); + } + + public boolean isDiscoverAccounts() { + return discoverAccounts; } } diff --git a/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java b/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java index 841fe01a..967822c5 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java @@ -400,7 +400,7 @@ public class TransactionDiagram extends GridPane { WalletNode toNode = walletTx.getWallet() != null ? walletTx.getWallet().getWalletAddresses().get(payment.getAddress()) : null; Tooltip recipientTooltip = new Tooltip((toWallet == null ? (toNode != null ? "Consolidate " : "Pay ") : "Receive ") + getSatsValue(payment.getAmount()) + " sats to " - + (payment instanceof AdditionalPayment ? "\n" + payment : (toWallet == null ? (payment.getLabel() == null ? (toNode != null ? toNode : "external address") : payment.getLabel()) : toWallet.getName()) + "\n" + payment.getAddress().toString())); + + (payment instanceof AdditionalPayment ? "\n" + payment : (toWallet == null ? (payment.getLabel() == null ? (toNode != null ? toNode : "external address") : payment.getLabel()) : toWallet.getFullName()) + "\n" + payment.getAddress().toString())); recipientTooltip.getStyleClass().add("recipient-label"); recipientTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY)); recipientLabel.setTooltip(recipientTooltip); diff --git a/src/main/java/com/sparrowwallet/sparrow/io/db/BlockTransactionDao.java b/src/main/java/com/sparrowwallet/sparrow/io/db/BlockTransactionDao.java index efb01490..2c707560 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/db/BlockTransactionDao.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/db/BlockTransactionDao.java @@ -10,6 +10,7 @@ import org.jdbi.v3.sqlobject.statement.SqlQuery; import org.jdbi.v3.sqlobject.statement.SqlUpdate; import java.util.Date; +import java.util.HashMap; import java.util.Map; public interface BlockTransactionDao { @@ -35,7 +36,8 @@ public interface BlockTransactionDao { void clear(long wallet); default void addBlockTransactions(Wallet wallet) { - for(Map.Entry blkTxEntry : wallet.getTransactions().entrySet()) { + Map walletTransactions = new HashMap<>(wallet.getTransactions()); + for(Map.Entry blkTxEntry : walletTransactions.entrySet()) { blkTxEntry.getValue().setId(null); addOrUpdate(wallet, blkTxEntry.getKey(), blkTxEntry.getValue()); } diff --git a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java index 6b52dfe7..12c292fe 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java @@ -1415,4 +1415,35 @@ public class ElectrumServer { }; } } + + public static class WalletDiscoveryService extends Service> { + private final Wallet masterWalletCopy; + private final List standardAccounts; + + public WalletDiscoveryService(Wallet masterWallet, List standardAccounts) { + this.masterWalletCopy = masterWallet.copy(); + this.standardAccounts = standardAccounts; + } + + @Override + protected Task> createTask() { + return new Task<>() { + protected List call() throws ServerException { + ElectrumServer electrumServer = new ElectrumServer(); + List discoveredAccounts = new ArrayList<>(); + + for(StandardAccount standardAccount : standardAccounts) { + Wallet wallet = masterWalletCopy.addChildWallet(standardAccount); + Map> nodeTransactionMap = new TreeMap<>(); + electrumServer.getReferences(wallet, wallet.getNode(KeyPurpose.RECEIVE).getChildren(), nodeTransactionMap, 0); + if(nodeTransactionMap.values().stream().anyMatch(blockTransactionHashes -> !blockTransactionHashes.isEmpty())) { + discoveredAccounts.add(standardAccount); + } + } + + return discoveredAccounts; + } + }; + } + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsController.java index c2b048f7..b968336c 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsController.java @@ -15,6 +15,7 @@ import com.sparrowwallet.sparrow.control.*; import com.sparrowwallet.sparrow.event.*; import com.sparrowwallet.sparrow.io.Storage; import com.sparrowwallet.sparrow.io.StorageException; +import com.sparrowwallet.sparrow.net.ElectrumServer; import com.sparrowwallet.sparrow.whirlpool.WhirlpoolServices; import javafx.application.Platform; import javafx.beans.property.SimpleIntegerProperty; @@ -452,61 +453,104 @@ public class SettingsController extends WalletFormController implements Initiali Wallet masterWallet = openWallet.isMasterWallet() ? openWallet : openWallet.getMasterWallet(); AddAccountDialog addAccountDialog = new AddAccountDialog(masterWallet); - Optional optAccount = addAccountDialog.showAndWait(); - if(optAccount.isPresent()) { - StandardAccount standardAccount = optAccount.get(); + Optional> optAccounts = addAccountDialog.showAndWait(); + if(optAccounts.isPresent()) { + List standardAccounts = optAccounts.get(); + if(addAccountDialog.isDiscoverAccounts() && !AppServices.isConnected()) { + return; + } - if(masterWallet.getKeystores().stream().allMatch(ks -> ks.getSource() == KeystoreSource.SW_SEED)) { - if(masterWallet.isEncrypted()) { - String walletId = walletForm.getWalletId(); - WalletPasswordDialog dlg = new WalletPasswordDialog(masterWallet.getName(), WalletPasswordDialog.PasswordRequirement.LOAD); - Optional password = dlg.showAndWait(); - if(password.isPresent()) { - Storage.KeyDerivationService keyDerivationService = new Storage.KeyDerivationService(walletForm.getStorage(), password.get(), true); - keyDerivationService.setOnSucceeded(workerStateEvent -> { - EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Done")); - ECKey encryptionFullKey = keyDerivationService.getValue(); - Key key = new Key(encryptionFullKey.getPrivKeyBytes(), walletForm.getStorage().getKeyDeriver().getSalt(), EncryptionType.Deriver.ARGON2); - masterWallet.decrypt(key); + addAccounts(masterWallet, standardAccounts, addAccountDialog.isDiscoverAccounts()); + } + } - try { - addAndSaveAccount(masterWallet, standardAccount); - } finally { - masterWallet.encrypt(key); - for(Wallet childWallet : masterWallet.getChildWallets()) { - if(!childWallet.isEncrypted()) { - childWallet.encrypt(key); - } - } - key.clear(); - encryptionFullKey.clear(); - password.get().clear(); + private void addAccounts(Wallet masterWallet, List standardAccounts, boolean discoverAccounts) { + if(masterWallet.getKeystores().stream().allMatch(ks -> ks.getSource() == KeystoreSource.SW_SEED)) { + if(masterWallet.isEncrypted()) { + String walletId = walletForm.getWalletId(); + WalletPasswordDialog dlg = new WalletPasswordDialog(masterWallet.getName(), WalletPasswordDialog.PasswordRequirement.LOAD); + Optional password = dlg.showAndWait(); + if(password.isPresent()) { + Storage.KeyDerivationService keyDerivationService = new Storage.KeyDerivationService(walletForm.getStorage(), password.get(), true); + keyDerivationService.setOnSucceeded(workerStateEvent -> { + EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Done")); + ECKey encryptionFullKey = keyDerivationService.getValue(); + Key key = new Key(encryptionFullKey.getPrivKeyBytes(), walletForm.getStorage().getKeyDeriver().getSalt(), EncryptionType.Deriver.ARGON2); + encryptionFullKey.clear(); + masterWallet.decrypt(key); + + if(discoverAccounts) { + ElectrumServer.WalletDiscoveryService walletDiscoveryService = new ElectrumServer.WalletDiscoveryService(masterWallet, standardAccounts); + walletDiscoveryService.setOnSucceeded(event -> { + addAndEncryptAccounts(masterWallet, walletDiscoveryService.getValue(), key); + }); + walletDiscoveryService.setOnFailed(event -> { + log.error("Failed to discover accounts", event.getSource().getException()); + addAndEncryptAccounts(masterWallet, Collections.emptyList(), key); + AppServices.showErrorDialog("Failed to discover accounts", event.getSource().getException().getMessage()); + }); + walletDiscoveryService.start(); + } else { + addAndEncryptAccounts(masterWallet, standardAccounts, key); + } + }); + keyDerivationService.setOnFailed(workerStateEvent -> { + EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Failed")); + if(keyDerivationService.getException() instanceof InvalidPasswordException) { + Optional optResponse = showErrorDialog("Invalid Password", "The wallet password was invalid. Try again?", ButtonType.CANCEL, ButtonType.OK); + if(optResponse.isPresent() && optResponse.get().equals(ButtonType.OK)) { + Platform.runLater(() -> addAccount(null)); } - }); - keyDerivationService.setOnFailed(workerStateEvent -> { - EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Failed")); - if(keyDerivationService.getException() instanceof InvalidPasswordException) { - Optional optResponse = showErrorDialog("Invalid Password", "The wallet password was invalid. Try again?", ButtonType.CANCEL, ButtonType.OK); - if(optResponse.isPresent() && optResponse.get().equals(ButtonType.OK)) { - Platform.runLater(() -> addAccount(null)); - } - } else { - log.error("Error deriving wallet key", keyDerivationService.getException()); - } - }); - EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.START, "Decrypting wallet...")); - keyDerivationService.start(); - } - } else { - addAndSaveAccount(masterWallet, standardAccount); + } else { + log.error("Error deriving wallet key", keyDerivationService.getException()); + } + }); + EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.START, "Decrypting wallet...")); + keyDerivationService.start(); } } else { + if(discoverAccounts) { + ElectrumServer.WalletDiscoveryService walletDiscoveryService = new ElectrumServer.WalletDiscoveryService(masterWallet, standardAccounts); + walletDiscoveryService.setOnSucceeded(event -> { + addAndSaveAccounts(masterWallet, walletDiscoveryService.getValue()); + }); + walletDiscoveryService.setOnFailed(event -> { + log.error("Failed to discover accounts", event.getSource().getException()); + AppServices.showErrorDialog("Failed to discover accounts", event.getSource().getException().getMessage()); + }); + walletDiscoveryService.start(); + } else { + addAndSaveAccounts(masterWallet, standardAccounts); + } + } + } else { + for(StandardAccount standardAccount : standardAccounts) { Wallet childWallet = masterWallet.addChildWallet(standardAccount); EventManager.get().post(new ChildWalletAddedEvent(getWalletForm().getStorage(), masterWallet, childWallet)); } } } + private void addAndEncryptAccounts(Wallet masterWallet, List standardAccounts, Key key) { + try { + addAndSaveAccounts(masterWallet, standardAccounts); + } finally { + masterWallet.encrypt(key); + for(Wallet childWallet : masterWallet.getChildWallets()) { + if(!childWallet.isEncrypted()) { + childWallet.encrypt(key); + } + } + key.clear(); + } + } + + private void addAndSaveAccounts(Wallet masterWallet, List standardAccounts) { + for(StandardAccount standardAccount : standardAccounts) { + addAndSaveAccount(masterWallet, standardAccount); + } + } + private void addAndSaveAccount(Wallet masterWallet, StandardAccount standardAccount) { if(StandardAccount.WHIRLPOOL_ACCOUNTS.contains(standardAccount)) { WhirlpoolServices.prepareWhirlpoolWallet(masterWallet, getWalletForm().getWalletId(), getWalletForm().getStorage()); @@ -521,6 +565,7 @@ public class SettingsController extends WalletFormController implements Initiali try { storage.saveWallet(childWallet); } catch(Exception e) { + log.error("Error saving wallet", e); AppServices.showErrorDialog("Error saving wallet " + childWallet.getName(), e.getMessage()); } }