From 6a58e8a7997bf2b501df18a24578e0cbacfcb183 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Mon, 1 Mar 2021 15:27:54 +0200 Subject: [PATCH] dont lose labels when rescanning, even if app is restarted --- drongo | 2 +- .../sparrowwallet/sparrow/AppController.java | 37 +++--- .../sparrow/control/CoinTreeTable.java | 17 ++- .../com/sparrowwallet/sparrow/io/Storage.java | 123 +++++++++++++++--- .../sparrow/wallet/SettingsController.java | 8 +- .../sparrow/wallet/SettingsWalletForm.java | 7 +- .../sparrow/wallet/WalletForm.java | 35 +++-- 7 files changed, 169 insertions(+), 60 deletions(-) diff --git a/drongo b/drongo index 08acfe5b..faa8f713 160000 --- a/drongo +++ b/drongo @@ -1 +1 @@ -Subproject commit 08acfe5ba16ed54ebbdcb8177cea88e4c53bda77 +Subproject commit faa8f71313ff102c9611f8a9265511029654a83c diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java index 1c4134ca..94339d1f 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppController.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppController.java @@ -569,7 +569,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, false); + addWalletTabOrWindow(storage, wallet, null, false); } } @@ -594,13 +594,13 @@ public class AppController implements Initializable { Storage storage = new Storage(file); FileType fileType = IOUtils.getFileType(file); if(FileType.JSON.equals(fileType)) { - Wallet wallet = storage.loadWallet(); - checkWalletNetwork(wallet); - restorePublicKeysFromSeed(wallet, null); - if(!wallet.isValid()) { + Storage.WalletBackupAndKey walletBackupAndKey = storage.loadWallet(); + checkWalletNetwork(walletBackupAndKey.wallet); + restorePublicKeysFromSeed(walletBackupAndKey.wallet, null); + if(!walletBackupAndKey.wallet.isValid()) { throw new IllegalStateException("Wallet file is not valid."); } - addWalletTabOrWindow(storage, wallet, forceSameWindow); + addWalletTabOrWindow(storage, walletBackupAndKey.wallet, walletBackupAndKey.backupWallet, forceSameWindow); } else if(FileType.BINARY.equals(fileType)) { WalletPasswordDialog dlg = new WalletPasswordDialog(file.getName(), WalletPasswordDialog.PasswordRequirement.LOAD); Optional optionalPassword = dlg.showAndWait(); @@ -612,15 +612,15 @@ public class AppController implements Initializable { Storage.LoadWalletService loadWalletService = new Storage.LoadWalletService(storage, password); loadWalletService.setOnSucceeded(workerStateEvent -> { EventManager.get().post(new StorageEvent(storage.getWalletFile(), TimedEvent.Action.END, "Done")); - Storage.WalletAndKey walletAndKey = loadWalletService.getValue(); + Storage.WalletBackupAndKey walletBackupAndKey = loadWalletService.getValue(); try { - checkWalletNetwork(walletAndKey.wallet); - restorePublicKeysFromSeed(walletAndKey.wallet, walletAndKey.key); - addWalletTabOrWindow(storage, walletAndKey.wallet, forceSameWindow); + checkWalletNetwork(walletBackupAndKey.wallet); + restorePublicKeysFromSeed(walletBackupAndKey.wallet, walletBackupAndKey.key); + addWalletTabOrWindow(storage, walletBackupAndKey.wallet, walletBackupAndKey.backupWallet, forceSameWindow); } catch(Exception e) { showErrorDialog("Error Opening Wallet", e.getMessage()); } finally { - walletAndKey.key.clear(); + walletBackupAndKey.key.clear(); } }); loadWalletService.setOnFailed(workerStateEvent -> { @@ -779,7 +779,7 @@ public class AppController implements Initializable { if(password.isPresent()) { if(password.get().length() == 0) { storage.setEncryptionPubKey(Storage.NO_PASSWORD_KEY); - addWalletTabOrWindow(storage, wallet, false); + addWalletTabOrWindow(storage, wallet, null, false); } else { Storage.KeyDerivationService keyDerivationService = new Storage.KeyDerivationService(storage, password.get()); keyDerivationService.setOnSucceeded(workerStateEvent -> { @@ -792,7 +792,7 @@ public class AppController implements Initializable { key = new Key(encryptionFullKey.getPrivKeyBytes(), storage.getKeyDeriver().getSalt(), EncryptionType.Deriver.ARGON2); wallet.encrypt(key); storage.setEncryptionPubKey(encryptionPubKey); - addWalletTabOrWindow(storage, wallet, false); + addWalletTabOrWindow(storage, wallet, null, false); } finally { encryptionFullKey.clear(); if(key != null) { @@ -856,12 +856,13 @@ public class AppController implements Initializable { WalletTabData walletTabData = (WalletTabData) tabData; Wallet wallet = walletTabData.getWallet(); Wallet pastWallet = wallet.copy(); + walletTabData.getStorage().backupTempWallet(); wallet.clearHistory(); EventManager.get().post(new WalletSettingsChangedEvent(wallet, pastWallet, walletTabData.getStorage().getWalletFile())); } } - public void addWalletTabOrWindow(Storage storage, Wallet wallet, boolean forceSameWindow) { + public void addWalletTabOrWindow(Storage storage, Wallet wallet, Wallet backupWallet, boolean forceSameWindow) { Window existingWalletWindow = AppServices.get().getWindowForWallet(storage); if(existingWalletWindow instanceof Stage) { Stage existingWalletStage = (Stage)existingWalletWindow; @@ -876,13 +877,13 @@ public class AppController implements Initializable { AppController appController = AppServices.newAppWindow(stage); stage.toFront(); stage.setX(AppServices.get().getWalletWindowMaxX() + 30); - appController.addWalletTab(storage, wallet); + appController.addWalletTab(storage, wallet, backupWallet); } else { - addWalletTab(storage, wallet); + addWalletTab(storage, wallet, backupWallet); } } - public void addWalletTab(Storage storage, Wallet wallet) { + public void addWalletTab(Storage storage, Wallet wallet, Wallet backupWallet) { try { String name = storage.getWalletFile().getName(); if(name.endsWith(".json")) { @@ -901,7 +902,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); + WalletForm walletForm = new WalletForm(storage, wallet, backupWallet); EventManager.get().register(walletForm); controller.setWalletForm(walletForm); diff --git a/src/main/java/com/sparrowwallet/sparrow/control/CoinTreeTable.java b/src/main/java/com/sparrowwallet/sparrow/control/CoinTreeTable.java index 200c94c1..d5aa5911 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/CoinTreeTable.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/CoinTreeTable.java @@ -84,16 +84,15 @@ public class CoinTreeTable extends TreeTableView { WalletBirthDateDialog dlg = new WalletBirthDateDialog(wallet.getBirthDate()); Optional optDate = dlg.showAndWait(); if(optDate.isPresent()) { - Wallet pastWallet = wallet.copy(); - wallet.setBirthDate(optDate.get()); Storage storage = AppServices.get().getOpenWallets().get(wallet); - if(storage != null) { - //Trigger background save of birthdate - EventManager.get().post(new WalletDataChangedEvent(wallet)); - //Trigger full wallet rescan - wallet.clearHistory(); - EventManager.get().post(new WalletSettingsChangedEvent(wallet, pastWallet, storage.getWalletFile())); - } + Wallet pastWallet = wallet.copy(); + storage.backupTempWallet(); + wallet.setBirthDate(optDate.get()); + //Trigger background save of birthdate + EventManager.get().post(new WalletDataChangedEvent(wallet)); + //Trigger full wallet rescan + wallet.clearHistory(); + EventManager.get().post(new WalletSettingsChangedEvent(wallet, pastWallet, storage.getWalletFile())); } }); if(wallet.getBirthDate() == null) { diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Storage.java b/src/main/java/com/sparrowwallet/sparrow/io/Storage.java index 6e7e084f..9b9668a3 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/Storage.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/Storage.java @@ -10,13 +10,14 @@ import com.sparrowwallet.drongo.crypto.*; import com.sparrowwallet.drongo.protocol.Sha256Hash; import com.sparrowwallet.drongo.protocol.Transaction; import com.sparrowwallet.drongo.wallet.Keystore; -import com.sparrowwallet.drongo.wallet.MnemonicException; import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.drongo.wallet.WalletNode; import com.sparrowwallet.sparrow.MainApp; import javafx.concurrent.Service; import javafx.concurrent.Task; import org.controlsfx.tools.Platform; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.*; import java.lang.reflect.Type; @@ -26,14 +27,18 @@ import java.security.SecureRandom; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import java.util.zip.*; import static com.sparrowwallet.drongo.crypto.Argon2KeyDeriver.SPRW1_PARAMETERS; public class Storage { + private static final Logger log = LoggerFactory.getLogger(Storage.class); public static final ECKey NO_PASSWORD_KEY = ECKey.fromPublicOnly(ECKey.fromPrivate(Utils.hexToBytes("885e5a09708a167ea356a252387aa7c4893d138d632e296df8fbf5c12798bd28"))); private static final DateFormat BACKUP_DATE_FORMAT = new SimpleDateFormat("yyyyMMddHHmmss"); + private static final Pattern DATE_PATTERN = Pattern.compile(".+-([0-9]{14}?).*"); public static final String SPARROW_DIR = ".sparrow"; public static final String WINDOWS_SPARROW_DIR = "Sparrow"; @@ -41,6 +46,7 @@ public class Storage { public static final String WALLETS_BACKUP_DIR = "backup"; public static final String HEADER_MAGIC_1 = "SPRW1"; private static final int BINARY_HEADER_LENGTH = 28; + public static final String TEMP_BACKUP_EXTENSION = "tmp"; private File walletFile; private final Gson gson; @@ -81,17 +87,49 @@ public class Storage { return gsonBuilder.setPrettyPrinting().disableHtmlEscaping().create(); } - public Wallet loadWallet() throws IOException { - Reader reader = new FileReader(walletFile); + public WalletBackupAndKey loadWallet() throws IOException { + Wallet wallet = loadWallet(walletFile); + + Wallet backupWallet = null; + File[] backups = getBackups("json." + TEMP_BACKUP_EXTENSION); + if(backups.length > 0) { + try { + backupWallet = loadWallet(backups[0]); + } catch(Exception e) { + log.error("Error loading backup wallet " + TEMP_BACKUP_EXTENSION, e); + } + } + + encryptionPubKey = NO_PASSWORD_KEY; + return new WalletBackupAndKey(wallet, backupWallet, null); + } + + public Wallet loadWallet(File jsonFile) throws IOException { + Reader reader = new FileReader(jsonFile); Wallet wallet = gson.fromJson(reader, Wallet.class); reader.close(); - encryptionPubKey = NO_PASSWORD_KEY; return wallet; } - public WalletAndKey loadWallet(CharSequence password) throws IOException, StorageException { - InputStream fileStream = new FileInputStream(walletFile); + public WalletBackupAndKey loadWallet(CharSequence password) throws IOException, StorageException { + WalletAndKey walletAndKey = loadWallet(walletFile, password); + + WalletAndKey backupAndKey = new WalletAndKey(null, null); + File[] backups = getBackups(TEMP_BACKUP_EXTENSION, "json." + TEMP_BACKUP_EXTENSION); + if(backups.length > 0) { + try { + backupAndKey = loadWallet(backups[0], password); + } catch(Exception e) { + log.error("Error loading backup wallet " + TEMP_BACKUP_EXTENSION, e); + } + } + + return new WalletBackupAndKey(walletAndKey.wallet, backupAndKey.wallet, walletAndKey.key); + } + + public WalletAndKey loadWallet(File encryptedFile, CharSequence password) throws IOException, StorageException { + InputStream fileStream = new FileInputStream(encryptedFile); ECKey encryptionKey = getEncryptionKey(password, fileStream); InputStream inputStream = new InflaterInputStream(new ECIESInputStream(fileStream, encryptionKey, getEncryptionMagic())); @@ -168,6 +206,18 @@ public class Storage { } public void backupWallet() throws IOException { + backupWallet(null); + } + + public void backupTempWallet() { + try { + backupWallet(TEMP_BACKUP_EXTENSION); + } catch(IOException e) { + log.error("Error creating ." + TEMP_BACKUP_EXTENSION + " backup wallet", e); + } + } + + public void backupWallet(String extension) throws IOException { File backupDir = getWalletsBackupDir(); Date backupDate = new Date(); @@ -180,20 +230,50 @@ public class Storage { backupName += dateSuffix; } + if(extension != null) { + backupName += "." + extension; + } + File backupFile = new File(backupDir, backupName); Files.copy(walletFile, backupFile); } public void deleteBackups() { + deleteBackups(null); + } + + public void deleteBackups(String extension) { + File[] backups = getBackups(extension); + for(File backup : backups) { + backup.delete(); + } + } + + private File[] getBackups(String extension) { + return getBackups(extension, null); + } + + private File[] getBackups(String extension, String notExtension) { File backupDir = getWalletsBackupDir(); - File[] unencryptedBackups = backupDir.listFiles((dir, name) -> { - int dotIndex = name.lastIndexOf('.'); - return name.startsWith(walletFile.getName() + "-") && name.substring(walletFile.getName().length() + 1, dotIndex > -1 ? dotIndex : name.length()).matches("[0-9]+"); + File[] backups = backupDir.listFiles((dir, name) -> { + return name.startsWith(Files.getNameWithoutExtension(walletFile.getName()) + "-") && + getBackupDate(name) != null && + (extension == null || name.endsWith("." + extension)) && + (notExtension == null || !name.endsWith("." + notExtension)); }); - for(File unencryptedBackup : unencryptedBackups) { - unencryptedBackup.delete(); + Arrays.sort(backups, Comparator.comparing(o -> getBackupDate(((File)o).getName())).reversed()); + + return backups; + } + + private String getBackupDate(String backupFileName) { + Matcher matcher = DATE_PATTERN.matcher(backupFileName); + if(matcher.matches()) { + return matcher.group(1); } + + return null; } public ECKey getEncryptionPubKey() { @@ -250,6 +330,8 @@ public class Storage { } keyDeriver = new Argon2KeyDeriver(salt); + } else if(inputStream != null) { + inputStream.skip(BINARY_HEADER_LENGTH); } return keyDeriver; @@ -477,7 +559,16 @@ public class Storage { } } - public static class LoadWalletService extends Service { + public static class WalletBackupAndKey extends WalletAndKey { + public final Wallet backupWallet; + + public WalletBackupAndKey(Wallet wallet, Wallet backupWallet, Key key) { + super(wallet, key); + this.backupWallet = backupWallet; + } + } + + public static class LoadWalletService extends Service { private final Storage storage; private final SecureString password; @@ -487,12 +578,12 @@ public class Storage { } @Override - protected Task createTask() { + protected Task createTask() { return new Task<>() { - protected WalletAndKey call() throws IOException, StorageException, MnemonicException { - WalletAndKey walletAndKey = storage.loadWallet(password); + protected WalletBackupAndKey call() throws IOException, StorageException { + WalletBackupAndKey walletBackupAndKey = storage.loadWallet(password); password.clear(); - return walletAndKey; + return walletBackupAndKey; } }; } diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsController.java index c5b4db07..99031130 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsController.java @@ -443,14 +443,14 @@ public class SettingsController extends WalletFormController implements Initiali return; } - walletForm.getWallet().encrypt(key); - walletForm.getStorage().setEncryptionPubKey(encryptionPubKey); - walletForm.saveAndRefresh(); - if(dlg.isDeleteBackups()) { walletForm.deleteBackups(); } + walletForm.getWallet().encrypt(key); + walletForm.getStorage().setEncryptionPubKey(encryptionPubKey); + walletForm.saveAndRefresh(); + if(requirement == WalletPasswordDialog.PasswordRequirement.UPDATE_NEW || requirement == WalletPasswordDialog.PasswordRequirement.UPDATE_EMPTY) { EventManager.get().post(new RequestOpenWalletsEvent()); } diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsWalletForm.java b/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsWalletForm.java index 77f044c3..0f7b9e0a 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsWalletForm.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/SettingsWalletForm.java @@ -17,7 +17,7 @@ public class SettingsWalletForm extends WalletForm { private Wallet walletCopy; public SettingsWalletForm(Storage storage, Wallet currentWallet) { - super(storage, currentWallet); + super(storage, currentWallet, null, false); this.walletCopy = currentWallet.copy(); } @@ -38,10 +38,13 @@ public class SettingsWalletForm extends WalletForm { @Override public void saveAndRefresh() throws IOException { - Wallet pastWallet = wallet.copy(); + Wallet pastWallet = null; boolean refreshAll = isRefreshNecessary(wallet, walletCopy); if(refreshAll) { + pastWallet = wallet.copy(); + save(); //Save here for the temp backup in case password has been changed + getStorage().backupTempWallet(); walletCopy.clearNodes(); } diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletForm.java b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletForm.java index 04b83146..2575b887 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletForm.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletForm.java @@ -34,10 +34,20 @@ public class WalletForm { private final List accountEntries = new ArrayList<>(); private final List> walletTransactionNodes = new ArrayList<>(); - public WalletForm(Storage storage, Wallet currentWallet) { + public WalletForm(Storage storage, Wallet currentWallet, Wallet backupWallet) { + this(storage, currentWallet, backupWallet, true); + } + + public WalletForm(Storage storage, Wallet currentWallet, Wallet backupWallet, boolean refreshHistory) { this.storage = storage; this.wallet = currentWallet; - refreshHistory(AppServices.getCurrentBlockHeight(), null); + + //Unencrypted wallets load before isConnected is true, waiting for the ConnectionEvent to refresh history - save the backup for this event + savedPastWallet = backupWallet; + + if(refreshHistory) { + refreshHistory(AppServices.getCurrentBlockHeight(), backupWallet); + } } public Wallet getWallet() { @@ -66,6 +76,7 @@ public class WalletForm { public void saveAndRefresh() throws IOException { Wallet pastWallet = wallet.copy(); + storage.backupTempWallet(); wallet.clearHistory(); save(); refreshHistory(AppServices.getCurrentBlockHeight(), pastWallet); @@ -106,31 +117,36 @@ public class WalletForm { wallet.setStoredBlockHeight(blockHeight); } + boolean labelsChanged = false; if(pastWallet != null) { - copyLabels(pastWallet); + labelsChanged = copyLabels(pastWallet); } - notifyIfChanged(blockHeight, previousWallet); + notifyIfChanged(blockHeight, previousWallet, labelsChanged); } - private void copyLabels(Wallet pastWallet) { - wallet.getNode(KeyPurpose.RECEIVE).copyLabels(pastWallet.getNode(KeyPurpose.RECEIVE)); - wallet.getNode(KeyPurpose.CHANGE).copyLabels(pastWallet.getNode(KeyPurpose.CHANGE)); + private boolean copyLabels(Wallet pastWallet) { + boolean changed = wallet.getNode(KeyPurpose.RECEIVE).copyLabels(pastWallet.getNode(KeyPurpose.RECEIVE)); + changed |= wallet.getNode(KeyPurpose.CHANGE).copyLabels(pastWallet.getNode(KeyPurpose.CHANGE)); for(Map.Entry 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()); + changed = true; } } + + storage.deleteBackups(Storage.TEMP_BACKUP_EXTENSION); + return changed; } - private void notifyIfChanged(Integer blockHeight, Wallet previousWallet) { + private void notifyIfChanged(Integer blockHeight, Wallet previousWallet, boolean labelsChanged) { List 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; + boolean changed = labelsChanged; if(!historyChangedNodes.isEmpty()) { Platform.runLater(() -> EventManager.get().post(new WalletHistoryChangedEvent(wallet, storage, historyChangedNodes))); changed = true; @@ -254,7 +270,6 @@ public class WalletForm { 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 - //The savedPastWallet variable can be removed once bwt supports dynamic loading of wallets without needing to disconnect/reconnect if(Config.get().getServerType() == ServerType.BITCOIN_CORE) { savedPastWallet = event.getPastWallet(); }