mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-12-24 12:46:45 +00:00
add bip44 account discovery
This commit is contained in:
parent
784fa5e1e8
commit
72cb696451
6 changed files with 143 additions and 49 deletions
|
@ -622,7 +622,7 @@ public class AppServices {
|
|||
}
|
||||
|
||||
public static Optional<ButtonType> 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<ButtonType> showAlertDialog(String title, String content, Alert.AlertType alertType, ButtonType... buttons) {
|
||||
|
|
|
@ -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<StandardAccount> {
|
||||
public class AddAccountDialog extends Dialog<List<StandardAccount>> {
|
||||
private final ComboBox<StandardAccount> standardAccountCombo;
|
||||
private boolean discoverAccounts = false;
|
||||
|
||||
public AddAccountDialog(Wallet wallet) {
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
|
@ -56,6 +58,16 @@ public class AddAccountDialog extends Dialog<StandardAccount> {
|
|||
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<StandardAccount> {
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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<Sha256Hash, BlockTransaction> blkTxEntry : wallet.getTransactions().entrySet()) {
|
||||
Map<Sha256Hash, BlockTransaction> walletTransactions = new HashMap<>(wallet.getTransactions());
|
||||
for(Map.Entry<Sha256Hash, BlockTransaction> blkTxEntry : walletTransactions.entrySet()) {
|
||||
blkTxEntry.getValue().setId(null);
|
||||
addOrUpdate(wallet, blkTxEntry.getKey(), blkTxEntry.getValue());
|
||||
}
|
||||
|
|
|
@ -1415,4 +1415,35 @@ public class ElectrumServer {
|
|||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static class WalletDiscoveryService extends Service<List<StandardAccount>> {
|
||||
private final Wallet masterWalletCopy;
|
||||
private final List<StandardAccount> standardAccounts;
|
||||
|
||||
public WalletDiscoveryService(Wallet masterWallet, List<StandardAccount> standardAccounts) {
|
||||
this.masterWalletCopy = masterWallet.copy();
|
||||
this.standardAccounts = standardAccounts;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Task<List<StandardAccount>> createTask() {
|
||||
return new Task<>() {
|
||||
protected List<StandardAccount> call() throws ServerException {
|
||||
ElectrumServer electrumServer = new ElectrumServer();
|
||||
List<StandardAccount> discoveredAccounts = new ArrayList<>();
|
||||
|
||||
for(StandardAccount standardAccount : standardAccounts) {
|
||||
Wallet wallet = masterWalletCopy.addChildWallet(standardAccount);
|
||||
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())) {
|
||||
discoveredAccounts.add(standardAccount);
|
||||
}
|
||||
}
|
||||
|
||||
return discoveredAccounts;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<StandardAccount> optAccount = addAccountDialog.showAndWait();
|
||||
if(optAccount.isPresent()) {
|
||||
StandardAccount standardAccount = optAccount.get();
|
||||
Optional<List<StandardAccount>> optAccounts = addAccountDialog.showAndWait();
|
||||
if(optAccounts.isPresent()) {
|
||||
List<StandardAccount> 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<SecureString> 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<StandardAccount> 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<SecureString> 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<ButtonType> 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<ButtonType> 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<StandardAccount> 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<StandardAccount> 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());
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue