mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-11-05 05:46:44 +00:00
support discovery of singlesig connected hardware wallet accounts
This commit is contained in:
parent
d3b1c51115
commit
180e76f0f8
7 changed files with 237 additions and 8 deletions
|
@ -5,6 +5,8 @@ import com.sparrowwallet.drongo.wallet.StandardAccount;
|
||||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||||
import com.sparrowwallet.sparrow.AppServices;
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||||
|
import com.sparrowwallet.sparrow.io.Config;
|
||||||
|
import com.sparrowwallet.sparrow.net.ServerType;
|
||||||
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolServices;
|
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolServices;
|
||||||
import javafx.collections.FXCollections;
|
import javafx.collections.FXCollections;
|
||||||
import javafx.scene.control.*;
|
import javafx.scene.control.*;
|
||||||
|
@ -59,7 +61,9 @@ public class AddAccountDialog extends Dialog<List<StandardAccount>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
final ButtonType discoverButtonType = new javafx.scene.control.ButtonType("Discover", ButtonBar.ButtonData.LEFT);
|
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)) {
|
if(!availableAccounts.isEmpty() && Config.get().getServerType() != ServerType.BITCOIN_CORE &&
|
||||||
|
(masterWallet.getKeystores().stream().allMatch(ks -> ks.getSource() == KeystoreSource.SW_SEED)
|
||||||
|
|| (masterWallet.getKeystores().size() == 1 && masterWallet.getKeystores().stream().allMatch(ks -> ks.getSource() == KeystoreSource.HW_USB)))) {
|
||||||
dialogPane.getButtonTypes().add(discoverButtonType);
|
dialogPane.getButtonTypes().add(discoverButtonType);
|
||||||
Button discoverButton = (Button)dialogPane.lookupButton(discoverButtonType);
|
Button discoverButton = (Button)dialogPane.lookupButton(discoverButtonType);
|
||||||
discoverButton.disableProperty().bind(AppServices.onlineProperty().not());
|
discoverButton.disableProperty().bind(AppServices.onlineProperty().not());
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
|
import com.google.common.eventbus.Subscribe;
|
||||||
|
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||||
|
import com.sparrowwallet.drongo.wallet.StandardAccount;
|
||||||
|
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||||
|
import com.sparrowwallet.sparrow.EventManager;
|
||||||
|
import com.sparrowwallet.sparrow.event.KeystoresDiscoveredEvent;
|
||||||
|
import com.sparrowwallet.sparrow.io.Device;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class DeviceKeystoreDiscoverDialog extends DeviceDialog<Map<StandardAccount, Keystore>> {
|
||||||
|
private final Wallet masterWallet;
|
||||||
|
private final List<StandardAccount> availableAccounts;
|
||||||
|
|
||||||
|
public DeviceKeystoreDiscoverDialog(List<String> operationFingerprints, Wallet masterWallet, List<StandardAccount> availableAccounts) {
|
||||||
|
super(operationFingerprints);
|
||||||
|
this.masterWallet = masterWallet;
|
||||||
|
this.availableAccounts = availableAccounts;
|
||||||
|
EventManager.get().register(this);
|
||||||
|
setOnCloseRequest(event -> {
|
||||||
|
EventManager.get().unregister(this);
|
||||||
|
});
|
||||||
|
setResultConverter(dialogButton -> dialogButton.getButtonData().isCancelButton() ? null : Collections.emptyMap());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected DevicePane getDevicePane(Device device, boolean defaultDevice) {
|
||||||
|
return new DevicePane(masterWallet, availableAccounts, device, defaultDevice);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void keystoresDiscovered(KeystoresDiscoveredEvent event) {
|
||||||
|
setResult(event.getDiscoveredKeystores());
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,12 +10,14 @@ import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||||
import com.sparrowwallet.drongo.wallet.KeystoreSource;
|
import com.sparrowwallet.drongo.wallet.KeystoreSource;
|
||||||
|
import com.sparrowwallet.drongo.wallet.StandardAccount;
|
||||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||||
import com.sparrowwallet.sparrow.EventManager;
|
import com.sparrowwallet.sparrow.EventManager;
|
||||||
import com.sparrowwallet.sparrow.event.*;
|
import com.sparrowwallet.sparrow.event.*;
|
||||||
import com.sparrowwallet.sparrow.io.Device;
|
import com.sparrowwallet.sparrow.io.Device;
|
||||||
import com.sparrowwallet.sparrow.io.Hwi;
|
import com.sparrowwallet.sparrow.io.Hwi;
|
||||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||||
|
import com.sparrowwallet.sparrow.net.ElectrumServer;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.beans.property.SimpleStringProperty;
|
import javafx.beans.property.SimpleStringProperty;
|
||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
|
@ -33,8 +35,7 @@ import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.*;
|
||||||
import java.util.List;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public class DevicePane extends TitledDescriptionPane {
|
public class DevicePane extends TitledDescriptionPane {
|
||||||
|
@ -46,6 +47,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||||
private final OutputDescriptor outputDescriptor;
|
private final OutputDescriptor outputDescriptor;
|
||||||
private final KeyDerivation keyDerivation;
|
private final KeyDerivation keyDerivation;
|
||||||
private final String message;
|
private final String message;
|
||||||
|
private final List<StandardAccount> availableAccounts;
|
||||||
private final Device device;
|
private final Device device;
|
||||||
|
|
||||||
private CustomPasswordField pinField;
|
private CustomPasswordField pinField;
|
||||||
|
@ -56,6 +58,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||||
private Button signButton;
|
private Button signButton;
|
||||||
private Button displayAddressButton;
|
private Button displayAddressButton;
|
||||||
private Button signMessageButton;
|
private Button signMessageButton;
|
||||||
|
private Button discoverKeystoresButton;
|
||||||
|
|
||||||
private final SimpleStringProperty passphrase = new SimpleStringProperty("");
|
private final SimpleStringProperty passphrase = new SimpleStringProperty("");
|
||||||
|
|
||||||
|
@ -69,6 +72,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||||
this.outputDescriptor = null;
|
this.outputDescriptor = null;
|
||||||
this.keyDerivation = requiredDerivation;
|
this.keyDerivation = requiredDerivation;
|
||||||
this.message = null;
|
this.message = null;
|
||||||
|
this.availableAccounts = null;
|
||||||
this.device = device;
|
this.device = device;
|
||||||
this.defaultDevice = defaultDevice;
|
this.defaultDevice = defaultDevice;
|
||||||
|
|
||||||
|
@ -91,6 +95,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||||
this.outputDescriptor = null;
|
this.outputDescriptor = null;
|
||||||
this.keyDerivation = null;
|
this.keyDerivation = null;
|
||||||
this.message = null;
|
this.message = null;
|
||||||
|
this.availableAccounts = null;
|
||||||
this.device = device;
|
this.device = device;
|
||||||
this.defaultDevice = defaultDevice;
|
this.defaultDevice = defaultDevice;
|
||||||
|
|
||||||
|
@ -113,6 +118,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||||
this.outputDescriptor = outputDescriptor;
|
this.outputDescriptor = outputDescriptor;
|
||||||
this.keyDerivation = null;
|
this.keyDerivation = null;
|
||||||
this.message = null;
|
this.message = null;
|
||||||
|
this.availableAccounts = null;
|
||||||
this.device = device;
|
this.device = device;
|
||||||
this.defaultDevice = defaultDevice;
|
this.defaultDevice = defaultDevice;
|
||||||
|
|
||||||
|
@ -135,6 +141,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||||
this.outputDescriptor = null;
|
this.outputDescriptor = null;
|
||||||
this.keyDerivation = keyDerivation;
|
this.keyDerivation = keyDerivation;
|
||||||
this.message = message;
|
this.message = message;
|
||||||
|
this.availableAccounts = null;
|
||||||
this.device = device;
|
this.device = device;
|
||||||
this.defaultDevice = defaultDevice;
|
this.defaultDevice = defaultDevice;
|
||||||
|
|
||||||
|
@ -149,6 +156,29 @@ public class DevicePane extends TitledDescriptionPane {
|
||||||
buttonBox.getChildren().addAll(setPassphraseButton, signMessageButton);
|
buttonBox.getChildren().addAll(setPassphraseButton, signMessageButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public DevicePane(Wallet wallet, List<StandardAccount> availableAccounts, Device device, boolean defaultDevice) {
|
||||||
|
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
|
||||||
|
this.deviceOperation = DeviceOperation.DISCOVER_KEYSTORES;
|
||||||
|
this.wallet = wallet;
|
||||||
|
this.psbt = null;
|
||||||
|
this.outputDescriptor = null;
|
||||||
|
this.keyDerivation = null;
|
||||||
|
this.message = null;
|
||||||
|
this.device = device;
|
||||||
|
this.defaultDevice = defaultDevice;
|
||||||
|
this.availableAccounts = availableAccounts;
|
||||||
|
|
||||||
|
setDefaultStatus();
|
||||||
|
showHideLink.setVisible(false);
|
||||||
|
|
||||||
|
createSetPassphraseButton();
|
||||||
|
createDiscoverKeystoresButton();
|
||||||
|
|
||||||
|
initialise(device);
|
||||||
|
|
||||||
|
buttonBox.getChildren().addAll(setPassphraseButton, discoverKeystoresButton);
|
||||||
|
}
|
||||||
|
|
||||||
private void initialise(Device device) {
|
private void initialise(Device device) {
|
||||||
if(device.isNeedsPinSent()) {
|
if(device.isNeedsPinSent()) {
|
||||||
unlockButton.setDefaultButton(defaultDevice);
|
unlockButton.setDefaultButton(defaultDevice);
|
||||||
|
@ -281,6 +311,17 @@ public class DevicePane extends TitledDescriptionPane {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void createDiscoverKeystoresButton() {
|
||||||
|
discoverKeystoresButton = new Button("Discover");
|
||||||
|
discoverKeystoresButton.setAlignment(Pos.CENTER_RIGHT);
|
||||||
|
discoverKeystoresButton.setOnAction(event -> {
|
||||||
|
discoverKeystoresButton.setDisable(true);
|
||||||
|
discoverKeystores();
|
||||||
|
});
|
||||||
|
discoverKeystoresButton.managedProperty().bind(discoverKeystoresButton.visibleProperty());
|
||||||
|
discoverKeystoresButton.setVisible(false);
|
||||||
|
}
|
||||||
|
|
||||||
private void unlock(Device device) {
|
private void unlock(Device device) {
|
||||||
if(device.getModel().requiresPinPrompt()) {
|
if(device.getModel().requiresPinPrompt()) {
|
||||||
promptPin();
|
promptPin();
|
||||||
|
@ -620,6 +661,63 @@ public class DevicePane extends TitledDescriptionPane {
|
||||||
signMessageService.start();
|
signMessageService.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void discoverKeystores() {
|
||||||
|
if(wallet.getKeystores().size() != 1) {
|
||||||
|
setError("Could not discover keystores", "Only single signature wallets are supported for keystore discovery");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String masterFingerprint = wallet.getKeystores().get(0).getKeyDerivation().getMasterFingerprint();
|
||||||
|
|
||||||
|
Wallet copyWallet = wallet.copy();
|
||||||
|
Map<StandardAccount, 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
for(Map.Entry<StandardAccount, String> entry : accountXpubs.entrySet()) {
|
||||||
|
try {
|
||||||
|
Keystore keystore = new Keystore();
|
||||||
|
keystore.setLabel(device.getModel().toDisplayString());
|
||||||
|
keystore.setSource(KeystoreSource.HW_USB);
|
||||||
|
keystore.setWalletModel(device.getModel());
|
||||||
|
keystore.setKeyDerivation(new KeyDerivation(masterFingerprint, accountDerivationPaths.get(entry.getKey())));
|
||||||
|
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(entry.getValue()));
|
||||||
|
importedKeystores.put(entry.getKey(), keystore);
|
||||||
|
} catch(Exception e) {
|
||||||
|
setError("Could not retrieve xpub", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ElectrumServer.WalletDiscoveryService walletDiscoveryService = new ElectrumServer.WalletDiscoveryService(wallet, importedKeystores);
|
||||||
|
walletDiscoveryService.setOnSucceeded(event -> {
|
||||||
|
importedKeystores.keySet().retainAll(walletDiscoveryService.getValue());
|
||||||
|
EventManager.get().post(new KeystoresDiscoveredEvent(importedKeystores));
|
||||||
|
});
|
||||||
|
walletDiscoveryService.setOnFailed(event -> {
|
||||||
|
log.error("Failed to discover accounts", event.getSource().getException());
|
||||||
|
setError("Failed to discover accounts", event.getSource().getException().getMessage());
|
||||||
|
discoverKeystoresButton.setDisable(false);
|
||||||
|
});
|
||||||
|
walletDiscoveryService.start();
|
||||||
|
});
|
||||||
|
getXpubsService.setOnFailed(workerStateEvent -> {
|
||||||
|
setError("Could not retrieve xpub", getXpubsService.getException().getMessage());
|
||||||
|
discoverKeystoresButton.setDisable(false);
|
||||||
|
});
|
||||||
|
setDescription("Discovering...");
|
||||||
|
showHideLink.setVisible(false);
|
||||||
|
getXpubsService.start();
|
||||||
|
}
|
||||||
|
|
||||||
private void showOperationButton() {
|
private void showOperationButton() {
|
||||||
if(deviceOperation.equals(DeviceOperation.IMPORT)) {
|
if(deviceOperation.equals(DeviceOperation.IMPORT)) {
|
||||||
if(defaultDevice) {
|
if(defaultDevice) {
|
||||||
|
@ -642,6 +740,10 @@ public class DevicePane extends TitledDescriptionPane {
|
||||||
signMessageButton.setDefaultButton(defaultDevice);
|
signMessageButton.setDefaultButton(defaultDevice);
|
||||||
signMessageButton.setVisible(true);
|
signMessageButton.setVisible(true);
|
||||||
showHideLink.setVisible(false);
|
showHideLink.setVisible(false);
|
||||||
|
} else if(deviceOperation.equals(DeviceOperation.DISCOVER_KEYSTORES)) {
|
||||||
|
discoverKeystoresButton.setDefaultButton(defaultDevice);
|
||||||
|
discoverKeystoresButton.setVisible(true);
|
||||||
|
showHideLink.setVisible(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -689,6 +791,6 @@ public class DevicePane extends TitledDescriptionPane {
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum DeviceOperation {
|
public enum DeviceOperation {
|
||||||
IMPORT, SIGN, DISPLAY_ADDRESS, SIGN_MESSAGE;
|
IMPORT, SIGN, DISPLAY_ADDRESS, SIGN_MESSAGE, DISCOVER_KEYSTORES;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
package com.sparrowwallet.sparrow.event;
|
||||||
|
|
||||||
|
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||||
|
import com.sparrowwallet.drongo.wallet.StandardAccount;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class KeystoresDiscoveredEvent {
|
||||||
|
private final Map<StandardAccount, Keystore> discoveredKeystores;
|
||||||
|
|
||||||
|
public KeystoresDiscoveredEvent(Map<StandardAccount, Keystore> discoveredKeystores) {
|
||||||
|
this.discoveredKeystores = discoveredKeystores;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<StandardAccount, Keystore> getDiscoveredKeystores() {
|
||||||
|
return discoveredKeystores;
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,6 +8,7 @@ import com.sparrowwallet.drongo.OutputDescriptor;
|
||||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||||
import com.sparrowwallet.drongo.psbt.PSBTParseException;
|
import com.sparrowwallet.drongo.psbt.PSBTParseException;
|
||||||
|
import com.sparrowwallet.drongo.wallet.StandardAccount;
|
||||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||||
import javafx.concurrent.ScheduledService;
|
import javafx.concurrent.ScheduledService;
|
||||||
import javafx.concurrent.Service;
|
import javafx.concurrent.Service;
|
||||||
|
@ -91,6 +92,15 @@ 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()) {
|
||||||
|
accountXpubs.put(entry.getKey(), getXpub(device, passphrase, entry.getValue()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return accountXpubs;
|
||||||
|
}
|
||||||
|
|
||||||
public String getXpub(Device device, String passphrase, String derivationPath) throws ImportException {
|
public String getXpub(Device device, String passphrase, String derivationPath) throws ImportException {
|
||||||
try {
|
try {
|
||||||
String output;
|
String output;
|
||||||
|
@ -580,6 +590,28 @@ public class Hwi {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class GetXpubsService extends Service<Map<StandardAccount, String>> {
|
||||||
|
private final Device device;
|
||||||
|
private final String passphrase;
|
||||||
|
private final Map<StandardAccount, String> accountDerivationPaths;
|
||||||
|
|
||||||
|
public GetXpubsService(Device device, String passphrase, Map<StandardAccount, String> accountDerivationPaths) {
|
||||||
|
this.device = device;
|
||||||
|
this.passphrase = passphrase;
|
||||||
|
this.accountDerivationPaths = accountDerivationPaths;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Task<Map<StandardAccount, String>> createTask() {
|
||||||
|
return new Task<>() {
|
||||||
|
protected Map<StandardAccount, String> call() throws ImportException {
|
||||||
|
Hwi hwi = new Hwi();
|
||||||
|
return hwi.getXpubs(device, passphrase, accountDerivationPaths);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static class SignPSBTService extends Service<PSBT> {
|
public static class SignPSBTService extends Service<PSBT> {
|
||||||
private final Device device;
|
private final Device device;
|
||||||
private final String passphrase;
|
private final String passphrase;
|
||||||
|
|
|
@ -1419,10 +1419,18 @@ public class ElectrumServer {
|
||||||
public static class WalletDiscoveryService extends Service<List<StandardAccount>> {
|
public static class WalletDiscoveryService extends Service<List<StandardAccount>> {
|
||||||
private final Wallet masterWalletCopy;
|
private final Wallet masterWalletCopy;
|
||||||
private final List<StandardAccount> standardAccounts;
|
private final List<StandardAccount> standardAccounts;
|
||||||
|
private final Map<StandardAccount, Keystore> importedKeystores;
|
||||||
|
|
||||||
public WalletDiscoveryService(Wallet masterWallet, List<StandardAccount> standardAccounts) {
|
public WalletDiscoveryService(Wallet masterWallet, List<StandardAccount> standardAccounts) {
|
||||||
this.masterWalletCopy = masterWallet.copy();
|
this.masterWalletCopy = masterWallet.copy();
|
||||||
this.standardAccounts = standardAccounts;
|
this.standardAccounts = standardAccounts;
|
||||||
|
this.importedKeystores = new HashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public WalletDiscoveryService(Wallet masterWallet, Map<StandardAccount, Keystore> importedKeystores) {
|
||||||
|
this.masterWalletCopy = masterWallet.copy();
|
||||||
|
this.standardAccounts = new ArrayList<>(importedKeystores.keySet());
|
||||||
|
this.importedKeystores = importedKeystores;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -1434,6 +1442,11 @@ public class ElectrumServer {
|
||||||
|
|
||||||
for(StandardAccount standardAccount : standardAccounts) {
|
for(StandardAccount standardAccount : standardAccounts) {
|
||||||
Wallet wallet = masterWalletCopy.addChildWallet(standardAccount);
|
Wallet wallet = masterWalletCopy.addChildWallet(standardAccount);
|
||||||
|
if(importedKeystores.containsKey(standardAccount)) {
|
||||||
|
wallet.getKeystores().clear();
|
||||||
|
wallet.getKeystores().add(importedKeystores.get(standardAccount));
|
||||||
|
}
|
||||||
|
|
||||||
Map<WalletNode, Set<BlockTransactionHash>> nodeTransactionMap = new TreeMap<>();
|
Map<WalletNode, Set<BlockTransactionHash>> nodeTransactionMap = new TreeMap<>();
|
||||||
electrumServer.getReferences(wallet, wallet.getNode(KeyPurpose.RECEIVE).getChildren(), nodeTransactionMap, 0);
|
electrumServer.getReferences(wallet, wallet.getNode(KeyPurpose.RECEIVE).getChildren(), nodeTransactionMap, 0);
|
||||||
if(nodeTransactionMap.values().stream().anyMatch(blockTransactionHashes -> !blockTransactionHashes.isEmpty())) {
|
if(nodeTransactionMap.values().stream().anyMatch(blockTransactionHashes -> !blockTransactionHashes.isEmpty())) {
|
||||||
|
|
|
@ -430,7 +430,8 @@ public class SettingsController extends WalletFormController implements Initiali
|
||||||
throw new IllegalStateException("Cannot export unsaved wallet");
|
throw new IllegalStateException("Cannot export unsaved wallet");
|
||||||
}
|
}
|
||||||
|
|
||||||
Optional<Wallet> optWallet = AppServices.get().getOpenWallets().entrySet().stream().filter(entry -> walletForm.getWalletFile().equals(entry.getValue().getWalletFile())).map(Map.Entry::getKey).findFirst();
|
Optional<Wallet> optWallet = AppServices.get().getOpenWallets().entrySet().stream()
|
||||||
|
.filter(entry -> walletForm.getWalletFile().equals(entry.getValue().getWalletFile()) && entry.getKey().isMasterWallet()).map(Map.Entry::getKey).findFirst();
|
||||||
if(optWallet.isPresent()) {
|
if(optWallet.isPresent()) {
|
||||||
Wallet wallet = optWallet.get();
|
Wallet wallet = optWallet.get();
|
||||||
if(!walletForm.getWallet().getName().equals(wallet.getName())) {
|
if(!walletForm.getWallet().getName().equals(wallet.getName())) {
|
||||||
|
@ -523,6 +524,21 @@ public class SettingsController extends WalletFormController implements Initiali
|
||||||
addAndSaveAccounts(masterWallet, standardAccounts);
|
addAndSaveAccounts(masterWallet, standardAccounts);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
if(discoverAccounts && masterWallet.getKeystores().size() == 1 && masterWallet.getKeystores().stream().allMatch(ks -> ks.getSource() == KeystoreSource.HW_USB)) {
|
||||||
|
String fingerprint = masterWallet.getKeystores().get(0).getKeyDerivation().getMasterFingerprint();
|
||||||
|
DeviceKeystoreDiscoverDialog deviceKeystoreDiscoverDialog = new DeviceKeystoreDiscoverDialog(List.of(fingerprint), masterWallet, standardAccounts);
|
||||||
|
Optional<Map<StandardAccount, Keystore>> optDiscoveredKeystores = deviceKeystoreDiscoverDialog.showAndWait();
|
||||||
|
if(optDiscoveredKeystores.isPresent()) {
|
||||||
|
Map<StandardAccount, Keystore> discoveredKeystores = optDiscoveredKeystores.get();
|
||||||
|
for(Map.Entry<StandardAccount, Keystore> entry : discoveredKeystores.entrySet()) {
|
||||||
|
Wallet childWallet = masterWallet.addChildWallet(entry.getKey());
|
||||||
|
childWallet.getKeystores().clear();
|
||||||
|
childWallet.getKeystores().add(entry.getValue());
|
||||||
|
EventManager.get().post(new ChildWalletAddedEvent(getWalletForm().getStorage(), masterWallet, childWallet));
|
||||||
|
}
|
||||||
|
saveChildWallets(masterWallet);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
for(StandardAccount standardAccount : standardAccounts) {
|
for(StandardAccount standardAccount : standardAccounts) {
|
||||||
Wallet childWallet = masterWallet.addChildWallet(standardAccount);
|
Wallet childWallet = masterWallet.addChildWallet(standardAccount);
|
||||||
|
@ -530,6 +546,7 @@ public class SettingsController extends WalletFormController implements Initiali
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void addAndEncryptAccounts(Wallet masterWallet, List<StandardAccount> standardAccounts, Key key) {
|
private void addAndEncryptAccounts(Wallet masterWallet, List<StandardAccount> standardAccounts, Key key) {
|
||||||
try {
|
try {
|
||||||
|
@ -559,6 +576,10 @@ public class SettingsController extends WalletFormController implements Initiali
|
||||||
EventManager.get().post(new ChildWalletAddedEvent(getWalletForm().getStorage(), masterWallet, childWallet));
|
EventManager.get().post(new ChildWalletAddedEvent(getWalletForm().getStorage(), masterWallet, childWallet));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
saveChildWallets(masterWallet);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveChildWallets(Wallet masterWallet) {
|
||||||
for(Wallet childWallet : masterWallet.getChildWallets()) {
|
for(Wallet childWallet : masterWallet.getChildWallets()) {
|
||||||
Storage storage = AppServices.get().getOpenWallets().get(childWallet);
|
Storage storage = AppServices.get().getOpenWallets().get(childWallet);
|
||||||
if(!storage.isPersisted(childWallet)) {
|
if(!storage.isPersisted(childWallet)) {
|
||||||
|
|
Loading…
Reference in a new issue