remove tmp backup approach for retaining labels over wallet restarts while refreshing, replaced by detached labels

This commit is contained in:
Craig Raw 2022-02-09 16:09:12 +02:00
parent dd7a3a6c8a
commit 2ca286d826
14 changed files with 97 additions and 260 deletions

2
drongo

@ -1 +1 @@
Subproject commit 78359961f369f4f8b016973529241048f2ef216d
Subproject commit f73cabad3c76c1eb28b4f02b17c9beb608ba2aa4

View file

@ -861,7 +861,7 @@ public class AppController implements Initializable {
File walletFile = Storage.getWalletFile(nameAndBirthDate.getName());
Storage storage = new Storage(walletFile);
Wallet wallet = new Wallet(nameAndBirthDate.getName(), PolicyType.SINGLE, ScriptType.P2WPKH, nameAndBirthDate.getBirthDate());
addWalletTabOrWindow(storage, wallet, null, false);
addWalletTabOrWindow(storage, wallet, false);
}
}
@ -888,8 +888,8 @@ public class AppController implements Initializable {
if(!storage.isEncrypted()) {
Storage.LoadWalletService loadWalletService = new Storage.LoadWalletService(storage);
loadWalletService.setOnSucceeded(workerStateEvent -> {
WalletBackupAndKey walletBackupAndKey = loadWalletService.getValue();
openWallet(storage, walletBackupAndKey, this, forceSameWindow);
WalletAndKey walletAndKey = loadWalletService.getValue();
openWallet(storage, walletAndKey, this, forceSameWindow);
});
loadWalletService.setOnFailed(workerStateEvent -> {
Throwable exception = workerStateEvent.getSource().getException();
@ -912,8 +912,8 @@ public class AppController implements Initializable {
Storage.LoadWalletService loadWalletService = new Storage.LoadWalletService(storage, password);
loadWalletService.setOnSucceeded(workerStateEvent -> {
EventManager.get().post(new StorageEvent(storage.getWalletId(null), TimedEvent.Action.END, "Done"));
WalletBackupAndKey walletBackupAndKey = loadWalletService.getValue();
openWallet(storage, walletBackupAndKey, this, forceSameWindow);
WalletAndKey walletAndKey = loadWalletService.getValue();
openWallet(storage, walletAndKey, this, forceSameWindow);
});
loadWalletService.setOnFailed(workerStateEvent -> {
EventManager.get().post(new StorageEvent(storage.getWalletId(null), TimedEvent.Action.END, "Failed"));
@ -947,23 +947,23 @@ public class AppController implements Initializable {
}
}
private void openWallet(Storage storage, WalletBackupAndKey walletBackupAndKey, AppController appController, boolean forceSameWindow) {
private void openWallet(Storage storage, WalletAndKey walletAndKey, AppController appController, boolean forceSameWindow) {
try {
checkWalletNetwork(walletBackupAndKey.getWallet());
restorePublicKeysFromSeed(storage, walletBackupAndKey.getWallet(), walletBackupAndKey.getKey());
if(!walletBackupAndKey.getWallet().isValid()) {
checkWalletNetwork(walletAndKey.getWallet());
restorePublicKeysFromSeed(storage, walletAndKey.getWallet(), walletAndKey.getKey());
if(!walletAndKey.getWallet().isValid()) {
throw new IllegalStateException("Wallet file is not valid.");
}
AppController walletAppController = appController.addWalletTabOrWindow(storage, walletBackupAndKey.getWallet(), walletBackupAndKey.getBackupWallet(), forceSameWindow);
for(Map.Entry<WalletBackupAndKey, Storage> entry : walletBackupAndKey.getChildWallets().entrySet()) {
AppController walletAppController = appController.addWalletTabOrWindow(storage, walletAndKey.getWallet(), forceSameWindow);
for(Map.Entry<WalletAndKey, Storage> entry : walletAndKey.getChildWallets().entrySet()) {
openWallet(entry.getValue(), entry.getKey(), walletAppController, true);
}
Platform.runLater(() -> selectTab(walletBackupAndKey.getWallet()));
Platform.runLater(() -> selectTab(walletAndKey.getWallet()));
} catch(Exception e) {
log.error("Error opening wallet", e);
showErrorDialog("Error Opening Wallet", e.getMessage());
} finally {
walletBackupAndKey.clear();
walletAndKey.clear();
}
}
@ -1141,13 +1141,13 @@ public class AppController implements Initializable {
storage.saveWallet(wallet);
checkWalletNetwork(wallet);
restorePublicKeysFromSeed(storage, wallet, null);
addWalletTabOrWindow(storage, wallet, null, false);
addWalletTabOrWindow(storage, wallet, false);
for(Wallet childWallet : wallet.getChildWallets()) {
storage.saveWallet(childWallet);
checkWalletNetwork(childWallet);
restorePublicKeysFromSeed(storage, childWallet, null);
addWalletTabOrWindow(storage, childWallet, null, false);
addWalletTabOrWindow(storage, childWallet, false);
}
Platform.runLater(() -> selectTab(wallet));
} catch(IOException | StorageException | MnemonicException e) {
@ -1168,14 +1168,14 @@ public class AppController implements Initializable {
storage.saveWallet(wallet);
checkWalletNetwork(wallet);
restorePublicKeysFromSeed(storage, wallet, key);
addWalletTabOrWindow(storage, wallet, null, false);
addWalletTabOrWindow(storage, wallet, false);
for(Wallet childWallet : wallet.getChildWallets()) {
childWallet.encrypt(key);
storage.saveWallet(childWallet);
checkWalletNetwork(childWallet);
restorePublicKeysFromSeed(storage, childWallet, key);
addWalletTabOrWindow(storage, childWallet, null, false);
addWalletTabOrWindow(storage, childWallet, false);
}
Platform.runLater(() -> selectTab(wallet));
} catch(IOException | StorageException | MnemonicException e) {
@ -1389,14 +1389,13 @@ public class AppController implements Initializable {
if(selectedWalletForm != null) {
Wallet wallet = selectedWalletForm.getWallet();
Wallet pastWallet = wallet.copy();
selectedWalletForm.getStorage().backupTempWallet();
wallet.clearHistory();
AppServices.clearTransactionHistoryCache(wallet);
EventManager.get().post(new WalletHistoryClearedEvent(wallet, pastWallet, selectedWalletForm.getWalletId()));
}
}
public AppController addWalletTabOrWindow(Storage storage, Wallet wallet, Wallet backupWallet, boolean forceSameWindow) {
public AppController addWalletTabOrWindow(Storage storage, Wallet wallet, boolean forceSameWindow) {
Window existingWalletWindow = AppServices.get().getWindowForWallet(storage.getWalletId(wallet));
if(existingWalletWindow instanceof Stage) {
Stage existingWalletStage = (Stage)existingWalletWindow;
@ -1411,15 +1410,15 @@ public class AppController implements Initializable {
AppController appController = AppServices.newAppWindow(stage);
stage.toFront();
stage.setX(AppServices.get().getWalletWindowMaxX() + 30);
appController.addWalletTab(storage, wallet, backupWallet);
appController.addWalletTab(storage, wallet);
return appController;
} else {
addWalletTab(storage, wallet, backupWallet);
addWalletTab(storage, wallet);
return this;
}
}
public void addWalletTab(Storage storage, Wallet wallet, Wallet backupWallet) {
public void addWalletTab(Storage storage, Wallet wallet) {
if(wallet.isMasterWallet()) {
String name = storage.getWalletName(wallet);
if(!name.equals(wallet.getName())) {
@ -1449,7 +1448,7 @@ public class AppController implements Initializable {
subTabs.rotateGraphicProperty().set(true);
tab.setContent(subTabs);
WalletForm walletForm = addWalletSubTab(subTabs, storage, wallet, backupWallet);
WalletForm walletForm = addWalletSubTab(subTabs, storage, wallet);
TabData tabData = new WalletTabData(TabData.TabType.WALLET, walletForm);
tab.setUserData(tabData);
tab.setContextMenu(getTabContextMenu(tab));
@ -1478,7 +1477,7 @@ public class AppController implements Initializable {
WalletTabData walletTabData = (WalletTabData)tabData;
if(walletTabData.getWallet() == wallet.getMasterWallet()) {
TabPane subTabs = (TabPane)walletTab.getContent();
addWalletSubTab(subTabs, storage, wallet, backupWallet);
addWalletSubTab(subTabs, storage, wallet);
Tab masterTab = subTabs.getTabs().stream().filter(tab -> ((WalletTabData)tab.getUserData()).getWallet().isMasterWallet()).findFirst().orElse(subTabs.getTabs().get(0));
Label masterLabel = (Label)masterTab.getGraphic();
masterLabel.setText(wallet.getMasterWallet().getLabel() != null ? wallet.getMasterWallet().getLabel() : wallet.getMasterWallet().getAutomaticName());
@ -1507,7 +1506,7 @@ public class AppController implements Initializable {
}
}
public WalletForm addWalletSubTab(TabPane subTabs, Storage storage, Wallet wallet, Wallet backupWallet) {
public WalletForm addWalletSubTab(TabPane subTabs, Storage storage, Wallet wallet) {
try {
Tab subTab = new Tab();
subTab.setClosable(false);
@ -1525,7 +1524,7 @@ public class AppController implements Initializable {
EventManager.get().post(new WalletOpeningEvent(storage, wallet));
//Note that only one WalletForm is created per wallet tab, and registered to listen for events. All wallet controllers (except SettingsController) share this instance.
WalletForm walletForm = new WalletForm(storage, wallet, backupWallet);
WalletForm walletForm = new WalletForm(storage, wallet);
EventManager.get().register(walletForm);
controller.setWalletForm(walletForm);
@ -2478,7 +2477,7 @@ public class AppController implements Initializable {
throw new IllegalStateException("Cannot find storage for master wallet");
}
addWalletTab(storage, event.getChildWallet(), null);
addWalletTab(storage, event.getChildWallet());
}
@Subscribe

View file

@ -86,7 +86,6 @@ public class CoinTreeTable extends TreeTableView<Entry> {
if(optDate.isPresent()) {
Storage storage = AppServices.get().getOpenWallets().get(wallet);
Wallet pastWallet = wallet.copy();
storage.backupTempWallet();
wallet.setBirthDate(optDate.get());
//Trigger background save of birthdate
EventManager.get().post(new WalletDataChangedEvent(wallet));

View file

@ -36,29 +36,26 @@ public class JsonPersistence implements Persistence {
}
@Override
public WalletBackupAndKey loadWallet(Storage storage) throws IOException, StorageException {
public WalletAndKey loadWallet(Storage storage) throws IOException, StorageException {
Wallet wallet;
try(Reader reader = new FileReader(storage.getWalletFile())) {
wallet = gson.fromJson(reader, Wallet.class);
}
Map<WalletBackupAndKey, Storage> childWallets = loadChildWallets(storage, wallet, null);
wallet.setChildWallets(childWallets.keySet().stream().map(WalletBackupAndKey::getWallet).collect(Collectors.toList()));
Map<WalletAndKey, Storage> childWallets = loadChildWallets(storage, wallet, null);
wallet.setChildWallets(childWallets.keySet().stream().map(WalletAndKey::getWallet).collect(Collectors.toList()));
File backupFile = storage.getTempBackup();
Wallet backupWallet = backupFile == null ? null : loadWallet(backupFile, null);
return new WalletBackupAndKey(wallet, backupWallet, null, null, childWallets);
return new WalletAndKey(wallet, null, null, childWallets);
}
@Override
public WalletBackupAndKey loadWallet(Storage storage, CharSequence password) throws IOException, StorageException {
public WalletAndKey loadWallet(Storage storage, CharSequence password) throws IOException, StorageException {
return loadWallet(storage, password, null);
}
@Override
public WalletBackupAndKey loadWallet(Storage storage, CharSequence password, ECKey alreadyDerivedKey) throws IOException, StorageException {
public WalletAndKey loadWallet(Storage storage, CharSequence password, ECKey alreadyDerivedKey) throws IOException, StorageException {
Wallet wallet;
ECKey encryptionKey;
@ -68,25 +65,22 @@ public class JsonPersistence implements Persistence {
wallet = gson.fromJson(reader, Wallet.class);
}
Map<WalletBackupAndKey, Storage> childWallets = loadChildWallets(storage, wallet, encryptionKey);
wallet.setChildWallets(childWallets.keySet().stream().map(WalletBackupAndKey::getWallet).collect(Collectors.toList()));
Map<WalletAndKey, Storage> childWallets = loadChildWallets(storage, wallet, encryptionKey);
wallet.setChildWallets(childWallets.keySet().stream().map(WalletAndKey::getWallet).collect(Collectors.toList()));
File backupFile = storage.getTempBackup();
Wallet backupWallet = backupFile == null ? null : loadWallet(backupFile, encryptionKey);
return new WalletBackupAndKey(wallet, backupWallet, encryptionKey, keyDeriver, childWallets);
return new WalletAndKey(wallet, encryptionKey, keyDeriver, childWallets);
}
private Map<WalletBackupAndKey, Storage> loadChildWallets(Storage storage, Wallet masterWallet, ECKey encryptionKey) throws IOException, StorageException {
private Map<WalletAndKey, Storage> loadChildWallets(Storage storage, Wallet masterWallet, ECKey encryptionKey) throws IOException, StorageException {
File[] walletFiles = getChildWalletFiles(storage.getWalletFile(), masterWallet);
Map<WalletBackupAndKey, Storage> childWallets = new TreeMap<>();
Map<WalletAndKey, Storage> childWallets = new TreeMap<>();
for(File childFile : walletFiles) {
Wallet childWallet = loadWallet(childFile, encryptionKey);
Storage childStorage = new Storage(childFile);
childStorage.setEncryptionPubKey(encryptionKey == null ? Storage.NO_PASSWORD_KEY : ECKey.fromPublicOnly(encryptionKey));
childStorage.setKeyDeriver(getKeyDeriver());
childWallet.setMasterWallet(masterWallet);
childWallets.put(new WalletBackupAndKey(childWallet, null, encryptionKey, keyDeriver, Collections.emptyMap()), storage);
childWallets.put(new WalletAndKey(childWallet, encryptionKey, keyDeriver, Collections.emptyMap()), storage);
}
return childWallets;

View file

@ -9,9 +9,9 @@ import java.io.IOException;
import java.io.OutputStream;
public interface Persistence {
WalletBackupAndKey loadWallet(Storage storage) throws IOException, StorageException;
WalletBackupAndKey loadWallet(Storage storage, CharSequence password) throws IOException, StorageException;
WalletBackupAndKey loadWallet(Storage storage, CharSequence password, ECKey alreadyDerivedKey) throws IOException, StorageException;
WalletAndKey loadWallet(Storage storage) throws IOException, StorageException;
WalletAndKey loadWallet(Storage storage, CharSequence password) throws IOException, StorageException;
WalletAndKey loadWallet(Storage storage, CharSequence password, ECKey alreadyDerivedKey) throws IOException, StorageException;
File storeWallet(Storage storage, Wallet wallet) throws IOException, StorageException;
File storeWallet(Storage storage, Wallet wallet, ECKey encryptionPubKey) throws IOException, StorageException;
void updateWallet(Storage storage, Wallet wallet) throws IOException, StorageException;

View file

@ -111,10 +111,10 @@ public class Sparrow implements WalletImport, WalletExport {
if(!isEncrypted(tempFile)) {
wallet = storage.loadUnencryptedWallet().getWallet();
} else {
WalletBackupAndKey walletBackupAndKey = storage.loadEncryptedWallet(password);
wallet = walletBackupAndKey.getWallet();
wallet.decrypt(walletBackupAndKey.getKey());
for(Map.Entry<WalletBackupAndKey, Storage> entry : walletBackupAndKey.getChildWallets().entrySet()) {
WalletAndKey walletAndKey = storage.loadEncryptedWallet(password);
wallet = walletAndKey.getWallet();
wallet.decrypt(walletAndKey.getKey());
for(Map.Entry<WalletAndKey, Storage> entry : walletAndKey.getChildWallets().entrySet()) {
entry.getKey().getWallet().decrypt(entry.getKey().getKey());
}
}

View file

@ -20,7 +20,6 @@ import java.nio.file.attribute.PosixFilePermissions;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.regex.Matcher;
@ -38,7 +37,6 @@ public class Storage {
public static final String WALLETS_DIR = "wallets";
public static final String WALLETS_BACKUP_DIR = "backup";
public static final String CERTS_DIR = "certs";
public static final String TEMP_BACKUP_PREFIX = "tmp";
public static final List<String> RESERVED_WALLET_NAMES = List.of("temp");
private Persistence persistence;
@ -87,14 +85,14 @@ public class Storage {
return "";
}
public WalletBackupAndKey loadUnencryptedWallet() throws IOException, StorageException {
WalletBackupAndKey masterWalletAndKey = persistence.loadWallet(this);
public WalletAndKey loadUnencryptedWallet() throws IOException, StorageException {
WalletAndKey masterWalletAndKey = persistence.loadWallet(this);
encryptionPubKey = NO_PASSWORD_KEY;
return migrateToDb(masterWalletAndKey);
}
public WalletBackupAndKey loadEncryptedWallet(CharSequence password) throws IOException, StorageException {
WalletBackupAndKey masterWalletAndKey = persistence.loadWallet(this, password);
public WalletAndKey loadEncryptedWallet(CharSequence password) throws IOException, StorageException {
WalletAndKey masterWalletAndKey = persistence.loadWallet(this, password);
encryptionPubKey = ECKey.fromPublicOnly(masterWalletAndKey.getEncryptionKey());
return migrateToDb(masterWalletAndKey);
}
@ -136,14 +134,6 @@ public class Storage {
}
}
public void backupTempWallet() {
try {
backupWallet(TEMP_BACKUP_PREFIX);
} catch(IOException e) {
log.error("Error creating " + TEMP_BACKUP_PREFIX + " backup wallet", e);
}
}
private void backupWallet(String prefix) throws IOException {
File backupDir = getWalletsBackupDir();
@ -174,16 +164,6 @@ public class Storage {
deleteBackups(null);
}
public void deleteTempBackups(boolean forceSave) {
File[] backups = getBackups(Storage.TEMP_BACKUP_PREFIX);
if(backups.length > 0 && (forceSave || hasStartedSince(backups[0]))) {
File permanent = new File(backups[0].getParent(), backups[0].getName().substring(Storage.TEMP_BACKUP_PREFIX.length() + 1));
backups[0].renameTo(permanent);
}
deleteBackups(Storage.TEMP_BACKUP_PREFIX);
}
private boolean hasStartedSince(File lastBackup) {
try {
Date date = BACKUP_DATE_FORMAT.parse(getBackupDate(lastBackup.getName()));
@ -202,11 +182,6 @@ public class Storage {
}
}
public File getTempBackup() {
File[] backups = getBackups(TEMP_BACKUP_PREFIX);
return backups.length == 0 ? null : backups[0];
}
File[] getBackups(String prefix) {
File backupDir = getWalletsBackupDir();
String walletName = persistence.getWalletName(walletFile, null);
@ -232,7 +207,7 @@ public class Storage {
return null;
}
private WalletBackupAndKey migrateToDb(WalletBackupAndKey masterWalletAndKey) throws IOException, StorageException {
private WalletAndKey migrateToDb(WalletAndKey masterWalletAndKey) throws IOException, StorageException {
if(getType() == PersistenceType.JSON) {
log.info("Migrating " + masterWalletAndKey.getWallet().getName() + " from JSON to DB persistence");
masterWalletAndKey = migrateType(PersistenceType.DB, masterWalletAndKey.getWallet(), masterWalletAndKey.getEncryptionKey());
@ -241,7 +216,7 @@ public class Storage {
return masterWalletAndKey;
}
private WalletBackupAndKey migrateType(PersistenceType type, Wallet wallet, ECKey encryptionKey) throws IOException, StorageException {
private WalletAndKey migrateType(PersistenceType type, Wallet wallet, ECKey encryptionKey) throws IOException, StorageException {
File existingFile = walletFile;
try {
@ -530,7 +505,7 @@ public class Storage {
return ownerOnly;
}
public static class LoadWalletService extends Service<WalletBackupAndKey> {
public static class LoadWalletService extends Service<WalletAndKey> {
private final Storage storage;
private final SecureString password;
@ -545,19 +520,19 @@ public class Storage {
}
@Override
protected Task<WalletBackupAndKey> createTask() {
protected Task<WalletAndKey> createTask() {
return new Task<>() {
protected WalletBackupAndKey call() throws IOException, StorageException {
WalletBackupAndKey walletBackupAndKey;
protected WalletAndKey call() throws IOException, StorageException {
WalletAndKey walletAndKey;
if(password != null) {
walletBackupAndKey = storage.loadEncryptedWallet(password);
walletAndKey = storage.loadEncryptedWallet(password);
password.clear();
} else {
walletBackupAndKey = storage.loadUnencryptedWallet();
walletAndKey = storage.loadUnencryptedWallet();
}
return walletBackupAndKey;
return walletAndKey;
}
};
}

View file

@ -5,16 +5,14 @@ import com.sparrowwallet.drongo.wallet.Wallet;
import java.util.Map;
public class WalletBackupAndKey implements Comparable<WalletBackupAndKey> {
public class WalletAndKey implements Comparable<WalletAndKey> {
private final Wallet wallet;
private final Wallet backupWallet;
private final ECKey encryptionKey;
private final Key key;
private final Map<WalletBackupAndKey, Storage> childWallets;
private final Map<WalletAndKey, Storage> childWallets;
public WalletBackupAndKey(Wallet wallet, Wallet backupWallet, ECKey encryptionKey, AsymmetricKeyDeriver keyDeriver, Map<WalletBackupAndKey, Storage> childWallets) {
public WalletAndKey(Wallet wallet, ECKey encryptionKey, AsymmetricKeyDeriver keyDeriver, Map<WalletAndKey, Storage> childWallets) {
this.wallet = wallet;
this.backupWallet = backupWallet;
this.encryptionKey = encryptionKey;
this.key = encryptionKey == null ? null : new Key(encryptionKey.getPrivKeyBytes(), keyDeriver.getSalt(), EncryptionType.Deriver.ARGON2);
this.childWallets = childWallets;
@ -24,10 +22,6 @@ public class WalletBackupAndKey implements Comparable<WalletBackupAndKey> {
return wallet;
}
public Wallet getBackupWallet() {
return backupWallet;
}
public ECKey getEncryptionKey() {
return encryptionKey;
}
@ -36,7 +30,7 @@ public class WalletBackupAndKey implements Comparable<WalletBackupAndKey> {
return key;
}
public Map<WalletBackupAndKey, Storage> getChildWallets() {
public Map<WalletAndKey, Storage> getChildWallets() {
return childWallets;
}
@ -50,7 +44,7 @@ public class WalletBackupAndKey implements Comparable<WalletBackupAndKey> {
}
@Override
public int compareTo(WalletBackupAndKey other) {
public int compareTo(WalletAndKey other) {
return wallet.compareTo(other.wallet);
}
}

View file

@ -66,17 +66,17 @@ public class DbPersistence implements Persistence {
}
@Override
public WalletBackupAndKey loadWallet(Storage storage) throws IOException, StorageException {
public WalletAndKey loadWallet(Storage storage) throws IOException, StorageException {
return loadWallet(storage, null, null);
}
@Override
public WalletBackupAndKey loadWallet(Storage storage, CharSequence password) throws IOException, StorageException {
public WalletAndKey loadWallet(Storage storage, CharSequence password) throws IOException, StorageException {
return loadWallet(storage, password, null);
}
@Override
public WalletBackupAndKey loadWallet(Storage storage, CharSequence password, ECKey alreadyDerivedKey) throws IOException, StorageException {
public WalletAndKey loadWallet(Storage storage, CharSequence password, ECKey alreadyDerivedKey) throws IOException, StorageException {
ECKey encryptionKey = getEncryptionKey(password, storage.getWalletFile(), alreadyDerivedKey);
migrate(storage, MASTER_SCHEMA, encryptionKey);
@ -87,31 +87,22 @@ public class DbPersistence implements Persistence {
return walletDao.getMainWallet(MASTER_SCHEMA);
});
File backupFile = storage.getTempBackup();
Wallet backupWallet = null;
if(backupFile != null) {
Persistence backupPersistence = PersistenceType.DB.getInstance();
backupPersistence.setKeyDeriver(keyDeriver);
backupWallet = backupPersistence.loadWallet(new Storage(backupPersistence, backupFile), password, encryptionKey).getWallet();
backupPersistence.close();
}
Map<WalletBackupAndKey, Storage> childWallets = loadChildWallets(storage, masterWallet, backupWallet, encryptionKey);
masterWallet.setChildWallets(childWallets.keySet().stream().map(WalletBackupAndKey::getWallet).collect(Collectors.toList()));
Map<WalletAndKey, Storage> childWallets = loadChildWallets(storage, masterWallet, encryptionKey);
masterWallet.setChildWallets(childWallets.keySet().stream().map(WalletAndKey::getWallet).collect(Collectors.toList()));
createUpdateExecutor(masterWallet);
return new WalletBackupAndKey(masterWallet, backupWallet, encryptionKey, keyDeriver, childWallets);
return new WalletAndKey(masterWallet, encryptionKey, keyDeriver, childWallets);
}
private Map<WalletBackupAndKey, Storage> loadChildWallets(Storage storage, Wallet masterWallet, Wallet backupWallet, ECKey encryptionKey) throws StorageException {
private Map<WalletAndKey, Storage> loadChildWallets(Storage storage, Wallet masterWallet, ECKey encryptionKey) throws StorageException {
Jdbi jdbi = getJdbi(storage, getFilePassword(encryptionKey));
List<String> schemas = jdbi.withHandle(handle -> {
return handle.createQuery("show schemas").mapTo(String.class).list();
});
List<String> childSchemas = schemas.stream().filter(schema -> schema.startsWith(WALLET_SCHEMA_PREFIX) && !schema.equals(MASTER_SCHEMA)).collect(Collectors.toList());
Map<WalletBackupAndKey, Storage> childWallets = new TreeMap<>();
Map<WalletAndKey, Storage> childWallets = new TreeMap<>();
for(String schema : childSchemas) {
migrate(storage, schema, encryptionKey);
@ -123,8 +114,7 @@ public class DbPersistence implements Persistence {
childWallet.setMasterWallet(masterWallet);
return childWallet;
});
Wallet backupChildWallet = backupWallet == null ? null : backupWallet.getChildWallets().stream().filter(child -> wallet.getName().equals(child.getName())).findFirst().orElse(null);
childWallets.put(new WalletBackupAndKey(wallet, backupChildWallet, encryptionKey, keyDeriver, Collections.emptyMap()), storage);
childWallets.put(new WalletAndKey(wallet, encryptionKey, keyDeriver, Collections.emptyMap()), storage);
}
return childWallets;

View file

@ -90,45 +90,4 @@ public class NodeEntry extends Entry implements Comparable<NodeEntry> {
public int compareTo(NodeEntry other) {
return node.compareTo(other.node);
}
public Set<Entry> copyLabels(WalletNode pastNode) {
if(pastNode == null) {
return Collections.emptySet();
}
Set<Entry> changedEntries = new LinkedHashSet<>();
if(node.getLabel() == null && pastNode.getLabel() != null) {
node.setLabel(pastNode.getLabel());
labelProperty().set(pastNode.getLabel());
changedEntries.add(this);
}
for(Entry childEntry : getChildren()) {
if(childEntry instanceof HashIndexEntry) {
HashIndexEntry hashIndexEntry = (HashIndexEntry)childEntry;
BlockTransactionHashIndex txo = hashIndexEntry.getHashIndex();
Optional<BlockTransactionHashIndex> optPastTxo = pastNode.getTransactionOutputs().stream().filter(pastTxo -> pastTxo.equals(txo)).findFirst();
if(optPastTxo.isPresent()) {
BlockTransactionHashIndex pastTxo = optPastTxo.get();
if(txo.getLabel() == null && pastTxo.getLabel() != null) {
txo.setLabel(pastTxo.getLabel());
changedEntries.add(childEntry);
}
if(txo.isSpent() && pastTxo.isSpent() && txo.getSpentBy().getLabel() == null && pastTxo.getSpentBy().getLabel() != null) {
txo.getSpentBy().setLabel(pastTxo.getSpentBy().getLabel());
changedEntries.add(childEntry);
}
}
}
if(childEntry instanceof NodeEntry) {
NodeEntry childNodeEntry = (NodeEntry)childEntry;
Optional<WalletNode> optPastChildNodeEntry = pastNode.getChildren().stream().filter(childNodeEntry.node::equals).findFirst();
optPastChildNodeEntry.ifPresent(pastChildNode -> changedEntries.addAll(childNodeEntry.copyLabels(pastChildNode)));
}
}
return changedEntries;
}
}

View file

@ -278,7 +278,10 @@ public class ReceiveController extends WalletFormController implements Initializ
@Subscribe
public void walletNodesChanged(WalletNodesChangedEvent event) {
if(event.getWallet().equals(walletForm.getWallet())) {
if(currentEntry != null) {
label.textProperty().unbindBidirectional(currentEntry.labelProperty());
currentEntry = null;
}
refreshAddress();
}
}

View file

@ -24,7 +24,7 @@ public class SettingsWalletForm extends WalletForm {
private Wallet walletCopy;
public SettingsWalletForm(Storage storage, Wallet currentWallet) {
super(storage, currentWallet, null, false);
super(storage, currentWallet, false);
this.walletCopy = currentWallet.copy();
}
@ -51,15 +51,6 @@ public class SettingsWalletForm extends WalletForm {
boolean addressChange = isAddressChange();
if(wallet.isValid()) {
//Don't create temp backup on changing addresses - there are no labels to lose
if(!addressChange) {
backgroundUpdate(); //Save existing wallet here for the temp backup in case password has been changed - this will update the password on the existing wallet
if(AppServices.isConnected()) {
//Backup the wallet so labels will survive application shutdown
getStorage().backupTempWallet();
}
}
//Clear transaction history cache before we clear the nodes
AppServices.clearTransactionHistoryCache(wallet);
}

View file

@ -8,12 +8,10 @@ import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.WalletTabData;
import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.io.StorageException;
import com.sparrowwallet.sparrow.net.AllHistoryChangedException;
import com.sparrowwallet.sparrow.net.ElectrumServer;
import com.sparrowwallet.sparrow.io.Storage;
import com.sparrowwallet.sparrow.net.ServerType;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
@ -26,7 +24,6 @@ import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;
import static com.sparrowwallet.drongo.wallet.WalletNode.nodeRangesToString;
@ -35,7 +32,6 @@ public class WalletForm {
private final Storage storage;
protected Wallet wallet;
private Wallet savedPastWallet;
private WalletTransactionsEntry walletTransactionsEntry;
private WalletUtxosEntry walletUtxosEntry;
@ -47,19 +43,16 @@ public class WalletForm {
private final BooleanProperty lockedProperty = new SimpleBooleanProperty(false);
public WalletForm(Storage storage, Wallet currentWallet, Wallet backupWallet) {
this(storage, currentWallet, backupWallet, true);
public WalletForm(Storage storage, Wallet currentWallet) {
this(storage, currentWallet, true);
}
public WalletForm(Storage storage, Wallet currentWallet, Wallet backupWallet, boolean refreshHistory) {
public WalletForm(Storage storage, Wallet currentWallet, boolean refreshHistory) {
this.storage = storage;
this.wallet = currentWallet;
//Unencrypted wallets load before isConnected is true, waiting for the ConnectionEvent to refresh history - save the backup for this event
savedPastWallet = backupWallet;
if(refreshHistory && wallet.isValid()) {
refreshHistory(AppServices.getCurrentBlockHeight(), backupWallet);
refreshHistory(AppServices.getCurrentBlockHeight());
}
}
@ -100,11 +93,9 @@ public class WalletForm {
}
public void saveAndRefresh() throws IOException, StorageException {
Wallet pastWallet = wallet.copy();
storage.backupTempWallet();
wallet.clearHistory();
save();
refreshHistory(AppServices.getCurrentBlockHeight(), pastWallet);
refreshHistory(AppServices.getCurrentBlockHeight());
}
public void saveBackup() throws IOException {
@ -124,11 +115,11 @@ public class WalletForm {
storage.deleteBackups();
}
public void refreshHistory(Integer blockHeight, Wallet pastWallet) {
refreshHistory(blockHeight, pastWallet, null);
public void refreshHistory(Integer blockHeight) {
refreshHistory(blockHeight, null);
}
public void refreshHistory(Integer blockHeight, Wallet pastWallet, Set<WalletNode> nodes) {
public void refreshHistory(Integer blockHeight, Set<WalletNode> nodes) {
Wallet previousWallet = wallet.copy();
if(wallet.isValid() && AppServices.isConnected()) {
if(log.isDebugEnabled()) {
@ -139,7 +130,7 @@ public class WalletForm {
historyService.setOnSucceeded(workerStateEvent -> {
if(historyService.getValue()) {
EventManager.get().post(new WalletHistoryFinishedEvent(wallet));
updateWallet(blockHeight, pastWallet, previousWallet);
updateWallet(blockHeight, previousWallet);
}
});
historyService.setOnFailed(workerStateEvent -> {
@ -152,7 +143,7 @@ public class WalletForm {
wallet.clearHistory();
AppServices.clearTransactionHistoryCache(wallet);
EventManager.get().post(new WalletHistoryClearedEvent(wallet, pastWallet == null ? previousWallet : pastWallet, getWalletId()));
EventManager.get().post(new WalletHistoryClearedEvent(wallet, previousWallet, getWalletId()));
} else {
if(AppServices.isConnected()) {
log.error("Error retrieving wallet history", workerStateEvent.getSource().getException());
@ -169,72 +160,20 @@ public class WalletForm {
}
}
private void updateWallet(Integer blockHeight, Wallet pastWallet, Wallet previousWallet) {
private void updateWallet(Integer blockHeight, Wallet previousWallet) {
if(blockHeight != null) {
wallet.setStoredBlockHeight(blockHeight);
}
//After the wallet settings are changed, the previous wallet is copied to pastWallet and used here to copy labels from past nodes, txos and txes
Set<Entry> labelChangedEntries = Collections.emptySet();
if(pastWallet != null) {
labelChangedEntries = copyLabels(pastWallet);
copyMixData(pastWallet);
notifyIfChanged(blockHeight, previousWallet);
}
notifyIfChanged(blockHeight, previousWallet, labelChangedEntries);
}
private Set<Entry> copyLabels(Wallet pastWallet) {
Set<Entry> changedEntries = new LinkedHashSet<>();
//On a full wallet refresh, walletUtxosEntry and walletTransactionsEntry will have no children yet, but AddressesController may have created accountEntries on a walletNodesChangedEvent
//Copy nodeEntry labels
for(KeyPurpose keyPurpose : KeyPurpose.DEFAULT_PURPOSES) {
NodeEntry purposeEntry = getNodeEntry(keyPurpose);
changedEntries.addAll(purposeEntry.copyLabels(pastWallet.getNode(purposeEntry.getNode().getKeyPurpose())));
}
//Copy node and txo labels
for(KeyPurpose keyPurpose : KeyPurpose.DEFAULT_PURPOSES) {
if(wallet.getNode(keyPurpose).copyLabels(pastWallet.getNode(keyPurpose))) {
changedEntries.add(getWalletUtxosEntry());
}
}
//Copy tx labels
for(Map.Entry<Sha256Hash, BlockTransaction> txEntry : wallet.getTransactions().entrySet()) {
BlockTransaction pastBlockTransaction = pastWallet.getTransactions().get(txEntry.getKey());
if(pastBlockTransaction != null && txEntry.getValue() != null && txEntry.getValue().getLabel() == null && pastBlockTransaction.getLabel() != null) {
txEntry.getValue().setLabel(pastBlockTransaction.getLabel());
changedEntries.add(getWalletTransactionsEntry());
}
}
//Force saving the backup if the current wallet has fewer transactions than the past wallet (i.e. incomplete load)
storage.deleteTempBackups(wallet.getTransactions().size() < pastWallet.getTransactions().size());
return changedEntries;
}
private void copyMixData(Wallet pastWallet) {
wallet.getUtxoMixes().forEach(pastWallet.getUtxoMixes()::putIfAbsent);
}
private void notifyIfChanged(Integer blockHeight, Wallet previousWallet, Set<Entry> labelChangedEntries) {
private void notifyIfChanged(Integer blockHeight, Wallet previousWallet) {
List<WalletNode> historyChangedNodes = new ArrayList<>();
historyChangedNodes.addAll(getHistoryChangedNodes(previousWallet.getNode(KeyPurpose.RECEIVE).getChildren(), wallet.getNode(KeyPurpose.RECEIVE).getChildren()));
historyChangedNodes.addAll(getHistoryChangedNodes(previousWallet.getNode(KeyPurpose.CHANGE).getChildren(), wallet.getNode(KeyPurpose.CHANGE).getChildren()));
boolean changed = false;
if(!labelChangedEntries.isEmpty()) {
List<Entry> eventEntries = labelChangedEntries.stream().filter(entry -> entry != getWalletTransactionsEntry() && entry != getWalletUtxosEntry()).collect(Collectors.toList());
if(!eventEntries.isEmpty()) {
Platform.runLater(() -> EventManager.get().post(new WalletEntryLabelsChangedEvent(wallet, eventEntries)));
}
changed = true;
}
if(!historyChangedNodes.isEmpty()) {
Platform.runLater(() -> EventManager.get().post(new WalletHistoryChangedEvent(wallet, storage, historyChangedNodes)));
changed = true;
@ -374,14 +313,9 @@ public class WalletForm {
accountEntries.clear();
EventManager.get().post(new WalletNodesChangedEvent(wallet));
//It is necessary to save the past wallet because the actual copying of the past labels only occurs on a later ConnectionEvent with bwt
if(Config.get().getServerType() == ServerType.BITCOIN_CORE) {
savedPastWallet = event.getPastWallet();
}
//Clear the cache - we will need to fetch everything again
AppServices.clearTransactionHistoryCache(wallet);
refreshHistory(AppServices.getCurrentBlockHeight(), event.getPastWallet());
refreshHistory(AppServices.getCurrentBlockHeight());
}
}
@ -417,14 +351,13 @@ public class WalletForm {
public void newBlock(NewBlockEvent event) {
//Check if wallet is valid to avoid saving wallets in initial setup
if(wallet.isValid()) {
updateWallet(event.getHeight(), null, wallet.copy());
updateWallet(event.getHeight(), wallet.copy());
}
}
@Subscribe
public void connected(ConnectionEvent event) {
refreshHistory(event.getBlockHeight(), savedPastWallet);
savedPastWallet = null;
refreshHistory(event.getBlockHeight());
}
@Subscribe
@ -437,7 +370,7 @@ public class WalletForm {
WalletNode walletNode = event.getWalletNode(wallet);
if(walletNode != null) {
log.debug(wallet.getFullName() + " history event for node " + walletNode + " (" + event.getScriptHash() + ")");
refreshHistory(AppServices.getCurrentBlockHeight(), null, Set.of(walletNode));
refreshHistory(AppServices.getCurrentBlockHeight(), Set.of(walletNode));
}
}
}
@ -576,7 +509,7 @@ public class WalletForm {
}
if(!newNodes.isEmpty()) {
Platform.runLater(() -> refreshHistory(AppServices.getCurrentBlockHeight(), null, newNodes));
Platform.runLater(() -> refreshHistory(AppServices.getCurrentBlockHeight(), newNodes));
}
}
}

View file

@ -23,7 +23,7 @@ public class StorageTest extends IoTest {
@Test
public void loadSeedWallet() throws IOException, MnemonicException, StorageException {
Storage storage = new Storage(getFile("sparrow-single-seed-wallet"));
WalletBackupAndKey walletAndKey = storage.loadEncryptedWallet("pass");
WalletAndKey walletAndKey = storage.loadEncryptedWallet("pass");
Wallet wallet = walletAndKey.getWallet();
Wallet copy = wallet.copy();
copy.decrypt(walletAndKey.getKey());