add connected device wallet discovery with configurable number of accounts to scan
What changed, and why it matters
This commit adds a new feature to Sparrow Wallet that lets users scan a connected hardware wallet for existing accounts and transactions across multiple script types. It is a feature addition, not a security fix. The code changes how wallet import events and discovery results are handled, moving from single-wallet to multi-wallet results. There is no direct evidence in the commit of a vulnerability being patched.
No security action required. Reviewers may optionally verify that the new `RangeInputDialog` bounds and the multi-wallet import loop in `AppController` behave correctly, and that no private key material is logged during discovery, but the commit itself does not indicate a security issue.
Security signals we found
No security-relevant signals detected in the diff
Feature addition: hardware wallet account discovery
Refactoring of wallet import event payload from single to list of wallets
No input sanitization or bounds-checking changes beyond standard Spinner range limits
Evidence from the diff
The commit introduces connected-device wallet discovery with a configurable number of accounts to scan. Key changes include: adding a discoverWallet() method in DevicePane, a new RangeInputDialog for account count selection, refactoring WalletImportEvent and WalletImportDialog to carry List<Wallet> instead of a single Wallet, updating Hwi.GetXpubsService to use a new WalletType record (script type + account) and report progress, and extending ElectrumServer.WalletDiscoveryService to scan child wallets and return multiple discovered wallets. The existing discoverKeystores() flow is refactored to use the new WalletType map and progress reporting. No security-relevant fixes (e.g., input validation, cryptographic hardening, privilege reduction) are visible in the diff.
Changed components
com.sparrowwallet.sparrow.control.DevicePanecom.sparrowwallet.sparrow.control.WalletImportDialogcom.sparrowwallet.sparrow.control.MnemonicWalletKeystoreImportPanecom.sparrowwallet.sparrow.control.RangeInputDialogcom.sparrowwallet.sparrow.event.WalletImportEventcom.sparrowwallet.sparrow.io.Hwicom.sparrowwallet.sparrow.net.ElectrumServercom.sparrowwallet.sparrow.terminal.wallet.NewWalletDialogcom.sparrowwallet.sparrow.AppControllerInspect captured patch +256 / −52
diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java
index e816e59..9a52fc3 100644
--- a/src/main/java/com/sparrowwallet/sparrow/AppController.java
+++ b/src/main/java/com/sparrowwallet/sparrow/AppController.java
@@ -1217,22 +1217,24 @@ public class AppController implements Initializable {
List<WalletForm> selectedWalletForms = getSelectedWalletForms();
WalletImportDialog dlg = new WalletImportDialog(selectedWalletForms);
dlg.initOwner(rootStack.getScene().getWindow());
- Optional<Wallet> optionalWallet = dlg.showAndWait();
- if(optionalWallet.isPresent()) {
- Wallet wallet = optionalWallet.get();
-
- List<WalletTabData> walletTabData = getOpenWalletTabData();
- List<ExtendedKey> xpubs = wallet.getKeystores().stream().map(Keystore::getExtendedPublicKey).collect(Collectors.toList());
- Optional<WalletForm> optNewWalletForm = walletTabData.stream()
- .map(WalletTabData::getWalletForm)
- .filter(wf -> wf.getSettingsWalletForm() != null && wf.getSettingsWalletForm().getWallet().getPolicyType() == PolicyType.MULTI &&
- wf.getSettingsWalletForm().getWallet().getScriptType() == wallet.getScriptType() && !wf.getSettingsWalletForm().getWallet().isValid() &&
- wf.getSettingsWalletForm().getWallet().getKeystores().stream().map(Keystore::getExtendedPublicKey).anyMatch(xpubs::contains)).findFirst();
- if(optNewWalletForm.isPresent()) {
- EventManager.get().post(new ExistingWalletImportedEvent(optNewWalletForm.get().getWalletId(), wallet));
- selectTab(optNewWalletForm.get().getWallet());
- } else if(selectedWalletForms.isEmpty() || wallet != selectedWalletForms.get(0).getWallet()) {
- addImportedWallet(wallet);
+ Optional<List<Wallet>> optionalWallets = dlg.showAndWait();
+ if(optionalWallets.isPresent()) {
+ List<Wallet> wallets = optionalWallets.get();
+
+ for(Wallet wallet : wallets) {
+ List<WalletTabData> walletTabData = getOpenWalletTabData();
+ List<ExtendedKey> xpubs = wallet.getKeystores().stream().map(Keystore::getExtendedPublicKey).collect(Collectors.toList());
+ Optional<WalletForm> optNewWalletForm = walletTabData.stream()
+ .map(WalletTabData::getWalletForm)
+ .filter(wf -> wf.getSettingsWalletForm() != null && wf.getSettingsWalletForm().getWallet().getPolicyType() == PolicyType.MULTI &&
+ wf.getSettingsWalletForm().getWallet().getScriptType() == wallet.getScriptType() && !wf.getSettingsWalletForm().getWallet().isValid() &&
+ wf.getSettingsWalletForm().getWallet().getKeystores().stream().map(Keystore::getExtendedPublicKey).anyMatch(xpubs::contains)).findFirst();
+ if(optNewWalletForm.isPresent()) {
+ EventManager.get().post(new ExistingWalletImportedEvent(optNewWalletForm.get().getWalletId(), wallet));
+ selectTab(optNewWalletForm.get().getWallet());
+ } else if(selectedWalletForms.isEmpty() || wallet != selectedWalletForms.get(0).getWallet()) {
+ addImportedWallet(wallet);
+ }
}
}
}
diff --git a/src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java b/src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java
index cd82c54..65a5569 100644
--- a/src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java
+++ b/src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java
@@ -19,6 +19,7 @@ import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.io.*;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.net.ElectrumServer;
+import com.sparrowwallet.sparrow.net.ServerType;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
@@ -313,6 +314,11 @@ public class DevicePane extends TitledDescriptionPane {
});
importMenuButton.getItems().add(item);
}
+ importMenuButton.getItems().add(new SeparatorMenuItem());
+ MenuItem discoverItem = new MenuItem("Discover Wallet...");
+ discoverItem.setDisable(!AppServices.isConnected());
+ discoverItem.setOnAction(_ -> discoverWallet());
+ importMenuButton.getItems().add(discoverItem);
} else {
String[] accounts = new String[] {"Default Account #0", "Account #1", "Account #2", "Account #3", "Account #4", "Account #5", "Account #6", "Account #7", "Account #8", "Account #9"};
int scriptAccountsLength = ScriptType.P2SH.equals(wallet.getScriptType()) ? 1 : accounts.length;
@@ -378,7 +384,6 @@ public class DevicePane extends TitledDescriptionPane {
discoverKeystoresButton = new Button("Discover");
discoverKeystoresButton.setAlignment(Pos.CENTER_RIGHT);
discoverKeystoresButton.setOnAction(event -> {
- discoverKeystoresButton.setDisable(true);
discoverKeystores();
});
discoverKeystoresButton.managedProperty().bind(discoverKeystoresButton.visibleProperty());
@@ -903,29 +908,129 @@ public class DevicePane extends TitledDescriptionPane {
}
}
+ private void discoverWallet() {
+ importButton.setDisable(true);
+ importButton.setMaxHeight(importButton.getHeight());
+ ProgressIndicator progressIndicator = new ProgressIndicator(0);
+ progressIndicator.getStyleClass().add("button-progress");
+ importButton.setGraphic(progressIndicator);
+ List<Wallet> wallets = new ArrayList<>();
+
+ RangeInputDialog rangeInputDialog = new RangeInputDialog(StandardAccount.ACCOUNT_0.getAccountNumber(), StandardAccount.ACCOUNT_30.getAccountNumber(), StandardAccount.ACCOUNT_10.getAccountNumber());
+ rangeInputDialog.setTitle("Choose number of accounts");
+ rangeInputDialog.setHeaderText("Enter the number of additional accounts to scan for existing funds.\n\nThis may take a few minutes depending on how many accounts are selected.");
+ Optional<Integer> optRange = rangeInputDialog.showAndWait();
+ if(optRange.isEmpty()) {
+ return;
+ }
+
+ List<StandardAccount> discoveryAccounts = new ArrayList<>(Arrays.asList(StandardAccount.values()).subList(0, optRange.get() + 1));
+ Map<Hwi.WalletType, String> derivationPaths = new LinkedHashMap<>();
+ for(ScriptType scriptType : ScriptType.getAddressableScriptTypes(PolicyType.SINGLE)) {
+ for(StandardAccount discoveryAccount : discoveryAccounts) {
+ derivationPaths.put(new Hwi.WalletType(scriptType, discoveryAccount), KeyDerivation.writePath(scriptType.getDefaultDerivation(discoveryAccount.getAccountNumber())));
+ }
+ }
+
+ Hwi.GetXpubsService getXpubsService = new Hwi.GetXpubsService(device, passphrase.get(), derivationPaths);
+ getXpubsService.setOnSucceeded(_ -> {
+ Map<Hwi.WalletType, String> accountXpubs = getXpubsService.getValue();
+
+ for(Map.Entry<Hwi.WalletType, String> entry : accountXpubs.entrySet()) {
+ try {
+ Wallet wallet = new Wallet(device.getModel().toDisplayString());
+ wallet.setPolicyType(PolicyType.SINGLE);
+ wallet.setScriptType(entry.getKey().scriptType());
+ Keystore keystore = new Keystore();
+ keystore.setLabel(device.getModel().toDisplayString());
+ keystore.setSource(KeystoreSource.HW_USB);
+ keystore.setWalletModel(device.getModel());
+ keystore.setKeyDerivation(new KeyDerivation(device.getFingerprint(), derivationPaths.get(entry.getKey())));
+ keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(entry.getValue()));
+ wallet.getKeystores().add(keystore);
+ wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, entry.getKey().scriptType(), wallet.getKeystores(), 1));
+ if(entry.getKey().standardAccount().equals(StandardAccount.ACCOUNT_0)) {
+ wallets.add(wallet);
+ } else {
+ Wallet masterWallet = wallets.getLast();
+ wallet.setName(entry.getKey().standardAccount().getName());
+ wallet.setMasterWallet(masterWallet);
+ masterWallet.getChildWallets().add(wallet);
+ }
+ } catch(Exception e) {
+ setError("Could not retrieve xpub", e.getMessage());
+ }
+ }
+
+ ElectrumServer.WalletDiscoveryService walletDiscoveryService = new ElectrumServer.WalletDiscoveryService(wallets);
+ walletDiscoveryService.setOnSucceeded(_ -> {
+ importButton.setGraphic(null);
+ Optional<List<Wallet>> optWallets = walletDiscoveryService.getValue();
+ if(optWallets.isPresent()) {
+ List<Wallet> discoveredWallets = optWallets.get();
+ if(discoveredWallets.size() > 1) {
+ for(Wallet wallet : discoveredWallets) {
+ wallet.setName(wallet.getName() + " " + wallet.getScriptType().getDescription());
+ }
+ }
+ EventManager.get().post(new WalletImportEvent(discoveredWallets));
+ } else {
+ AppServices.showErrorDialog("No existing wallet found",
+ Config.get().getServerType() == ServerType.BITCOIN_CORE ? "The configured server type is Bitcoin Core, which does not support wallet discovery.\n\n" +
+ "You can however import the " + device.getModel().toDisplayString() + " and scan the blockchain by supplying a start date." :
+ "Could not find a wallet with existing transactions using the " + device.getModel().toDisplayString() + ".");
+ setDefaultStatus();
+ importButton.setDisable(false);
+ }
+ });
+ walletDiscoveryService.setOnFailed(failedEvent -> {
+ log.error("Failed to discover wallets", failedEvent.getSource().getException());
+ setError("Failed to discover wallets", failedEvent.getSource().getException().getMessage());
+ importButton.setGraphic(null);
+ importButton.setDisable(false);
+ });
+ walletDiscoveryService.start();
+ });
+ getXpubsService.setOnFailed(_ -> {
+ setError("Could not retrieve xpub", getXpubsService.getException().getMessage());
+ importButton.setGraphic(null);
+ importButton.setDisable(false);
+ });
+ progressIndicator.progressProperty().bind(getXpubsService.progressProperty());
+ getXpubsService.progressProperty().addListener((_, _, newValue) -> setDescription("Discovering... (" + Math.round(newValue.doubleValue() * 100) + "%)"));
+ showHideLink.setVisible(false);
+ getXpubsService.start();
+ }
+
private void discoverKeystores() {
if(wallet.getKeystores().size() != 1) {
setError("Could not discover keystores", "Only single signature wallets are supported for keystore discovery");
return;
}
+ discoverKeystoresButton.setDisable(true);
+ discoverKeystoresButton.setMaxHeight(discoverKeystoresButton.getHeight());
+ ProgressIndicator progressIndicator = new ProgressIndicator(0);
+ progressIndicator.getStyleClass().add("button-progress");
+ discoverKeystoresButton.setGraphic(progressIndicator);
+
String masterFingerprint = wallet.getKeystores().get(0).getKeyDerivation().getMasterFingerprint();
Wallet copyWallet = wallet.copy();
- Map<StandardAccount, String> accountDerivationPaths = new LinkedHashMap<>();
+ Map<Hwi.WalletType, String> accountDerivationPaths = new LinkedHashMap<>();
for(StandardAccount availableAccount : availableAccounts) {
Wallet availableWallet = copyWallet.addChildWallet(availableAccount);
Keystore availableKeystore = availableWallet.getKeystores().get(0);
String derivationPath = availableKeystore.getKeyDerivation().getDerivationPath();
- accountDerivationPaths.put(availableAccount, derivationPath);
+ accountDerivationPaths.put(new Hwi.WalletType(wallet.getScriptType(), availableAccount), derivationPath);
}
Map<StandardAccount, Keystore> importedKeystores = new LinkedHashMap<>();
Hwi.GetXpubsService getXpubsService = new Hwi.GetXpubsService(device, passphrase.get(), accountDerivationPaths);
getXpubsService.setOnSucceeded(workerStateEvent -> {
- Map<StandardAccount, String> accountXpubs = getXpubsService.getValue();
+ Map<Hwi.WalletType, String> accountXpubs = getXpubsService.getValue();
- for(Map.Entry<StandardAccount, String> entry : accountXpubs.entrySet()) {
+ for(Map.Entry<Hwi.WalletType, String> entry : accountXpubs.entrySet()) {
try {
Keystore keystore = new Keystore();
keystore.setLabel(device.getModel().toDisplayString());
@@ -933,7 +1038,7 @@ public class DevicePane extends TitledDescriptionPane {
keystore.setWalletModel(device.getModel());
keystore.setKeyDerivation(new KeyDerivation(masterFingerprint, accountDerivationPaths.get(entry.getKey())));
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(entry.getValue()));
- importedKeystores.put(entry.getKey(), keystore);
+ importedKeystores.put(entry.getKey().standardAccount(), keystore);
} catch(Exception e) {
setError("Could not retrieve xpub", e.getMessage());
}
@@ -947,15 +1052,18 @@ public class DevicePane extends TitledDescriptionPane {
accountDiscoveryService.setOnFailed(event -> {
log.error("Failed to discover accounts", event.getSource().getException());
setError("Failed to discover accounts", event.getSource().getException().getMessage());
+ discoverKeystoresButton.setGraphic(null);
discoverKeystoresButton.setDisable(false);
});
accountDiscoveryService.start();
});
getXpubsService.setOnFailed(workerStateEvent -> {
setError("Could not retrieve xpub", getXpubsService.getException().getMessage());
+ discoverKeystoresButton.setGraphic(null);
discoverKeystoresButton.setDisable(false);
});
- setDescription("Discovering...");
+ progressIndicator.progressProperty().bind(getXpubsService.progressProperty());
+ getXpubsService.progressProperty().addListener((_, _, newValue) -> setDescription("Discovering... (" + Math.round(newValue.doubleValue() * 100) + "%)"));
showHideLink.setVisible(false);
getXpubsService.start();
}
diff --git a/src/main/java/com/sparrowwallet/sparrow/control/MnemonicWalletKeystoreImportPane.java b/src/main/java/com/sparrowwallet/sparrow/control/MnemonicWalletKeystoreImportPane.java
index 9aff313..494f8eb 100644
--- a/src/main/java/com/sparrowwallet/sparrow/control/MnemonicWalletKeystoreImportPane.java
+++ b/src/main/java/com/sparrowwallet/sparrow/control/MnemonicWalletKeystoreImportPane.java
@@ -134,9 +134,15 @@ public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane {
progressIndicator.progressProperty().bind(walletDiscoveryService.progressProperty());
walletDiscoveryService.setOnSucceeded(successEvent -> {
discoverButton.setGraphic(null);
- Optional<Wallet> optWallet = walletDiscoveryService.getValue();
- if(optWallet.isPresent()) {
- EventManager.get().post(new WalletImportEvent(optWallet.get()));
+ Optional<List<Wallet>> optWallets = walletDiscoveryService.getValue();
+ if(optWallets.isPresent()) {
+ List<Wallet> discoveredWallets = optWallets.get();
+ if(discoveredWallets.size() > 1) {
+ for(Wallet wallet : discoveredWallets) {
+ wallet.setName(wallet.getKeystores().getFirst().getLabel() + " " + wallet.getScriptType().getDescription());
+ }
+ }
+ EventManager.get().post(new WalletImportEvent(discoveredWallets));
} else {
discoverButton.setDisable(false);
Optional<ButtonType> optButtonType = AppServices.showErrorDialog("No existing wallet found",
diff --git a/src/main/java/com/sparrowwallet/sparrow/control/RangeInputDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/RangeInputDialog.java
new file mode 100644
index 0000000..d1dc92c
--- /dev/null
+++ b/src/main/java/com/sparrowwallet/sparrow/control/RangeInputDialog.java
@@ -0,0 +1,61 @@
+package com.sparrowwallet.sparrow.control;
+
+import com.sparrowwallet.sparrow.AppServices;
+import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
+import javafx.application.Platform;
+import javafx.geometry.Insets;
+import javafx.scene.control.*;
+import javafx.scene.layout.GridPane;
+import org.controlsfx.glyphfont.Glyph;
+
+public class RangeInputDialog extends Dialog<Integer> {
+ private final Spinner<Integer> spinner;
+
+ public RangeInputDialog(int min, int max, int initialValue) {
+ final DialogPane dialogPane = getDialogPane();
+
+ setTitle("Select a Value");
+ setHeaderText("Choose a value between " + min + " and " + max);
+
+ Glyph key = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.SORT_NUMERIC_DOWN);
+ key.setFontSize(50);
+ key.setPadding(new Insets(0, 0, 0, 10));
+ dialogPane.setGraphic(key);
+ dialogPane.getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
+
+ spinner = new Spinner<>();
+ spinner.setValueFactory(new SpinnerValueFactory.IntegerSpinnerValueFactory(min, max, initialValue));
+ spinner.setPrefWidth(80);
+
+ GridPane grid = new GridPane();
+ grid.setHgap(10);
+ grid.setVgap(10);
+ grid.setPadding(new Insets(20, 20, 10, 20));
+
+ grid.add(new Label("Enter value between " + min + " and " + max + ":"), 0, 0);
+ grid.add(spinner, 1, 0);
+
+ dialogPane.setContent(grid);
+ dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
+ AppServices.setStageIcon(dialogPane.getScene().getWindow());
+
+ Platform.runLater(spinner::requestFocus);
+
+ setResultConverter((dialogButton) -> {
+ ButtonBar.ButtonData data = dialogButton == null ? null : dialogButton.getButtonData();
+ return data == ButtonBar.ButtonData.OK_DONE ? spinner.getValue() : null;
+ });
+
+ dialogPane.setPrefWidth(500);
+ dialogPane.setPrefHeight(230);
+ AppServices.moveToActiveWindowScreen(this);
+ }
+
+ public void setValue(int value) {
+ spinner.getValueFactory().setValue(value);
+ }
+
+ public int getValue() {
+ return spinner.getValue();
+ }
+}
diff --git a/src/main/java/com/sparrowwallet/sparrow/control/WalletImportDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/WalletImportDialog.java
index dcdef39..7c56b21 100644
--- a/src/main/java/com/sparrowwallet/sparrow/control/WalletImportDialog.java
+++ b/src/main/java/com/sparrowwallet/sparrow/control/WalletImportDialog.java
@@ -21,8 +21,8 @@ import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
-public class WalletImportDialog extends Dialog<Wallet> {
- private Wallet wallet;
+public class WalletImportDialog extends Dialog<List<Wallet>> {
+ private List<Wallet> wallets;
private final Accordion importAccordion;
private final Button scanButton;
@@ -95,13 +95,13 @@ public class WalletImportDialog extends Dialog<Wallet> {
dialogPane.setMinHeight(dialogPane.getPrefHeight());
AppServices.moveToActiveWindowScreen(this);
- setResultConverter(dialogButton -> dialogButton != cancelButtonType ? wallet : null);
+ setResultConverter(dialogButton -> dialogButton != cancelButtonType ? wallets : null);
}
@Subscribe
public void walletImported(WalletImportEvent event) {
- wallet = event.getWallet();
- setResult(wallet);
+ wallets = event.getWallets();
+ setResult(wallets);
}
private void scan() {
diff --git a/src/main/java/com/sparrowwallet/sparrow/event/WalletImportEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/WalletImportEvent.java
index 6dfc7ae..9a6afbe 100644
--- a/src/main/java/com/sparrowwallet/sparrow/event/WalletImportEvent.java
+++ b/src/main/java/com/sparrowwallet/sparrow/event/WalletImportEvent.java
@@ -2,14 +2,20 @@ package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet;
+import java.util.List;
+
public class WalletImportEvent {
- private Wallet wallet;
+ private List<Wallet> wallets;
public WalletImportEvent(Wallet wallet) {
- this.wallet = wallet;
+ this.wallets = List.of(wallet);
+ }
+
+ public WalletImportEvent(List<Wallet> wallets) {
+ this.wallets = wallets;
}
- public Wallet getWallet() {
- return wallet;
+ public List<Wallet> getWallets() {
+ return wallets;
}
}
diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Hwi.java b/src/main/java/com/sparrowwallet/sparrow/io/Hwi.java
index 75e319f..0b6ecc8 100644
--- a/src/main/java/com/sparrowwallet/sparrow/io/Hwi.java
+++ b/src/main/java/com/sparrowwallet/sparrow/io/Hwi.java
@@ -17,6 +17,9 @@ import com.sparrowwallet.sparrow.SparrowWallet;
import com.sparrowwallet.sparrow.control.BitBoxPairingDialog;
import com.sparrowwallet.sparrow.control.TextfieldDialog;
import javafx.application.Platform;
+import javafx.collections.FXCollections;
+import javafx.collections.MapChangeListener;
+import javafx.collections.ObservableMap;
import javafx.concurrent.ScheduledService;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
@@ -142,9 +145,8 @@ public class Hwi {
}
}
- public Map<StandardAccount, String> getXpubs(Device device, String passphrase, Map<StandardAccount, String> accountDerivationPaths) throws ImportException {
- Map<StandardAccount, String> accountXpubs = new LinkedHashMap<>();
- for(Map.Entry<StandardAccount, String> entry : accountDerivationPaths.entrySet()) {
+ public Map<WalletType, String> getXpubs(Device device, String passphrase, Map<WalletType, String> accountDerivationPaths, Map<WalletType, String> accountXpubs) throws ImportException {
+ for(Map.Entry<WalletType, String> entry : accountDerivationPaths.entrySet()) {
accountXpubs.put(entry.getKey(), getXpub(device, passphrase, entry.getValue()));
}
@@ -441,23 +443,26 @@ public class Hwi {
}
}
- public static class GetXpubsService extends Service<Map<StandardAccount, String>> {
+ public static class GetXpubsService extends Service<Map<WalletType, String>> {
private final Device device;
private final String passphrase;
- private final Map<StandardAccount, String> accountDerivationPaths;
+ private final Map<WalletType, String> accountDerivationPaths;
- public GetXpubsService(Device device, String passphrase, Map<StandardAccount, String> accountDerivationPaths) {
+ public GetXpubsService(Device device, String passphrase, Map<WalletType, String> accountDerivationPaths) {
this.device = device;
this.passphrase = passphrase;
this.accountDerivationPaths = accountDerivationPaths;
}
@Override
- protected Task<Map<StandardAccount, String>> createTask() {
+ protected Task<Map<WalletType, String>> createTask() {
return new Task<>() {
- protected Map<StandardAccount, String> call() throws ImportException {
+ protected Map<WalletType, String> call() throws ImportException {
Hwi hwi = new Hwi();
- return hwi.getXpubs(device, passphrase, accountDerivationPaths);
+ updateProgress(0, accountDerivationPaths.size());
+ ObservableMap<WalletType, String> accountXpubs = FXCollections.observableMap(new LinkedHashMap<>());
+ accountXpubs.addListener((MapChangeListener<? super WalletType, ? super String>) _ -> updateProgress(accountXpubs.size(), accountDerivationPaths.size()));
+ return hwi.getXpubs(device, passphrase, accountDerivationPaths, accountXpubs);
}
};
}
@@ -630,4 +635,6 @@ public class Hwi {
Platform.runLater(() -> AppServices.showSuccessDialog("Pairing Successful", "The " + deviceInfo + " has been successfully paired."));
}
}
+
+ public record WalletType(ScriptType scriptType, StandardAccount standardAccount) {}
}
diff --git a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java
index 3c089ee..9112d12 100644
--- a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java
+++ b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java
@@ -2136,7 +2136,7 @@ public class ElectrumServer {
}
}
- public static class WalletDiscoveryService extends Service<Optional<Wallet>> {
+ public static class WalletDiscoveryService extends Service<Optional<List<Wallet>>> {
private final List<Wallet> wallets;
public WalletDiscoveryService(List<Wallet> wallets) {
@@ -2144,17 +2144,31 @@ public class ElectrumServer {
}
@Override
- protected Task<Optional<Wallet>> createTask() {
+ protected Task<Optional<List<Wallet>>> createTask() {
return new Task<>() {
- protected Optional<Wallet> call() throws ServerException {
+ protected Optional<List<Wallet>> call() throws ServerException {
ElectrumServer electrumServer = new ElectrumServer();
+ List<Wallet> discoveredWallets = new ArrayList<>();
for(int i = 0; i < wallets.size(); i++) {
Wallet wallet = wallets.get(i);
updateProgress(i, wallets.size() + StandardAccount.DISCOVERY_ACCOUNTS.size());
Map<WalletNode, Set<BlockTransactionHash>> nodeTransactionMap = new TreeMap<>();
electrumServer.getReferences(wallet, wallet.getNode(KeyPurpose.RECEIVE).getChildren(), nodeTransactionMap, 0);
- if(nodeTransactionMap.values().stream().anyMatch(blockTransactionHashes -> !blockTransactionHashes.isEmpty())) {
+ boolean found = nodeTransactionMap.values().stream().anyMatch(blockTransactionHashes -> !blockTransactionHashes.isEmpty());
+
+ for(Iterator<Wallet> iterator = wallet.getChildWallets().iterator(); iterator.hasNext(); ) {
+ Wallet childWallet = iterator.next();
+ Map<WalletNode, Set<BlockTransactionHash>> childTransactionMap = new TreeMap<>();
+ electrumServer.getReferences(childWallet, childWallet.getNode(KeyPurpose.RECEIVE).getChildren(), childTransactionMap, 0);
+ if(childTransactionMap.values().stream().anyMatch(blockTransactionHashes -> !blockTransactionHashes.isEmpty())) {
+ found = true;
+ } else {
+ iterator.remove();
+ }
+ }
+
+ if(found) {
Wallet masterWalletCopy = wallet.copy();
List<StandardAccount> searchAccounts = getStandardAccounts(wallet);
Set<StandardAccount> foundAccounts = new LinkedHashSet<>();
@@ -2177,11 +2191,11 @@ public class ElectrumServer {
wallet.addChildWallet(standardAccount);
}
- return Optional.of(wallet);
+ discoveredWallets.add(wallet);
}
}
- return Optional.empty();
+ return discoveredWallets.isEmpty() ? Optional.empty() : Optional.of(discoveredWallets);
}
};
}
diff --git a/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/NewWalletDialog.java b/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/NewWalletDialog.java
index 953ecb0..945efe1 100644
--- a/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/NewWalletDialog.java
+++ b/src/main/java/com/sparrowwallet/sparrow/terminal/wallet/NewWalletDialog.java
@@ -88,8 +88,8 @@ public abstract class NewWalletDialog extends DialogWindow {
Platform.runLater(() -> {
ElectrumServer.WalletDiscoveryService walletDiscoveryService = new ElectrumServer.WalletDiscoveryService(wallets);
walletDiscoveryService.setOnSucceeded(successEvent -> {
- Optional<Wallet> optWallet = walletDiscoveryService.getValue();
- wallet = optWallet.orElseGet(() -> wallets.get(0));
+ Optional<List<Wallet>> optWallets = walletDiscoveryService.getValue();
+ wallet = optWallets.orElseGet(() -> wallets).getFirst();
SparrowTerminal.get().getGuiThread().invokeLater(() -> {
SparrowTerminal.get().getGui().removeWindow(discoveringDialog);
saveWallet(wallet);
Why this scored 18/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.