add bip44 account discovery

This commit is contained in:
Craig Raw 2021-10-28 16:44:36 +02:00
parent 784fa5e1e8
commit 72cb696451
6 changed files with 143 additions and 49 deletions

View file

@ -622,7 +622,7 @@ public class AppServices {
} }
public static Optional<ButtonType> showErrorDialog(String title, String content, ButtonType... buttons) { 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) { public static Optional<ButtonType> showAlertDialog(String title, String content, Alert.AlertType alertType, ButtonType... buttons) {

View file

@ -1,5 +1,6 @@
package com.sparrowwallet.sparrow.control; package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.wallet.KeystoreSource;
import com.sparrowwallet.drongo.wallet.StandardAccount; 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;
@ -14,8 +15,9 @@ import org.controlsfx.glyphfont.Glyph;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
public class AddAccountDialog extends Dialog<StandardAccount> { public class AddAccountDialog extends Dialog<List<StandardAccount>> {
private final ComboBox<StandardAccount> standardAccountCombo; private final ComboBox<StandardAccount> standardAccountCombo;
private boolean discoverAccounts = false;
public AddAccountDialog(Wallet wallet) { public AddAccountDialog(Wallet wallet) {
final DialogPane dialogPane = getDialogPane(); final DialogPane dialogPane = getDialogPane();
@ -56,6 +58,16 @@ public class AddAccountDialog extends Dialog<StandardAccount> {
availableAccounts.add(StandardAccount.WHIRLPOOL_PREMIX); 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.setItems(FXCollections.observableList(availableAccounts));
standardAccountCombo.setConverter(new StringConverter<>() { standardAccountCombo.setConverter(new StringConverter<>() {
@Override @Override
@ -86,6 +98,10 @@ public class AddAccountDialog extends Dialog<StandardAccount> {
content.getChildren().add(standardAccountCombo); content.getChildren().add(standardAccountCombo);
dialogPane.setContent(content); 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;
} }
} }

View file

@ -400,7 +400,7 @@ public class TransactionDiagram extends GridPane {
WalletNode toNode = walletTx.getWallet() != null ? walletTx.getWallet().getWalletAddresses().get(payment.getAddress()) : null; WalletNode toNode = walletTx.getWallet() != null ? walletTx.getWallet().getWalletAddresses().get(payment.getAddress()) : null;
Tooltip recipientTooltip = new Tooltip((toWallet == null ? (toNode != null ? "Consolidate " : "Pay ") : "Receive ") Tooltip recipientTooltip = new Tooltip((toWallet == null ? (toNode != null ? "Consolidate " : "Pay ") : "Receive ")
+ getSatsValue(payment.getAmount()) + " sats to " + 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.getStyleClass().add("recipient-label");
recipientTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY)); recipientTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
recipientLabel.setTooltip(recipientTooltip); recipientLabel.setTooltip(recipientTooltip);

View file

@ -10,6 +10,7 @@ import org.jdbi.v3.sqlobject.statement.SqlQuery;
import org.jdbi.v3.sqlobject.statement.SqlUpdate; import org.jdbi.v3.sqlobject.statement.SqlUpdate;
import java.util.Date; import java.util.Date;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
public interface BlockTransactionDao { public interface BlockTransactionDao {
@ -35,7 +36,8 @@ public interface BlockTransactionDao {
void clear(long wallet); void clear(long wallet);
default void addBlockTransactions(Wallet 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); blkTxEntry.getValue().setId(null);
addOrUpdate(wallet, blkTxEntry.getKey(), blkTxEntry.getValue()); addOrUpdate(wallet, blkTxEntry.getKey(), blkTxEntry.getValue());
} }

View file

@ -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;
}
};
}
}
} }

View file

@ -15,6 +15,7 @@ import com.sparrowwallet.sparrow.control.*;
import com.sparrowwallet.sparrow.event.*; import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.io.Storage; import com.sparrowwallet.sparrow.io.Storage;
import com.sparrowwallet.sparrow.io.StorageException; import com.sparrowwallet.sparrow.io.StorageException;
import com.sparrowwallet.sparrow.net.ElectrumServer;
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolServices; import com.sparrowwallet.sparrow.whirlpool.WhirlpoolServices;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleIntegerProperty;
@ -452,10 +453,18 @@ public class SettingsController extends WalletFormController implements Initiali
Wallet masterWallet = openWallet.isMasterWallet() ? openWallet : openWallet.getMasterWallet(); Wallet masterWallet = openWallet.isMasterWallet() ? openWallet : openWallet.getMasterWallet();
AddAccountDialog addAccountDialog = new AddAccountDialog(masterWallet); AddAccountDialog addAccountDialog = new AddAccountDialog(masterWallet);
Optional<StandardAccount> optAccount = addAccountDialog.showAndWait(); Optional<List<StandardAccount>> optAccounts = addAccountDialog.showAndWait();
if(optAccount.isPresent()) { if(optAccounts.isPresent()) {
StandardAccount standardAccount = optAccount.get(); List<StandardAccount> standardAccounts = optAccounts.get();
if(addAccountDialog.isDiscoverAccounts() && !AppServices.isConnected()) {
return;
}
addAccounts(masterWallet, standardAccounts, addAccountDialog.isDiscoverAccounts());
}
}
private void addAccounts(Wallet masterWallet, List<StandardAccount> standardAccounts, boolean discoverAccounts) {
if(masterWallet.getKeystores().stream().allMatch(ks -> ks.getSource() == KeystoreSource.SW_SEED)) { if(masterWallet.getKeystores().stream().allMatch(ks -> ks.getSource() == KeystoreSource.SW_SEED)) {
if(masterWallet.isEncrypted()) { if(masterWallet.isEncrypted()) {
String walletId = walletForm.getWalletId(); String walletId = walletForm.getWalletId();
@ -467,20 +476,22 @@ public class SettingsController extends WalletFormController implements Initiali
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Done")); EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Done"));
ECKey encryptionFullKey = keyDerivationService.getValue(); ECKey encryptionFullKey = keyDerivationService.getValue();
Key key = new Key(encryptionFullKey.getPrivKeyBytes(), walletForm.getStorage().getKeyDeriver().getSalt(), EncryptionType.Deriver.ARGON2); Key key = new Key(encryptionFullKey.getPrivKeyBytes(), walletForm.getStorage().getKeyDeriver().getSalt(), EncryptionType.Deriver.ARGON2);
encryptionFullKey.clear();
masterWallet.decrypt(key); masterWallet.decrypt(key);
try { if(discoverAccounts) {
addAndSaveAccount(masterWallet, standardAccount); ElectrumServer.WalletDiscoveryService walletDiscoveryService = new ElectrumServer.WalletDiscoveryService(masterWallet, standardAccounts);
} finally { walletDiscoveryService.setOnSucceeded(event -> {
masterWallet.encrypt(key); addAndEncryptAccounts(masterWallet, walletDiscoveryService.getValue(), key);
for(Wallet childWallet : masterWallet.getChildWallets()) { });
if(!childWallet.isEncrypted()) { walletDiscoveryService.setOnFailed(event -> {
childWallet.encrypt(key); log.error("Failed to discover accounts", event.getSource().getException());
} addAndEncryptAccounts(masterWallet, Collections.emptyList(), key);
} AppServices.showErrorDialog("Failed to discover accounts", event.getSource().getException().getMessage());
key.clear(); });
encryptionFullKey.clear(); walletDiscoveryService.start();
password.get().clear(); } else {
addAndEncryptAccounts(masterWallet, standardAccounts, key);
} }
}); });
keyDerivationService.setOnFailed(workerStateEvent -> { keyDerivationService.setOnFailed(workerStateEvent -> {
@ -498,15 +509,48 @@ public class SettingsController extends WalletFormController implements Initiali
keyDerivationService.start(); keyDerivationService.start();
} }
} else { } else {
addAndSaveAccount(masterWallet, standardAccount); 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 { } else {
for(StandardAccount standardAccount : standardAccounts) {
Wallet childWallet = masterWallet.addChildWallet(standardAccount); Wallet childWallet = masterWallet.addChildWallet(standardAccount);
EventManager.get().post(new ChildWalletAddedEvent(getWalletForm().getStorage(), masterWallet, childWallet)); 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) { private void addAndSaveAccount(Wallet masterWallet, StandardAccount standardAccount) {
if(StandardAccount.WHIRLPOOL_ACCOUNTS.contains(standardAccount)) { if(StandardAccount.WHIRLPOOL_ACCOUNTS.contains(standardAccount)) {
WhirlpoolServices.prepareWhirlpoolWallet(masterWallet, getWalletForm().getWalletId(), getWalletForm().getStorage()); WhirlpoolServices.prepareWhirlpoolWallet(masterWallet, getWalletForm().getWalletId(), getWalletForm().getStorage());
@ -521,6 +565,7 @@ public class SettingsController extends WalletFormController implements Initiali
try { try {
storage.saveWallet(childWallet); storage.saveWallet(childWallet);
} catch(Exception e) { } catch(Exception e) {
log.error("Error saving wallet", e);
AppServices.showErrorDialog("Error saving wallet " + childWallet.getName(), e.getMessage()); AppServices.showErrorDialog("Error saving wallet " + childWallet.getName(), e.getMessage());
} }
} }