mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2026-08-10 08:37:05 +00:00
introduce database persistence with automatic migration of existing wallets
This commit is contained in:
@@ -762,11 +762,10 @@ public class AppController implements Initializable {
|
||||
public void openWalletFile(File file, boolean forceSameWindow) {
|
||||
try {
|
||||
Storage storage = new Storage(file);
|
||||
FileType fileType = IOUtils.getFileType(file);
|
||||
if(FileType.JSON.equals(fileType)) {
|
||||
if(!storage.isEncrypted()) {
|
||||
WalletBackupAndKey walletBackupAndKey = storage.loadUnencryptedWallet();
|
||||
openWallet(storage, walletBackupAndKey, this, forceSameWindow);
|
||||
} else if(FileType.BINARY.equals(fileType)) {
|
||||
} else {
|
||||
WalletPasswordDialog dlg = new WalletPasswordDialog(file.getName(), WalletPasswordDialog.PasswordRequirement.LOAD);
|
||||
Optional<SecureString> optionalPassword = dlg.showAndWait();
|
||||
if(optionalPassword.isEmpty()) {
|
||||
@@ -776,12 +775,12 @@ public class AppController implements Initializable {
|
||||
SecureString password = optionalPassword.get();
|
||||
Storage.LoadWalletService loadWalletService = new Storage.LoadWalletService(storage, password);
|
||||
loadWalletService.setOnSucceeded(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(storage.getWalletFile(), TimedEvent.Action.END, "Done"));
|
||||
EventManager.get().post(new StorageEvent(storage.getWalletId(null), TimedEvent.Action.END, "Done"));
|
||||
WalletBackupAndKey walletBackupAndKey = loadWalletService.getValue();
|
||||
openWallet(storage, walletBackupAndKey, this, forceSameWindow);
|
||||
});
|
||||
loadWalletService.setOnFailed(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(storage.getWalletFile(), TimedEvent.Action.END, "Failed"));
|
||||
EventManager.get().post(new StorageEvent(storage.getWalletId(null), TimedEvent.Action.END, "Failed"));
|
||||
Throwable exception = loadWalletService.getException();
|
||||
if(exception instanceof InvalidPasswordException) {
|
||||
Optional<ButtonType> optResponse = showErrorDialog("Invalid Password", "The wallet password was invalid. Try again?", ButtonType.CANCEL, ButtonType.OK);
|
||||
@@ -796,11 +795,11 @@ public class AppController implements Initializable {
|
||||
password.clear();
|
||||
}
|
||||
});
|
||||
EventManager.get().post(new StorageEvent(storage.getWalletFile(), TimedEvent.Action.START, "Decrypting wallet..."));
|
||||
EventManager.get().post(new StorageEvent(storage.getWalletId(null), TimedEvent.Action.START, "Decrypting wallet..."));
|
||||
loadWalletService.start();
|
||||
} else {
|
||||
throw new IOException("Unsupported file type");
|
||||
}
|
||||
} catch(StorageException e) {
|
||||
showErrorDialog("Error Opening Wallet", e.getMessage());
|
||||
} catch(Exception e) {
|
||||
if(!attemptImportWallet(file, null)) {
|
||||
log.error("Error opening wallet", e);
|
||||
@@ -822,6 +821,7 @@ public class AppController implements Initializable {
|
||||
}
|
||||
Platform.runLater(() -> selectTab(walletBackupAndKey.getWallet()));
|
||||
} catch(Exception e) {
|
||||
log.error("Error opening wallet", e);
|
||||
showErrorDialog("Error Opening Wallet", e.getMessage());
|
||||
} finally {
|
||||
walletBackupAndKey.clear();
|
||||
@@ -969,13 +969,13 @@ public class AppController implements Initializable {
|
||||
storage.setEncryptionPubKey(Storage.NO_PASSWORD_KEY);
|
||||
storage.saveWallet(wallet);
|
||||
addWalletTabOrWindow(storage, wallet, null, false);
|
||||
} catch(IOException e) {
|
||||
} catch(IOException | StorageException e) {
|
||||
log.error("Error saving imported wallet", e);
|
||||
}
|
||||
} else {
|
||||
Storage.KeyDerivationService keyDerivationService = new Storage.KeyDerivationService(storage, password.get());
|
||||
keyDerivationService.setOnSucceeded(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(Storage.getWalletFile(wallet.getName()), TimedEvent.Action.END, "Done"));
|
||||
EventManager.get().post(new StorageEvent(Storage.getWalletFile(wallet.getName()).getAbsolutePath(), TimedEvent.Action.END, "Done"));
|
||||
ECKey encryptionFullKey = keyDerivationService.getValue();
|
||||
Key key = null;
|
||||
|
||||
@@ -986,7 +986,7 @@ public class AppController implements Initializable {
|
||||
storage.setEncryptionPubKey(encryptionPubKey);
|
||||
storage.saveWallet(wallet);
|
||||
addWalletTabOrWindow(storage, wallet, null, false);
|
||||
} catch(IOException e) {
|
||||
} catch(IOException | StorageException e) {
|
||||
log.error("Error saving imported wallet", e);
|
||||
} finally {
|
||||
encryptionFullKey.clear();
|
||||
@@ -996,10 +996,10 @@ public class AppController implements Initializable {
|
||||
}
|
||||
});
|
||||
keyDerivationService.setOnFailed(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(Storage.getWalletFile(wallet.getName()), TimedEvent.Action.END, "Failed"));
|
||||
EventManager.get().post(new StorageEvent(Storage.getWalletFile(wallet.getName()).getAbsolutePath(), TimedEvent.Action.END, "Failed"));
|
||||
showErrorDialog("Error encrypting wallet", keyDerivationService.getException().getMessage());
|
||||
});
|
||||
EventManager.get().post(new StorageEvent(Storage.getWalletFile(wallet.getName()), TimedEvent.Action.START, "Encrypting wallet..."));
|
||||
EventManager.get().post(new StorageEvent(Storage.getWalletFile(wallet.getName()).getAbsolutePath(), TimedEvent.Action.START, "Encrypting wallet..."));
|
||||
keyDerivationService.start();
|
||||
}
|
||||
}
|
||||
@@ -1080,12 +1080,12 @@ public class AppController implements Initializable {
|
||||
walletTabData.getStorage().backupTempWallet();
|
||||
wallet.clearHistory();
|
||||
AppServices.clearTransactionHistoryCache(wallet);
|
||||
EventManager.get().post(new WalletAddressesChangedEvent(wallet, pastWallet, walletTabData.getStorage().getWalletFile()));
|
||||
EventManager.get().post(new WalletHistoryClearedEvent(wallet, pastWallet, walletTabData.getWalletForm().getWalletId()));
|
||||
}
|
||||
}
|
||||
|
||||
public AppController addWalletTabOrWindow(Storage storage, Wallet wallet, Wallet backupWallet, boolean forceSameWindow) {
|
||||
Window existingWalletWindow = AppServices.get().getWindowForWallet(storage);
|
||||
Window existingWalletWindow = AppServices.get().getWindowForWallet(storage.getWalletId(wallet));
|
||||
if(existingWalletWindow instanceof Stage) {
|
||||
Stage existingWalletStage = (Stage)existingWalletWindow;
|
||||
existingWalletStage.toFront();
|
||||
@@ -1109,10 +1109,7 @@ public class AppController implements Initializable {
|
||||
|
||||
public void addWalletTab(Storage storage, Wallet wallet, Wallet backupWallet) {
|
||||
try {
|
||||
String name = storage.getWalletFile().getName();
|
||||
if(name.endsWith(".json")) {
|
||||
name = name.substring(0, name.lastIndexOf('.'));
|
||||
}
|
||||
String name = storage.getWalletName(wallet);
|
||||
if(!name.equals(wallet.getName())) {
|
||||
wallet.setName(name);
|
||||
}
|
||||
@@ -1504,7 +1501,7 @@ public class AppController implements Initializable {
|
||||
TabData tabData = (TabData)tab.getUserData();
|
||||
if(tabData instanceof WalletTabData) {
|
||||
WalletTabData walletTabData = (WalletTabData)tabData;
|
||||
if(walletTabData.getWalletForm().getWalletFile().equals(event.getWalletFile())) {
|
||||
if(walletTabData.getWalletForm().getWalletId().equals(event.getWalletId())) {
|
||||
exportWallet.setDisable(!event.getWallet().isValid());
|
||||
}
|
||||
}
|
||||
@@ -1819,6 +1816,17 @@ public class AppController implements Initializable {
|
||||
@Subscribe
|
||||
public void viewWallet(ViewWalletEvent event) {
|
||||
if(tabs.getScene().getWindow().equals(event.getWindow())) {
|
||||
for(Tab tab : tabs.getTabs()) {
|
||||
TabData tabData = (TabData) tab.getUserData();
|
||||
if(tabData.getType() == TabData.TabType.WALLET) {
|
||||
WalletTabData walletTabData = (WalletTabData) tabData;
|
||||
if(event.getStorage().getWalletId(event.getWallet()).equals(walletTabData.getWalletForm().getWalletId())) {
|
||||
tabs.getSelectionModel().select(tab);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for(Tab tab : tabs.getTabs()) {
|
||||
TabData tabData = (TabData) tab.getUserData();
|
||||
if(tabData.getType() == TabData.TabType.WALLET) {
|
||||
|
||||
@@ -469,8 +469,8 @@ public class AppServices {
|
||||
return openWallets;
|
||||
}
|
||||
|
||||
public Window getWindowForWallet(Storage storage) {
|
||||
Optional<Window> optWindow = walletWindows.entrySet().stream().filter(entry -> entry.getValue().stream().anyMatch(walletTabData -> walletTabData.getStorage().getWalletFile().equals(storage.getWalletFile()))).map(Map.Entry::getKey).findFirst();
|
||||
public Window getWindowForWallet(String walletId) {
|
||||
Optional<Window> optWindow = walletWindows.entrySet().stream().filter(entry -> entry.getValue().stream().anyMatch(walletTabData -> walletTabData.getWalletForm().getWalletId().equals(walletId))).map(Map.Entry::getKey).findFirst();
|
||||
return optWindow.orElse(null);
|
||||
}
|
||||
|
||||
@@ -544,8 +544,7 @@ public class AppServices {
|
||||
}
|
||||
|
||||
public static boolean isWalletFile(File file) {
|
||||
FileType fileType = IOUtils.getFileType(file);
|
||||
return FileType.JSON.equals(fileType) || FileType.BINARY.equals(fileType);
|
||||
return Storage.isWalletFile(file);
|
||||
}
|
||||
|
||||
public static Optional<ButtonType> showWarningDialog(String title, String content, ButtonType... buttons) {
|
||||
@@ -616,10 +615,6 @@ public class AppServices {
|
||||
}
|
||||
}
|
||||
|
||||
public void moveToWalletWindowScreen(Storage storage, Dialog<?> dialog) {
|
||||
moveToWindowScreen(getWindowForWallet(storage), dialog);
|
||||
}
|
||||
|
||||
public static void moveToWindowScreen(Window currentWindow, Dialog<?> dialog) {
|
||||
Window newWindow = dialog.getDialogPane().getScene().getWindow();
|
||||
DialogPane dialogPane = dialog.getDialogPane();
|
||||
|
||||
@@ -7,8 +7,6 @@ import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5Brands;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.io.FileType;
|
||||
import com.sparrowwallet.sparrow.io.IOUtils;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
import com.sparrowwallet.sparrow.net.Bwt;
|
||||
import com.sparrowwallet.sparrow.net.PublicElectrumServer;
|
||||
@@ -99,9 +97,8 @@ public class MainApp extends Application {
|
||||
|
||||
List<File> recentWalletFiles = Config.get().getRecentWalletFiles();
|
||||
if(recentWalletFiles != null) {
|
||||
//Re-sort to preserve wallet order as far as possible. Unencrypted wallets will still be opened first.
|
||||
List<File> encryptedWalletFiles = recentWalletFiles.stream().filter(file -> FileType.BINARY.equals(IOUtils.getFileType(file))).collect(Collectors.toList());
|
||||
Collections.reverse(encryptedWalletFiles);
|
||||
//Preserve wallet order as far as possible. Unencrypted wallets will still be opened first.
|
||||
List<File> encryptedWalletFiles = recentWalletFiles.stream().filter(Storage::isEncrypted).collect(Collectors.toList());
|
||||
List<File> sortedWalletFiles = new ArrayList<>(recentWalletFiles);
|
||||
sortedWalletFiles.removeAll(encryptedWalletFiles);
|
||||
sortedWalletFiles.addAll(encryptedWalletFiles);
|
||||
|
||||
@@ -5,8 +5,8 @@ import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.WalletDataChangedEvent;
|
||||
import com.sparrowwallet.sparrow.event.WalletHistoryClearedEvent;
|
||||
import com.sparrowwallet.sparrow.event.WalletHistoryStatusEvent;
|
||||
import com.sparrowwallet.sparrow.event.WalletAddressesChangedEvent;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
import com.sparrowwallet.sparrow.net.ServerType;
|
||||
@@ -92,7 +92,7 @@ public class CoinTreeTable extends TreeTableView<Entry> {
|
||||
EventManager.get().post(new WalletDataChangedEvent(wallet));
|
||||
//Trigger full wallet rescan
|
||||
wallet.clearHistory();
|
||||
EventManager.get().post(new WalletAddressesChangedEvent(wallet, pastWallet, storage.getWalletFile()));
|
||||
EventManager.get().post(new WalletHistoryClearedEvent(wallet, pastWallet, storage.getWalletId(wallet)));
|
||||
}
|
||||
});
|
||||
if(wallet.getBirthDate() == null) {
|
||||
|
||||
@@ -100,10 +100,10 @@ public class FileWalletExportPane extends TitledDescriptionPane {
|
||||
WalletPasswordDialog dlg = new WalletPasswordDialog(wallet.getName(), WalletPasswordDialog.PasswordRequirement.LOAD);
|
||||
Optional<SecureString> password = dlg.showAndWait();
|
||||
if(password.isPresent()) {
|
||||
final File walletFile = AppServices.get().getOpenWallets().get(wallet).getWalletFile();
|
||||
final String walletId = AppServices.get().getOpenWallets().get(wallet).getWalletId(wallet);
|
||||
Storage.DecryptWalletService decryptWalletService = new Storage.DecryptWalletService(copy, password.get());
|
||||
decryptWalletService.setOnSucceeded(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(walletFile, TimedEvent.Action.END, "Done"));
|
||||
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Done"));
|
||||
Wallet decryptedWallet = decryptWalletService.getValue();
|
||||
|
||||
try {
|
||||
@@ -113,10 +113,10 @@ public class FileWalletExportPane extends TitledDescriptionPane {
|
||||
}
|
||||
});
|
||||
decryptWalletService.setOnFailed(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(walletFile, TimedEvent.Action.END, "Failed"));
|
||||
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Failed"));
|
||||
setError("Export Error", decryptWalletService.getException().getMessage());
|
||||
});
|
||||
EventManager.get().post(new StorageEvent(walletFile, TimedEvent.Action.START, "Decrypting wallet..."));
|
||||
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.START, "Decrypting wallet..."));
|
||||
decryptWalletService.start();
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -368,16 +368,16 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
if(password.isPresent()) {
|
||||
Storage.DecryptWalletService decryptWalletService = new Storage.DecryptWalletService(wallet.copy(), password.get());
|
||||
decryptWalletService.setOnSucceeded(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(storage.getWalletFile(), TimedEvent.Action.END, "Done"));
|
||||
EventManager.get().post(new StorageEvent(storage.getWalletId(wallet), TimedEvent.Action.END, "Done"));
|
||||
Wallet decryptedWallet = decryptWalletService.getValue();
|
||||
signUnencryptedKeystore(decryptedWallet);
|
||||
decryptedWallet.clearPrivate();
|
||||
});
|
||||
decryptWalletService.setOnFailed(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(storage.getWalletFile(), TimedEvent.Action.END, "Failed"));
|
||||
EventManager.get().post(new StorageEvent(storage.getWalletId(wallet), TimedEvent.Action.END, "Failed"));
|
||||
AppServices.showErrorDialog("Incorrect Password", decryptWalletService.getException().getMessage());
|
||||
});
|
||||
EventManager.get().post(new StorageEvent(storage.getWalletFile(), TimedEvent.Action.START, "Decrypting wallet..."));
|
||||
EventManager.get().post(new StorageEvent(storage.getWalletId(wallet), TimedEvent.Action.START, "Decrypting wallet..."));
|
||||
decryptWalletService.start();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* This event is trigger when one or more keystores on a wallet are updated, and the wallet is saved
|
||||
*/
|
||||
public class KeystoreLabelsChangedEvent extends WalletSettingsChangedEvent {
|
||||
private final List<Keystore> changedKeystores;
|
||||
|
||||
public KeystoreLabelsChangedEvent(Wallet wallet, Wallet pastWallet, String walletId, List<Keystore> changedKeystores) {
|
||||
super(wallet, pastWallet, walletId);
|
||||
this.changedKeystores = changedKeystores;
|
||||
}
|
||||
|
||||
public List<Keystore> getChangedKeystores() {
|
||||
return changedKeystores;
|
||||
}
|
||||
}
|
||||
@@ -8,9 +8,9 @@ import java.util.Map;
|
||||
|
||||
public class StorageEvent extends TimedEvent {
|
||||
private static boolean firstRunDone = false;
|
||||
private static final Map<File, Long> eventTime = new HashMap<>();
|
||||
private static final Map<String, Long> eventTime = new HashMap<>();
|
||||
|
||||
public StorageEvent(File file, Action action, String status) {
|
||||
public StorageEvent(String walletId, Action action, String status) {
|
||||
super(action, status);
|
||||
|
||||
Integer keyDerivationPeriod = Config.get().getKeyDerivationPeriod();
|
||||
@@ -19,10 +19,10 @@ public class StorageEvent extends TimedEvent {
|
||||
}
|
||||
|
||||
if(action == Action.START) {
|
||||
eventTime.put(file, System.currentTimeMillis());
|
||||
eventTime.put(walletId, System.currentTimeMillis());
|
||||
timeMills = keyDerivationPeriod;
|
||||
} else if(action == Action.END) {
|
||||
long start = eventTime.get(file);
|
||||
long start = eventTime.get(walletId);
|
||||
if(firstRunDone) {
|
||||
keyDerivationPeriod = (int)(System.currentTimeMillis() - start);
|
||||
Config.get().setKeyDerivationPeriod(keyDerivationPeriod);
|
||||
|
||||
@@ -11,8 +11,8 @@ import java.io.File;
|
||||
* This is because any failure in saving the wallet must be immediately reported to the user.
|
||||
* Note that all wallet detail controllers that share a WalletForm, and that class posts WalletNodesChangedEvent once it has cleared it's entry caches.
|
||||
*/
|
||||
public class WalletAddressesChangedEvent extends WalletSettingsChangedEvent {
|
||||
public WalletAddressesChangedEvent(Wallet wallet, Wallet pastWallet, File walletFile) {
|
||||
super(wallet, pastWallet, walletFile);
|
||||
public class WalletAddressesChangedEvent extends WalletHistoryClearedEvent {
|
||||
public WalletAddressesChangedEvent(Wallet wallet, Wallet pastWallet, String walletId) {
|
||||
super(wallet, pastWallet, walletId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,8 @@ import java.util.List;
|
||||
|
||||
/**
|
||||
* This event is fired when a wallet entry (transaction, txi or txo) label is changed.
|
||||
* Extends WalletDataChangedEvent so triggers a background save.
|
||||
*/
|
||||
public class WalletEntryLabelsChangedEvent extends WalletDataChangedEvent {
|
||||
public class WalletEntryLabelsChangedEvent extends WalletChangedEvent {
|
||||
private final List<Entry> entries;
|
||||
|
||||
public WalletEntryLabelsChangedEvent(Wallet wallet, Entry entry) {
|
||||
|
||||
@@ -23,8 +23,8 @@ public class WalletHistoryChangedEvent extends WalletChangedEvent {
|
||||
this.historyChangedNodes = historyChangedNodes;
|
||||
}
|
||||
|
||||
public File getWalletFile() {
|
||||
return storage.getWalletFile();
|
||||
public String getWalletId() {
|
||||
return storage.getWalletId(getWallet());
|
||||
}
|
||||
|
||||
public List<WalletNode> getHistoryChangedNodes() {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
public class WalletHistoryClearedEvent extends WalletSettingsChangedEvent {
|
||||
public WalletHistoryClearedEvent(Wallet wallet, Wallet pastWallet, String walletId) {
|
||||
super(wallet, pastWallet, walletId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
public class WalletPasswordChangedEvent extends WalletSettingsChangedEvent {
|
||||
public WalletPasswordChangedEvent(Wallet wallet, Wallet pastWallet, String walletId) {
|
||||
super(wallet, pastWallet, walletId);
|
||||
}
|
||||
}
|
||||
@@ -5,25 +5,24 @@ import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* This event is posted when a wallet's settings are changed in a way that does not update the wallet addresses or history.
|
||||
* For example, changing the keystore source or label will trigger this event, which updates the wallet but avoids a full wallet refresh.
|
||||
* It is impossible for the validity of a wallet to change on this event - listen for WalletAddressesChangedEvent for this
|
||||
* This is the base class for events posted when a wallet's settings are changed
|
||||
* Do not listen for this event directly - listen for a subclass, for example KeystoreLabelsChangedEvent or WalletAddressesChangedEvent
|
||||
*/
|
||||
public class WalletSettingsChangedEvent extends WalletChangedEvent {
|
||||
private final Wallet pastWallet;
|
||||
private final File walletFile;
|
||||
private final String walletId;
|
||||
|
||||
public WalletSettingsChangedEvent(Wallet wallet, Wallet pastWallet, File walletFile) {
|
||||
public WalletSettingsChangedEvent(Wallet wallet, Wallet pastWallet, String walletId) {
|
||||
super(wallet);
|
||||
this.pastWallet = pastWallet;
|
||||
this.walletFile = walletFile;
|
||||
this.walletId = walletId;
|
||||
}
|
||||
|
||||
public Wallet getPastWallet() {
|
||||
return pastWallet;
|
||||
}
|
||||
|
||||
public File getWalletFile() {
|
||||
return walletFile;
|
||||
public String getWalletId() {
|
||||
return walletId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package com.sparrowwallet.sparrow.event;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
public class WalletUtxoStatusChangedEvent extends WalletDataChangedEvent {
|
||||
public class WalletUtxoStatusChangedEvent extends WalletChangedEvent {
|
||||
private final BlockTransactionHashIndex utxo;
|
||||
|
||||
public WalletUtxoStatusChangedEvent(Wallet wallet, BlockTransactionHashIndex utxo) {
|
||||
|
||||
@@ -17,6 +17,9 @@
|
||||
|
||||
package com.sparrowwallet.sparrow.instance;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.DataInputStream;
|
||||
@@ -85,6 +88,7 @@ import java.nio.channels.FileLock;
|
||||
*
|
||||
*/
|
||||
public abstract class Instance {
|
||||
private static final Logger log = LoggerFactory.getLogger(Instance.class);
|
||||
|
||||
// starting position of port check
|
||||
private static final int PORT_START = 7221;
|
||||
@@ -513,7 +517,7 @@ public abstract class Instance {
|
||||
* @param exception exception occurring while first instance is listening for subsequent instances
|
||||
*/
|
||||
protected void handleException(Exception exception) {
|
||||
exception.printStackTrace();
|
||||
log.error("Error listening for instances", exception);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -16,10 +16,12 @@ public class IOUtils {
|
||||
if(file.exists()) {
|
||||
try(BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8))) {
|
||||
String line = br.readLine();
|
||||
if(line.startsWith("01000000") || line.startsWith("cHNid")) {
|
||||
return FileType.TEXT;
|
||||
} else if(line.startsWith("{")) {
|
||||
return FileType.JSON;
|
||||
if(line != null) {
|
||||
if(line.startsWith("01000000") || line.startsWith("cHNid")) {
|
||||
return FileType.TEXT;
|
||||
} else if(line.startsWith("{")) {
|
||||
return FileType.JSON;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.zip.DeflaterOutputStream;
|
||||
import java.util.zip.InflaterInputStream;
|
||||
|
||||
@@ -34,58 +35,77 @@ public class JsonPersistence implements Persistence {
|
||||
this.gson = getGson();
|
||||
}
|
||||
|
||||
public Wallet loadWallet(File jsonFile) throws IOException {
|
||||
try(Reader reader = new FileReader(jsonFile)) {
|
||||
return gson.fromJson(reader, Wallet.class);
|
||||
@Override
|
||||
public WalletBackupAndKey loadWallet(Storage storage) throws IOException, StorageException {
|
||||
Wallet wallet;
|
||||
|
||||
try(Reader reader = new FileReader(storage.getWalletFile())) {
|
||||
wallet = gson.fromJson(reader, Wallet.class);
|
||||
}
|
||||
|
||||
Map<Storage, WalletBackupAndKey> childWallets = loadChildWallets(storage, wallet, null);
|
||||
wallet.setChildWallets(childWallets.values().stream().map(WalletBackupAndKey::getWallet).collect(Collectors.toList()));
|
||||
|
||||
File backupFile = storage.getTempBackup();
|
||||
Wallet backupWallet = backupFile == null ? null : loadWallet(backupFile, null);
|
||||
|
||||
return new WalletBackupAndKey(wallet, backupWallet, null, null, childWallets);
|
||||
}
|
||||
|
||||
public WalletBackupAndKey loadWallet(File walletFile, CharSequence password) throws IOException, StorageException {
|
||||
@Override
|
||||
public WalletBackupAndKey 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 {
|
||||
Wallet wallet;
|
||||
ECKey encryptionKey;
|
||||
|
||||
try(InputStream fileStream = new FileInputStream(walletFile)) {
|
||||
encryptionKey = getEncryptionKey(password, fileStream, null);
|
||||
try(InputStream fileStream = new FileInputStream(storage.getWalletFile())) {
|
||||
encryptionKey = getEncryptionKey(password, fileStream, alreadyDerivedKey);
|
||||
Reader reader = new InputStreamReader(new InflaterInputStream(new ECIESInputStream(fileStream, encryptionKey, getEncryptionMagic())), StandardCharsets.UTF_8);
|
||||
wallet = gson.fromJson(reader, Wallet.class);
|
||||
}
|
||||
|
||||
return new WalletBackupAndKey(wallet, null, encryptionKey, keyDeriver, null);
|
||||
Map<Storage, WalletBackupAndKey> childWallets = loadChildWallets(storage, wallet, encryptionKey);
|
||||
wallet.setChildWallets(childWallets.values().stream().map(WalletBackupAndKey::getWallet).collect(Collectors.toList()));
|
||||
|
||||
File backupFile = storage.getTempBackup();
|
||||
Wallet backupWallet = backupFile == null ? null : loadWallet(backupFile, encryptionKey);
|
||||
|
||||
return new WalletBackupAndKey(wallet, backupWallet, encryptionKey, keyDeriver, childWallets);
|
||||
}
|
||||
|
||||
public Map<File, Wallet> loadWallets(File[] walletFiles, ECKey encryptionKey) throws IOException, StorageException {
|
||||
Map<File, Wallet> walletsMap = new LinkedHashMap<>();
|
||||
for(File file : walletFiles) {
|
||||
if(encryptionKey != null) {
|
||||
try(InputStream fileStream = new FileInputStream(file)) {
|
||||
encryptionKey = getEncryptionKey(null, fileStream, encryptionKey);
|
||||
Reader reader = new InputStreamReader(new InflaterInputStream(new ECIESInputStream(fileStream, encryptionKey, getEncryptionMagic())), StandardCharsets.UTF_8);
|
||||
walletsMap.put(file, gson.fromJson(reader, Wallet.class));
|
||||
}
|
||||
} else {
|
||||
walletsMap.put(file, loadWallet(file));
|
||||
}
|
||||
}
|
||||
|
||||
return walletsMap;
|
||||
}
|
||||
|
||||
public Map<Storage, WalletBackupAndKey> loadChildWallets(File walletFile, Wallet masterWallet, ECKey encryptionKey) throws IOException, StorageException {
|
||||
File[] walletFiles = getChildWalletFiles(walletFile, masterWallet);
|
||||
private Map<Storage, WalletBackupAndKey> loadChildWallets(Storage storage, Wallet masterWallet, ECKey encryptionKey) throws IOException, StorageException {
|
||||
File[] walletFiles = getChildWalletFiles(storage.getWalletFile(), masterWallet);
|
||||
Map<Storage, WalletBackupAndKey> childWallets = new LinkedHashMap<>();
|
||||
Map<File, Wallet> loadedWallets = loadWallets(walletFiles, encryptionKey);
|
||||
for(Map.Entry<File, Wallet> entry : loadedWallets.entrySet()) {
|
||||
Storage storage = new Storage(entry.getKey());
|
||||
storage.setEncryptionPubKey(encryptionKey == null ? Storage.NO_PASSWORD_KEY : ECKey.fromPublicOnly(encryptionKey));
|
||||
storage.setKeyDeriver(getKeyDeriver());
|
||||
Wallet childWallet = entry.getValue();
|
||||
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(storage, new WalletBackupAndKey(childWallet, null, encryptionKey, keyDeriver, Collections.emptyMap()));
|
||||
childWallets.put(childStorage, new WalletBackupAndKey(childWallet, null, encryptionKey, keyDeriver, Collections.emptyMap()));
|
||||
}
|
||||
|
||||
return childWallets;
|
||||
}
|
||||
|
||||
private Wallet loadWallet(File walletFile, ECKey encryptionKey) throws IOException, StorageException {
|
||||
if(encryptionKey != null) {
|
||||
try(InputStream fileStream = new FileInputStream(walletFile)) {
|
||||
encryptionKey = getEncryptionKey(null, fileStream, encryptionKey);
|
||||
Reader reader = new InputStreamReader(new InflaterInputStream(new ECIESInputStream(fileStream, encryptionKey, getEncryptionMagic())), StandardCharsets.UTF_8);
|
||||
return gson.fromJson(reader, Wallet.class);
|
||||
}
|
||||
} else {
|
||||
try(Reader reader = new FileReader(walletFile)) {
|
||||
return gson.fromJson(reader, Wallet.class);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private File[] getChildWalletFiles(File walletFile, Wallet masterWallet) {
|
||||
File childDir = new File(walletFile.getParentFile(), masterWallet.getName() + "-child");
|
||||
if(!childDir.exists()) {
|
||||
@@ -100,14 +120,12 @@ public class JsonPersistence implements Persistence {
|
||||
return childFiles == null ? new File[0] : childFiles;
|
||||
}
|
||||
|
||||
public File storeWallet(File walletFile, Wallet wallet) throws IOException {
|
||||
File parent = walletFile.getParentFile();
|
||||
if(!parent.exists() && !Storage.createOwnerOnlyDirectory(parent)) {
|
||||
throw new IOException("Could not create folder " + parent);
|
||||
}
|
||||
@Override
|
||||
public File storeWallet(Storage storage, Wallet wallet) throws IOException {
|
||||
File walletFile = storage.getWalletFile();
|
||||
|
||||
if(!walletFile.getName().endsWith(".json")) {
|
||||
File jsonFile = new File(parent, walletFile.getName() + ".json");
|
||||
File jsonFile = new File(walletFile.getParentFile(), walletFile.getName() + ".json");
|
||||
if(walletFile.exists()) {
|
||||
if(!walletFile.renameTo(jsonFile)) {
|
||||
throw new IOException("Could not rename " + walletFile.getName() + " to " + jsonFile.getName());
|
||||
@@ -127,14 +145,12 @@ public class JsonPersistence implements Persistence {
|
||||
return walletFile;
|
||||
}
|
||||
|
||||
public File storeWallet(File walletFile, Wallet wallet, ECKey encryptionPubKey) throws IOException {
|
||||
File parent = walletFile.getParentFile();
|
||||
if(!parent.exists() && !Storage.createOwnerOnlyDirectory(parent)) {
|
||||
throw new IOException("Could not create folder " + parent);
|
||||
}
|
||||
@Override
|
||||
public File storeWallet(Storage storage, Wallet wallet, ECKey encryptionPubKey) throws IOException {
|
||||
File walletFile = storage.getWalletFile();
|
||||
|
||||
if(walletFile.getName().endsWith(".json")) {
|
||||
File noJsonFile = new File(parent, walletFile.getName().substring(0, walletFile.getName().lastIndexOf('.')));
|
||||
File noJsonFile = new File(walletFile.getParentFile(), walletFile.getName().substring(0, walletFile.getName().lastIndexOf('.')));
|
||||
if(walletFile.exists()) {
|
||||
if(!walletFile.renameTo(noJsonFile)) {
|
||||
throw new IOException("Could not rename " + walletFile.getName() + " to " + noJsonFile.getName());
|
||||
@@ -158,6 +174,16 @@ public class JsonPersistence implements Persistence {
|
||||
return walletFile;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateWallet(Storage storage, Wallet wallet) throws IOException {
|
||||
storeWallet(storage, wallet);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateWallet(Storage storage, Wallet wallet, ECKey encryptionPubKey) throws IOException {
|
||||
storeWallet(storage, wallet, encryptionPubKey);
|
||||
}
|
||||
|
||||
private void writeBinaryHeader(OutputStream outputStream) throws IOException {
|
||||
ByteBuffer buf = ByteBuffer.allocate(21);
|
||||
buf.put(HEADER_MAGIC_1.getBytes(StandardCharsets.UTF_8));
|
||||
@@ -174,6 +200,7 @@ public class JsonPersistence implements Persistence {
|
||||
return "BIE1".getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ECKey getEncryptionKey(CharSequence password) throws IOException, StorageException {
|
||||
return getEncryptionKey(password, null, null);
|
||||
}
|
||||
@@ -187,10 +214,12 @@ public class JsonPersistence implements Persistence {
|
||||
return alreadyDerivedKey == null ? keyDeriver.deriveECKey(password) : alreadyDerivedKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AsymmetricKeyDeriver getKeyDeriver() {
|
||||
return keyDeriver;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setKeyDeriver(AsymmetricKeyDeriver keyDeriver) {
|
||||
this.keyDeriver = keyDeriver;
|
||||
}
|
||||
@@ -232,10 +261,53 @@ public class JsonPersistence implements Persistence {
|
||||
return new Argon2KeyDeriver(salt);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PersistenceType getType() {
|
||||
return PersistenceType.JSON;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEncrypted(File walletFile) throws IOException {
|
||||
FileType fileType = IOUtils.getFileType(walletFile);
|
||||
if(FileType.JSON.equals(fileType)) {
|
||||
return false;
|
||||
} else if(FileType.BINARY.equals(fileType)) {
|
||||
try(FileInputStream fileInputStream = new FileInputStream(walletFile)) {
|
||||
getWalletKeyDeriver(fileInputStream);
|
||||
return true;
|
||||
} catch(StorageException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
throw new IOException("Unsupported file type");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getWalletId(Storage storage, Wallet wallet) {
|
||||
return storage.getWalletFile().getParentFile().getAbsolutePath() + File.separator + getWalletName(storage.getWalletFile(), null) + ":" + (wallet == null || wallet.isMasterWallet() ? "master" : wallet.getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getWalletName(File walletFile, Wallet wallet) {
|
||||
String name = walletFile.getName();
|
||||
if(name.endsWith("." + getType().getExtension())) {
|
||||
name = name.substring(0, name.lastIndexOf('.'));
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void copyWallet(File walletFile, OutputStream outputStream) throws IOException {
|
||||
com.google.common.io.Files.copy(walletFile, outputStream);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
//Nothing required
|
||||
}
|
||||
|
||||
public static Gson getGson() {
|
||||
return getGson(true);
|
||||
}
|
||||
@@ -261,7 +333,7 @@ public class JsonPersistence implements Persistence {
|
||||
gsonBuilder.addSerializationExclusionStrategy(new ExclusionStrategy() {
|
||||
@Override
|
||||
public boolean shouldSkipField(FieldAttributes field) {
|
||||
return field.getDeclaringClass() == Wallet.class && field.getName().equals("masterWallet");
|
||||
return field.getName().equals("id") || field.getName().equals("masterWallet") || field.getName().equals("childWallets");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -6,17 +6,23 @@ import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public interface Persistence {
|
||||
Wallet loadWallet(File walletFile) throws IOException;
|
||||
WalletBackupAndKey loadWallet(File walletFile, CharSequence password) throws IOException, StorageException;
|
||||
Map<File, Wallet> loadWallets(File[] walletFiles, ECKey encryptionKey) throws IOException, StorageException;
|
||||
Map<Storage, WalletBackupAndKey> loadChildWallets(File walletFile, Wallet masterWallet, ECKey encryptionKey) throws IOException, StorageException;
|
||||
File storeWallet(File walletFile, Wallet wallet) throws IOException;
|
||||
File storeWallet(File walletFile, Wallet wallet, ECKey encryptionPubKey) throws IOException;
|
||||
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;
|
||||
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;
|
||||
void updateWallet(Storage storage, Wallet wallet, ECKey encryptionPubKey) throws IOException, StorageException;
|
||||
ECKey getEncryptionKey(CharSequence password) throws IOException, StorageException;
|
||||
AsymmetricKeyDeriver getKeyDeriver();
|
||||
void setKeyDeriver(AsymmetricKeyDeriver keyDeriver);
|
||||
PersistenceType getType();
|
||||
boolean isEncrypted(File walletFile) throws IOException;
|
||||
String getWalletId(Storage storage, Wallet wallet);
|
||||
String getWalletName(File walletFile, Wallet wallet);
|
||||
void copyWallet(File walletFile, OutputStream outputStream) throws IOException;
|
||||
void close();
|
||||
}
|
||||
|
||||
@@ -1,7 +1,30 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.sparrowwallet.sparrow.io.db.DbPersistence;
|
||||
|
||||
public enum PersistenceType {
|
||||
JSON("json");
|
||||
JSON("json") {
|
||||
@Override
|
||||
public String getExtension() {
|
||||
return getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Persistence getInstance() {
|
||||
return new JsonPersistence();
|
||||
}
|
||||
},
|
||||
DB("db") {
|
||||
@Override
|
||||
public String getExtension() {
|
||||
return "mv.db";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Persistence getInstance() {
|
||||
return new DbPersistence();
|
||||
}
|
||||
};
|
||||
|
||||
private final String name;
|
||||
|
||||
@@ -13,7 +36,7 @@ public enum PersistenceType {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getExtension() {
|
||||
return getName();
|
||||
}
|
||||
public abstract String getExtension();
|
||||
|
||||
public abstract Persistence getInstance();
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ public class Sparrow implements WalletExport {
|
||||
public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException {
|
||||
try {
|
||||
Storage storage = AppServices.get().getOpenWallets().get(wallet);
|
||||
Files.copy(storage.getWalletFile().toPath(), outputStream);
|
||||
storage.copyWallet(outputStream);
|
||||
outputStream.flush();
|
||||
} catch(Exception e) {
|
||||
log.error("Error exporting Sparrow wallet file", e);
|
||||
@@ -42,11 +42,7 @@ public class Sparrow implements WalletExport {
|
||||
@Override
|
||||
public String getExportFileExtension(Wallet wallet) {
|
||||
Storage storage = AppServices.get().getOpenWallets().get(wallet);
|
||||
if(storage != null && (storage.getEncryptionPubKey() == null || Storage.NO_PASSWORD_KEY.equals(storage.getEncryptionPubKey()))) {
|
||||
return "json";
|
||||
}
|
||||
|
||||
return "";
|
||||
return storage.getWalletFileExtension();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -36,14 +36,23 @@ 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_EXTENSION = "tmp";
|
||||
public static final String TEMP_BACKUP_PREFIX = "tmp";
|
||||
|
||||
private final Persistence persistence;
|
||||
private Persistence persistence;
|
||||
private File walletFile;
|
||||
private ECKey encryptionPubKey;
|
||||
|
||||
public Storage(File walletFile) {
|
||||
this.persistence = new JsonPersistence();
|
||||
this(!walletFile.exists() || walletFile.getName().endsWith("." + PersistenceType.DB.getExtension()) ? PersistenceType.DB : PersistenceType.JSON, walletFile);
|
||||
}
|
||||
|
||||
public Storage(PersistenceType persistenceType, File walletFile) {
|
||||
this.persistence = persistenceType.getInstance();
|
||||
this.walletFile = walletFile;
|
||||
}
|
||||
|
||||
public Storage(Persistence persistence, File walletFile) {
|
||||
this.persistence = persistence;
|
||||
this.walletFile = walletFile;
|
||||
}
|
||||
|
||||
@@ -51,45 +60,63 @@ public class Storage {
|
||||
return walletFile;
|
||||
}
|
||||
|
||||
public WalletBackupAndKey loadUnencryptedWallet() throws IOException, StorageException {
|
||||
Wallet wallet = persistence.loadWallet(walletFile);
|
||||
Wallet backupWallet = loadBackupWallet(null);
|
||||
Map<Storage, WalletBackupAndKey> childWallets = persistence.loadChildWallets(walletFile, wallet, null);
|
||||
public boolean isEncrypted() throws IOException {
|
||||
return persistence.isEncrypted(walletFile);
|
||||
}
|
||||
|
||||
public String getWalletId(Wallet wallet) {
|
||||
return persistence.getWalletId(this, wallet);
|
||||
}
|
||||
|
||||
public String getWalletName(Wallet wallet) {
|
||||
return persistence.getWalletName(walletFile, wallet);
|
||||
}
|
||||
|
||||
public String getWalletFileExtension() {
|
||||
if(walletFile.getName().endsWith("." + getType().getExtension())) {
|
||||
return getType().getExtension();
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
public WalletBackupAndKey loadUnencryptedWallet() throws IOException, StorageException {
|
||||
WalletBackupAndKey masterWalletAndKey = persistence.loadWallet(this);
|
||||
encryptionPubKey = NO_PASSWORD_KEY;
|
||||
return new WalletBackupAndKey(wallet, backupWallet, null, null, childWallets);
|
||||
return migrateToDb(masterWalletAndKey);
|
||||
}
|
||||
|
||||
public WalletBackupAndKey loadEncryptedWallet(CharSequence password) throws IOException, StorageException {
|
||||
WalletBackupAndKey masterWalletAndKey = persistence.loadWallet(walletFile, password);
|
||||
Wallet backupWallet = loadBackupWallet(masterWalletAndKey.getEncryptionKey());
|
||||
Map<Storage, WalletBackupAndKey> childWallets = persistence.loadChildWallets(walletFile, masterWalletAndKey.getWallet(), masterWalletAndKey.getEncryptionKey());
|
||||
|
||||
WalletBackupAndKey masterWalletAndKey = persistence.loadWallet(this, password);
|
||||
encryptionPubKey = ECKey.fromPublicOnly(masterWalletAndKey.getEncryptionKey());
|
||||
return new WalletBackupAndKey(masterWalletAndKey.getWallet(), backupWallet, masterWalletAndKey.getEncryptionKey(), persistence.getKeyDeriver(), childWallets);
|
||||
return migrateToDb(masterWalletAndKey);
|
||||
}
|
||||
|
||||
protected Wallet loadBackupWallet(ECKey encryptionKey) throws IOException, StorageException {
|
||||
Map<File, Wallet> backupWallets;
|
||||
if(encryptionKey != null) {
|
||||
File[] backups = getBackups(TEMP_BACKUP_EXTENSION, persistence.getType().getExtension() + "." + TEMP_BACKUP_EXTENSION);
|
||||
backupWallets = persistence.loadWallets(backups, encryptionKey);
|
||||
return backupWallets.isEmpty() ? null : backupWallets.values().iterator().next();
|
||||
} else {
|
||||
File[] backups = getBackups(persistence.getType().getExtension() + "." + TEMP_BACKUP_EXTENSION);
|
||||
backupWallets = persistence.loadWallets(backups, null);
|
||||
public void saveWallet(Wallet wallet) throws IOException, StorageException {
|
||||
File parent = walletFile.getParentFile();
|
||||
if(!parent.exists() && !Storage.createOwnerOnlyDirectory(parent)) {
|
||||
throw new IOException("Could not create folder " + parent);
|
||||
}
|
||||
|
||||
return backupWallets.isEmpty() ? null : backupWallets.values().iterator().next();
|
||||
}
|
||||
|
||||
public void saveWallet(Wallet wallet) throws IOException {
|
||||
if(encryptionPubKey != null && !NO_PASSWORD_KEY.equals(encryptionPubKey)) {
|
||||
walletFile = persistence.storeWallet(walletFile, wallet, encryptionPubKey);
|
||||
walletFile = persistence.storeWallet(this, wallet, encryptionPubKey);
|
||||
return;
|
||||
}
|
||||
|
||||
walletFile = persistence.storeWallet(walletFile, wallet);
|
||||
walletFile = persistence.storeWallet(this, wallet);
|
||||
}
|
||||
|
||||
public void updateWallet(Wallet wallet) throws IOException, StorageException {
|
||||
if(encryptionPubKey != null && !NO_PASSWORD_KEY.equals(encryptionPubKey)) {
|
||||
persistence.updateWallet(this, wallet, encryptionPubKey);
|
||||
} else {
|
||||
persistence.updateWallet(this, wallet);
|
||||
}
|
||||
}
|
||||
|
||||
public void close() {
|
||||
ClosePersistenceService closePersistenceService = new ClosePersistenceService();
|
||||
closePersistenceService.start();
|
||||
}
|
||||
|
||||
public void backupWallet() throws IOException {
|
||||
@@ -100,34 +127,36 @@ public class Storage {
|
||||
|
||||
public void backupTempWallet() {
|
||||
try {
|
||||
backupWallet(TEMP_BACKUP_EXTENSION);
|
||||
backupWallet(TEMP_BACKUP_PREFIX);
|
||||
} catch(IOException e) {
|
||||
log.error("Error creating ." + TEMP_BACKUP_EXTENSION + " backup wallet", e);
|
||||
log.error("Error creating " + TEMP_BACKUP_PREFIX + " backup wallet", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void backupWallet(String extension) throws IOException {
|
||||
private void backupWallet(String prefix) throws IOException {
|
||||
File backupDir = getWalletsBackupDir();
|
||||
|
||||
Date backupDate = new Date();
|
||||
String backupName = walletFile.getName();
|
||||
String walletName = persistence.getWalletName(walletFile, null);
|
||||
String dateSuffix = "-" + BACKUP_DATE_FORMAT.format(backupDate);
|
||||
int lastDot = backupName.lastIndexOf('.');
|
||||
if(lastDot > 0) {
|
||||
backupName = backupName.substring(0, lastDot) + dateSuffix + backupName.substring(lastDot);
|
||||
} else {
|
||||
backupName += dateSuffix;
|
||||
}
|
||||
String backupName = walletName + dateSuffix + walletFile.getName().substring(walletName.length());
|
||||
|
||||
if(extension != null) {
|
||||
backupName += "." + extension;
|
||||
if(prefix != null) {
|
||||
backupName = prefix + "_" + backupName;
|
||||
}
|
||||
|
||||
File backupFile = new File(backupDir, backupName);
|
||||
if(!backupFile.exists()) {
|
||||
createOwnerOnlyFile(backupFile);
|
||||
}
|
||||
com.google.common.io.Files.copy(walletFile, backupFile);
|
||||
|
||||
try(FileOutputStream outputStream = new FileOutputStream(backupFile)) {
|
||||
copyWallet(outputStream);
|
||||
}
|
||||
}
|
||||
|
||||
public void copyWallet(OutputStream outputStream) throws IOException {
|
||||
persistence.copyWallet(walletFile, outputStream);
|
||||
}
|
||||
|
||||
public void deleteBackups() {
|
||||
@@ -135,27 +164,29 @@ public class Storage {
|
||||
}
|
||||
|
||||
public void deleteTempBackups() {
|
||||
deleteBackups(Storage.TEMP_BACKUP_EXTENSION);
|
||||
deleteBackups(Storage.TEMP_BACKUP_PREFIX);
|
||||
}
|
||||
|
||||
private void deleteBackups(String extension) {
|
||||
File[] backups = getBackups(extension);
|
||||
private void deleteBackups(String prefix) {
|
||||
File[] backups = getBackups(prefix);
|
||||
for(File backup : backups) {
|
||||
backup.delete();
|
||||
}
|
||||
}
|
||||
|
||||
private File[] getBackups(String extension) {
|
||||
return getBackups(extension, null);
|
||||
public File getTempBackup() {
|
||||
File[] backups = getBackups(TEMP_BACKUP_PREFIX);
|
||||
return backups.length == 0 ? null : backups[0];
|
||||
}
|
||||
|
||||
private File[] getBackups(String extension, String notExtension) {
|
||||
File[] getBackups(String prefix) {
|
||||
File backupDir = getWalletsBackupDir();
|
||||
String walletName = persistence.getWalletName(walletFile, null);
|
||||
String extension = walletFile.getName().substring(walletName.length());
|
||||
File[] backups = backupDir.listFiles((dir, name) -> {
|
||||
return name.startsWith(com.google.common.io.Files.getNameWithoutExtension(walletFile.getName()) + "-") &&
|
||||
return name.startsWith((prefix == null ? "" : prefix + "_") + walletName + "-") &&
|
||||
getBackupDate(name) != null &&
|
||||
(extension == null || name.endsWith("." + extension)) &&
|
||||
(notExtension == null || !name.endsWith("." + notExtension));
|
||||
(extension.isEmpty() || name.endsWith(extension));
|
||||
});
|
||||
|
||||
backups = backups == null ? new File[0] : backups;
|
||||
@@ -173,6 +204,49 @@ public class Storage {
|
||||
return null;
|
||||
}
|
||||
|
||||
private WalletBackupAndKey migrateToDb(WalletBackupAndKey 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());
|
||||
}
|
||||
|
||||
return masterWalletAndKey;
|
||||
}
|
||||
|
||||
private WalletBackupAndKey migrateType(PersistenceType type, Wallet wallet, ECKey encryptionKey) throws IOException, StorageException {
|
||||
File existingFile = walletFile;
|
||||
|
||||
try {
|
||||
AsymmetricKeyDeriver keyDeriver = persistence.getKeyDeriver();
|
||||
persistence = type.getInstance();
|
||||
persistence.setKeyDeriver(keyDeriver);
|
||||
walletFile = new File(walletFile.getParentFile(), wallet.getName() + "." + type.getExtension());
|
||||
if(walletFile.exists()) {
|
||||
walletFile.delete();
|
||||
}
|
||||
|
||||
saveWallet(wallet);
|
||||
if(type == PersistenceType.DB) {
|
||||
for(Wallet childWallet : wallet.getChildWallets()) {
|
||||
saveWallet(childWallet);
|
||||
}
|
||||
}
|
||||
|
||||
if(NO_PASSWORD_KEY.equals(encryptionPubKey)) {
|
||||
return persistence.loadWallet(this);
|
||||
}
|
||||
|
||||
return persistence.loadWallet(this, null, encryptionKey);
|
||||
} catch(Exception e) {
|
||||
existingFile = null;
|
||||
throw e;
|
||||
} finally {
|
||||
if(existingFile != null) {
|
||||
existingFile.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ECKey getEncryptionPubKey() {
|
||||
return encryptionPubKey;
|
||||
}
|
||||
@@ -193,6 +267,10 @@ public class Storage {
|
||||
persistence.setKeyDeriver(keyDeriver);
|
||||
}
|
||||
|
||||
public PersistenceType getType() {
|
||||
return persistence.getType();
|
||||
}
|
||||
|
||||
public static boolean walletExists(String walletName) {
|
||||
File encrypted = new File(getWalletsDir(), walletName.trim());
|
||||
if(encrypted.exists()) {
|
||||
@@ -230,6 +308,38 @@ public class Storage {
|
||||
return new File(getWalletsDir(), walletName);
|
||||
}
|
||||
|
||||
public static boolean isWalletFile(File walletFile) {
|
||||
for(PersistenceType type : PersistenceType.values()) {
|
||||
if(walletFile.getName().endsWith("." + type.getExtension())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
if(type == PersistenceType.JSON && type.getInstance().isEncrypted(walletFile)) {
|
||||
return true;
|
||||
}
|
||||
} catch(IOException e) {
|
||||
//ignore
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean isEncrypted(File walletFile) {
|
||||
try {
|
||||
for(PersistenceType type : PersistenceType.values()) {
|
||||
if(walletFile.getName().endsWith("." + type.getExtension())) {
|
||||
return type.getInstance().isEncrypted(walletFile);
|
||||
}
|
||||
}
|
||||
} catch(IOException e) {
|
||||
//ignore
|
||||
}
|
||||
|
||||
return FileType.BINARY.equals(IOUtils.getFileType(walletFile));
|
||||
}
|
||||
|
||||
public static File getWalletsBackupDir() {
|
||||
File walletsBackupDir = new File(getWalletsDir(), WALLETS_BACKUP_DIR);
|
||||
if(!walletsBackupDir.exists()) {
|
||||
@@ -432,4 +542,16 @@ public class Storage {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public class ClosePersistenceService extends Service<Void> {
|
||||
@Override
|
||||
protected Task<Void> createTask() {
|
||||
return new Task<>() {
|
||||
protected Void call() {
|
||||
persistence.close();
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.sparrowwallet.sparrow.io.db;
|
||||
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransaction;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import org.jdbi.v3.sqlobject.config.RegisterRowMapper;
|
||||
import org.jdbi.v3.sqlobject.customizer.Bind;
|
||||
import org.jdbi.v3.sqlobject.statement.GetGeneratedKeys;
|
||||
import org.jdbi.v3.sqlobject.statement.SqlQuery;
|
||||
import org.jdbi.v3.sqlobject.statement.SqlUpdate;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
|
||||
public interface BlockTransactionDao {
|
||||
@SqlQuery("select id, txid, hash, height, date, fee, label, transaction, blockHash from blockTransaction where wallet = ? order by id")
|
||||
@RegisterRowMapper(BlockTransactionMapper.class)
|
||||
Map<Sha256Hash, BlockTransaction> getForWalletId(Long id);
|
||||
|
||||
@SqlQuery("select id, txid, hash, height, date, fee, label, transaction, blockHash from blockTransaction where txid = ?")
|
||||
@RegisterRowMapper(BlockTransactionMapper.class)
|
||||
Map<Sha256Hash, BlockTransaction> getForTxId(byte[] id);
|
||||
|
||||
@SqlUpdate("insert into blockTransaction (txid, hash, height, date, fee, label, transaction, blockHash, wallet) values (?, ?, ?, ?, ?, ?, ?, ?, ?)")
|
||||
@GetGeneratedKeys("id")
|
||||
long insertBlockTransaction(byte[] txid, byte[] hash, int height, Date date, Long fee, String label, byte[] transaction, byte[] blockHash, long wallet);
|
||||
|
||||
@SqlUpdate("update blockTransaction set txid = ?, hash = ?, height = ?, date = ?, fee = ?, label = ?, transaction = ?, blockHash = ?, wallet = ? where id = ?")
|
||||
void updateBlockTransaction(byte[] txid, byte[] hash, int height, Date date, Long fee, String label, byte[] transaction, byte[] blockHash, long wallet, long id);
|
||||
|
||||
@SqlUpdate("update blockTransaction set label = :label where id = :id")
|
||||
void updateLabel(@Bind("id") long id, @Bind("label") String label);
|
||||
|
||||
@SqlUpdate("delete from blockTransaction where wallet = ?")
|
||||
void clear(long wallet);
|
||||
|
||||
default void addBlockTransactions(Wallet wallet) {
|
||||
for(Map.Entry<Sha256Hash, BlockTransaction> blkTxEntry : wallet.getTransactions().entrySet()) {
|
||||
blkTxEntry.getValue().setId(null);
|
||||
addOrUpdate(wallet, blkTxEntry.getKey(), blkTxEntry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
default void addOrUpdate(Wallet wallet, Sha256Hash txid, BlockTransaction blkTx) {
|
||||
Map<Sha256Hash, BlockTransaction> existing = getForTxId(txid.getBytes());
|
||||
|
||||
if(existing.isEmpty() && blkTx.getId() == null) {
|
||||
long id = insertBlockTransaction(txid.getBytes(), blkTx.getHash().getBytes(), blkTx.getHeight(), blkTx.getDate(), blkTx.getFee(), blkTx.getLabel(),
|
||||
blkTx.getTransaction() == null ? null : blkTx.getTransaction().bitcoinSerialize(),
|
||||
blkTx.getBlockHash() == null ? null : blkTx.getBlockHash().getBytes(), wallet.getId());
|
||||
blkTx.setId(id);
|
||||
} else {
|
||||
Long existingId = existing.get(txid) != null ? existing.get(txid).getId() : blkTx.getId();
|
||||
updateBlockTransaction(txid.getBytes(), blkTx.getHash().getBytes(), blkTx.getHeight(), blkTx.getDate(), blkTx.getFee(), blkTx.getLabel(),
|
||||
blkTx.getTransaction() == null ? null : blkTx.getTransaction().bitcoinSerialize(),
|
||||
blkTx.getBlockHash() == null ? null : blkTx.getBlockHash().getBytes(), wallet.getId(), existingId);
|
||||
blkTx.setId(existingId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.sparrowwallet.sparrow.io.db;
|
||||
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
|
||||
import com.sparrowwallet.drongo.wallet.Status;
|
||||
import org.jdbi.v3.core.mapper.RowMapper;
|
||||
import org.jdbi.v3.core.statement.StatementContext;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
|
||||
public class BlockTransactionHashIndexMapper implements RowMapper<BlockTransactionHashIndex> {
|
||||
@Override
|
||||
public BlockTransactionHashIndex map(ResultSet rs, StatementContext ctx) throws SQLException {
|
||||
BlockTransactionHashIndex blockTransactionHashIndex = new BlockTransactionHashIndex(Sha256Hash.wrap(rs.getBytes("blockTransactionHashIndex.hash")),
|
||||
rs.getInt("blockTransactionHashIndex.height"), rs.getTimestamp("blockTransactionHashIndex.date"), rs.getLong("blockTransactionHashIndex.fee"),
|
||||
rs.getLong("blockTransactionHashIndex.index"), rs.getLong("blockTransactionHashIndex.value"), null, rs.getString("blockTransactionHashIndex.label"));
|
||||
blockTransactionHashIndex.setId(rs.getLong("blockTransactionHashIndex.id"));
|
||||
int statusIndex = rs.getInt("blockTransactionHashIndex.status");
|
||||
if(!rs.wasNull()) {
|
||||
blockTransactionHashIndex.setStatus(Status.values()[statusIndex]);
|
||||
}
|
||||
|
||||
return blockTransactionHashIndex;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.sparrowwallet.sparrow.io.db;
|
||||
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransaction;
|
||||
import org.jdbi.v3.core.mapper.RowMapper;
|
||||
import org.jdbi.v3.core.statement.StatementContext;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Map;
|
||||
|
||||
public class BlockTransactionMapper implements RowMapper<Map.Entry<Sha256Hash, BlockTransaction>> {
|
||||
|
||||
@Override
|
||||
public Map.Entry<Sha256Hash, BlockTransaction> map(ResultSet rs, StatementContext ctx) throws SQLException {
|
||||
Sha256Hash txid = Sha256Hash.wrap(rs.getBytes("txid"));
|
||||
|
||||
byte[] txBytes = rs.getBytes("transaction");
|
||||
Transaction transaction = null;
|
||||
if(txBytes != null) {
|
||||
transaction = new Transaction(txBytes);
|
||||
}
|
||||
|
||||
Long fee = rs.getLong("fee");
|
||||
if(rs.wasNull()) {
|
||||
fee = null;
|
||||
}
|
||||
|
||||
BlockTransaction blockTransaction = new BlockTransaction(Sha256Hash.wrap(rs.getBytes("hash")), rs.getInt("height"), rs.getTimestamp("date"),
|
||||
fee, transaction, rs.getBytes("blockHash") == null ? null : Sha256Hash.wrap(rs.getBytes("blockHash")), rs.getString("label"));
|
||||
blockTransaction.setId(rs.getLong("id"));
|
||||
|
||||
return new Map.Entry<>() {
|
||||
@Override
|
||||
public Sha256Hash getKey() {
|
||||
return txid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlockTransaction getValue() {
|
||||
return blockTransaction;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlockTransaction setValue(BlockTransaction value) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,607 @@
|
||||
package com.sparrowwallet.sparrow.io.db;
|
||||
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.crypto.Argon2KeyDeriver;
|
||||
import com.sparrowwallet.drongo.crypto.AsymmetricKeyDeriver;
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
import com.sparrowwallet.drongo.crypto.InvalidPasswordException;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.*;
|
||||
import com.sparrowwallet.sparrow.io.*;
|
||||
import com.sparrowwallet.sparrow.wallet.*;
|
||||
import com.zaxxer.hikari.HikariConfig;
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
import com.zaxxer.hikari.pool.HikariPool;
|
||||
import org.flywaydb.core.Flyway;
|
||||
import org.flywaydb.core.api.FlywayException;
|
||||
import org.flywaydb.core.api.exception.FlywayValidateException;
|
||||
import org.h2.tools.ChangeFileEncryption;
|
||||
import org.jdbi.v3.core.Jdbi;
|
||||
import org.jdbi.v3.core.h2.H2DatabasePlugin;
|
||||
import org.jdbi.v3.sqlobject.SqlObjectPlugin;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class DbPersistence implements Persistence {
|
||||
private static final Logger log = LoggerFactory.getLogger(DbPersistence.class);
|
||||
|
||||
static final String DEFAULT_SCHEMA = "PUBLIC";
|
||||
private static final String WALLET_SCHEMA_PREFIX = "wallet_";
|
||||
private static final String MASTER_SCHEMA = WALLET_SCHEMA_PREFIX + "master";
|
||||
private static final byte[] H2_ENCRYPT_HEADER = "H2encrypt\n".getBytes(StandardCharsets.UTF_8);
|
||||
private static final int H2_ENCRYPT_SALT_LENGTH_BYTES = 8;
|
||||
private static final int SALT_LENGTH_BYTES = 16;
|
||||
public static final byte[] HEADER_MAGIC_1 = "SPRW1\n".getBytes(StandardCharsets.UTF_8);
|
||||
private static final String H2_USER = "sa";
|
||||
private static final String H2_PASSWORD = "";
|
||||
|
||||
private HikariDataSource dataSource;
|
||||
private AsymmetricKeyDeriver keyDeriver;
|
||||
|
||||
private Wallet masterWallet;
|
||||
private final Map<Wallet, DirtyPersistables> dirtyPersistablesMap = new HashMap<>();
|
||||
|
||||
public DbPersistence() {
|
||||
EventManager.get().register(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public WalletBackupAndKey loadWallet(Storage storage) throws IOException, StorageException {
|
||||
return loadWallet(storage, null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public WalletBackupAndKey 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 {
|
||||
ECKey encryptionKey = getEncryptionKey(password, storage.getWalletFile(), alreadyDerivedKey);
|
||||
|
||||
migrate(storage, MASTER_SCHEMA, encryptionKey);
|
||||
|
||||
Jdbi jdbi = getJdbi(storage, getFilePassword(encryptionKey));
|
||||
masterWallet = jdbi.withHandle(handle -> {
|
||||
WalletDao walletDao = handle.attach(WalletDao.class);
|
||||
return walletDao.getMainWallet(MASTER_SCHEMA);
|
||||
});
|
||||
|
||||
File backupFile = storage.getTempBackup();
|
||||
Wallet backupWallet = null;
|
||||
if(backupFile != null) {
|
||||
Persistence backupPersistence = PersistenceType.DB.getInstance();
|
||||
backupWallet = backupPersistence.loadWallet(new Storage(backupPersistence, backupFile), password, encryptionKey).getWallet();
|
||||
}
|
||||
|
||||
Map<Storage, WalletBackupAndKey> childWallets = loadChildWallets(storage, masterWallet, backupWallet, encryptionKey);
|
||||
masterWallet.setChildWallets(childWallets.values().stream().map(WalletBackupAndKey::getWallet).collect(Collectors.toList()));
|
||||
|
||||
return new WalletBackupAndKey(masterWallet, backupWallet, encryptionKey, keyDeriver, childWallets);
|
||||
}
|
||||
|
||||
private Map<Storage, WalletBackupAndKey> loadChildWallets(Storage storage, Wallet masterWallet, Wallet backupWallet, 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<Storage, WalletBackupAndKey> childWallets = new LinkedHashMap<>();
|
||||
for(String schema : childSchemas) {
|
||||
migrate(storage, schema, encryptionKey);
|
||||
|
||||
Jdbi childJdbi = getJdbi(storage, getFilePassword(encryptionKey));
|
||||
Wallet wallet = childJdbi.withHandle(handle -> {
|
||||
WalletDao walletDao = handle.attach(WalletDao.class);
|
||||
Wallet childWallet = walletDao.getMainWallet(schema);
|
||||
childWallet.setName(schema.substring(WALLET_SCHEMA_PREFIX.length()));
|
||||
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(storage, new WalletBackupAndKey(wallet, backupChildWallet, encryptionKey, keyDeriver, Collections.emptyMap()));
|
||||
}
|
||||
|
||||
return childWallets;
|
||||
}
|
||||
|
||||
@Override
|
||||
public File storeWallet(Storage storage, Wallet wallet) throws IOException, StorageException {
|
||||
File walletFile = storage.getWalletFile();
|
||||
walletFile = renameToDbFile(walletFile);
|
||||
|
||||
if(walletFile.exists() && isEncrypted(walletFile)) {
|
||||
if(dataSource != null && !dataSource.isClosed()) {
|
||||
dataSource.close();
|
||||
}
|
||||
walletFile.delete();
|
||||
}
|
||||
|
||||
updatePassword(storage, null);
|
||||
cleanAndAddWallet(storage, wallet, null);
|
||||
|
||||
return walletFile;
|
||||
}
|
||||
|
||||
@Override
|
||||
public File storeWallet(Storage storage, Wallet wallet, ECKey encryptionPubKey) throws IOException, StorageException {
|
||||
File walletFile = storage.getWalletFile();
|
||||
walletFile = renameToDbFile(walletFile);
|
||||
|
||||
if(walletFile.exists() && !isEncrypted(walletFile)) {
|
||||
if(dataSource != null && !dataSource.isClosed()) {
|
||||
dataSource.close();
|
||||
}
|
||||
walletFile.delete();
|
||||
}
|
||||
|
||||
boolean existing = walletFile.exists();
|
||||
updatePassword(storage, encryptionPubKey);
|
||||
cleanAndAddWallet(storage, wallet, getFilePassword(encryptionPubKey));
|
||||
if(!existing) {
|
||||
writeBinaryHeader(walletFile);
|
||||
}
|
||||
|
||||
return walletFile;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateWallet(Storage storage, Wallet wallet) throws StorageException {
|
||||
updateWallet(storage, wallet, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateWallet(Storage storage, Wallet wallet, ECKey encryptionPubKey) throws StorageException {
|
||||
updatePassword(storage, encryptionPubKey);
|
||||
update(storage, wallet, getFilePassword(encryptionPubKey));
|
||||
}
|
||||
|
||||
private File renameToDbFile(File walletFile) throws IOException {
|
||||
if(!walletFile.getName().endsWith("." + getType().getExtension())) {
|
||||
File dbFile = new File(walletFile.getParentFile(), walletFile.getName() + "." + getType().getExtension());
|
||||
if(walletFile.exists()) {
|
||||
if(!walletFile.renameTo(dbFile)) {
|
||||
throw new IOException("Could not rename " + walletFile.getName() + " to " + dbFile.getName());
|
||||
}
|
||||
}
|
||||
|
||||
return dbFile;
|
||||
}
|
||||
|
||||
return walletFile;
|
||||
}
|
||||
|
||||
private void update(Storage storage, Wallet wallet, String password) throws StorageException {
|
||||
DirtyPersistables dirtyPersistables = dirtyPersistablesMap.get(wallet);
|
||||
if(dirtyPersistables == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
log.debug("Updating " + wallet.getName() + " on " + Thread.currentThread().getName());
|
||||
log.debug(dirtyPersistables.toString());
|
||||
|
||||
Jdbi jdbi = getJdbi(storage, password);
|
||||
jdbi.useHandle(handle -> {
|
||||
WalletDao walletDao = handle.attach(WalletDao.class);
|
||||
try {
|
||||
walletDao.setSchema(getSchema(wallet));
|
||||
|
||||
if(dirtyPersistables.clearHistory) {
|
||||
WalletNodeDao walletNodeDao = handle.attach(WalletNodeDao.class);
|
||||
BlockTransactionDao blockTransactionDao = handle.attach(BlockTransactionDao.class);
|
||||
walletNodeDao.clearHistory(wallet);
|
||||
blockTransactionDao.clear(wallet.getId());
|
||||
}
|
||||
|
||||
if(!dirtyPersistables.historyNodes.isEmpty()) {
|
||||
WalletNodeDao walletNodeDao = handle.attach(WalletNodeDao.class);
|
||||
BlockTransactionDao blockTransactionDao = handle.attach(BlockTransactionDao.class);
|
||||
Set<Sha256Hash> referencedTxIds = new HashSet<>();
|
||||
for(WalletNode addressNode : dirtyPersistables.historyNodes) {
|
||||
if(addressNode.getId() == null) {
|
||||
WalletNode purposeNode = wallet.getNode(addressNode.getKeyPurpose());
|
||||
if(purposeNode.getId() == null) {
|
||||
long purposeNodeId = walletNodeDao.insertWalletNode(purposeNode.getDerivationPath(), purposeNode.getLabel(), wallet.getId(), null);
|
||||
purposeNode.setId(purposeNodeId);
|
||||
}
|
||||
|
||||
long nodeId = walletNodeDao.insertWalletNode(addressNode.getDerivationPath(), addressNode.getLabel(), wallet.getId(), purposeNode.getId());
|
||||
addressNode.setId(nodeId);
|
||||
}
|
||||
|
||||
Set<BlockTransactionHashIndex> txos = addressNode.getTransactionOutputs().stream().flatMap(txo -> txo.isSpent() ? Stream.of(txo, txo.getSpentBy()) : Stream.of(txo)).collect(Collectors.toSet());
|
||||
List<Long> existingIds = txos.stream().map(Persistable::getId).filter(Objects::nonNull).collect(Collectors.toList());
|
||||
referencedTxIds.addAll(txos.stream().map(BlockTransactionHash::getHash).collect(Collectors.toSet()));
|
||||
|
||||
walletNodeDao.deleteNodeTxosNotInList(addressNode, existingIds.isEmpty() ? List.of(-1L) : existingIds);
|
||||
for(BlockTransactionHashIndex txo : addressNode.getTransactionOutputs()) {
|
||||
walletNodeDao.addOrUpdate(addressNode, txo);
|
||||
}
|
||||
}
|
||||
for(Sha256Hash txid : referencedTxIds) {
|
||||
BlockTransaction blkTx = wallet.getTransactions().get(txid);
|
||||
blockTransactionDao.addOrUpdate(wallet, txid, blkTx);
|
||||
}
|
||||
}
|
||||
|
||||
if(dirtyPersistables.blockHeight != null) {
|
||||
walletDao.updateStoredBlockHeight(wallet.getId(), dirtyPersistables.blockHeight);
|
||||
}
|
||||
|
||||
if(!dirtyPersistables.labelEntries.isEmpty()) {
|
||||
BlockTransactionDao blockTransactionDao = handle.attach(BlockTransactionDao.class);
|
||||
WalletNodeDao walletNodeDao = handle.attach(WalletNodeDao.class);
|
||||
for(Entry entry : dirtyPersistables.labelEntries) {
|
||||
if(entry instanceof TransactionEntry && ((TransactionEntry)entry).getBlockTransaction().getId() != null) {
|
||||
blockTransactionDao.updateLabel(((TransactionEntry)entry).getBlockTransaction().getId(), entry.getLabel());
|
||||
} else if(entry instanceof NodeEntry && ((NodeEntry)entry).getNode().getId() != null) {
|
||||
walletNodeDao.updateNodeLabel(((NodeEntry)entry).getNode().getId(), entry.getLabel());
|
||||
} else if(entry instanceof HashIndexEntry && ((HashIndexEntry)entry).getHashIndex().getId() != null) {
|
||||
walletNodeDao.updateTxoLabel(((HashIndexEntry)entry).getHashIndex().getId(), entry.getLabel());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(!dirtyPersistables.utxoStatuses.isEmpty()) {
|
||||
WalletNodeDao walletNodeDao = handle.attach(WalletNodeDao.class);
|
||||
for(BlockTransactionHashIndex utxo : dirtyPersistables.utxoStatuses) {
|
||||
walletNodeDao.updateTxoStatus(utxo.getId(), utxo.getStatus() == null ? null : utxo.getStatus().ordinal());
|
||||
}
|
||||
}
|
||||
|
||||
if(!dirtyPersistables.labelKeystores.isEmpty()) {
|
||||
KeystoreDao keystoreDao = handle.attach(KeystoreDao.class);
|
||||
for(Keystore keystore : dirtyPersistables.labelKeystores) {
|
||||
keystoreDao.updateLabel(keystore.getLabel(), keystore.getId());
|
||||
}
|
||||
}
|
||||
|
||||
dirtyPersistablesMap.remove(wallet);
|
||||
} finally {
|
||||
walletDao.setSchema(DEFAULT_SCHEMA);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void cleanAndAddWallet(Storage storage, Wallet wallet, String password) throws StorageException {
|
||||
String schema = getSchema(wallet);
|
||||
cleanAndMigrate(storage, schema, password);
|
||||
|
||||
Jdbi jdbi = getJdbi(storage, password);
|
||||
jdbi.useHandle(handle -> {
|
||||
WalletDao walletDao = handle.attach(WalletDao.class);
|
||||
walletDao.addWallet(schema, wallet);
|
||||
});
|
||||
|
||||
if(wallet.isMasterWallet()) {
|
||||
masterWallet = wallet;
|
||||
}
|
||||
}
|
||||
|
||||
private void migrate(Storage storage, String schema, ECKey encryptionKey) throws StorageException {
|
||||
try {
|
||||
Flyway flyway = getFlyway(storage, schema, getFilePassword(encryptionKey));
|
||||
flyway.migrate();
|
||||
} catch(FlywayValidateException e) {
|
||||
log.error("Failed to open wallet file. Validation error during schema migration.", e);
|
||||
throw new StorageException("Failed to open wallet file. Validation error during schema migration.", e);
|
||||
} catch(FlywayException e) {
|
||||
log.error("Failed to open wallet file. ", e);
|
||||
throw new StorageException("Failed to open wallet file.\n" + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void cleanAndMigrate(Storage storage, String schema, String password) throws StorageException {
|
||||
try {
|
||||
Flyway flyway = getFlyway(storage, schema, password);
|
||||
flyway.clean();
|
||||
flyway.migrate();
|
||||
} catch(FlywayException e) {
|
||||
log.error("Failed to save wallet file.", e);
|
||||
throw new StorageException("Failed to save wallet file.\n" + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private String getSchema(Wallet wallet) {
|
||||
return wallet.isMasterWallet() ? MASTER_SCHEMA : WALLET_SCHEMA_PREFIX + wallet.getName();
|
||||
}
|
||||
|
||||
private String getFilePassword(ECKey encryptionKey) {
|
||||
if(encryptionKey == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Utils.bytesToHex(encryptionKey.getPubKey());
|
||||
}
|
||||
|
||||
private void writeBinaryHeader(File walletFile) throws IOException {
|
||||
ByteBuffer header = ByteBuffer.allocate(HEADER_MAGIC_1.length + SALT_LENGTH_BYTES);
|
||||
header.put(HEADER_MAGIC_1);
|
||||
header.put(keyDeriver.getSalt());
|
||||
header.flip();
|
||||
|
||||
try(FileChannel fileChannel = new RandomAccessFile(walletFile, "rwd").getChannel()) {
|
||||
fileChannel.position(H2_ENCRYPT_HEADER.length + H2_ENCRYPT_SALT_LENGTH_BYTES);
|
||||
fileChannel.write(header);
|
||||
}
|
||||
}
|
||||
|
||||
private void updatePassword(Storage storage, ECKey encryptionPubKey) {
|
||||
String newPassword = getFilePassword(encryptionPubKey);
|
||||
String currentPassword = getDatasourcePassword();
|
||||
|
||||
//The password only needs to be changed if the datasource is null - either we have loaded the wallet from a datasource, or it is a new wallet and the datasource is still to be created
|
||||
if(dataSource != null && !Objects.equals(currentPassword, newPassword)) {
|
||||
if(!dataSource.isClosed()) {
|
||||
dataSource.close();
|
||||
}
|
||||
|
||||
try {
|
||||
File walletFile = storage.getWalletFile();
|
||||
ChangeFileEncryption.execute(walletFile.getParent(), getWalletName(walletFile, null), "AES",
|
||||
currentPassword == null ? null : currentPassword.toCharArray(),
|
||||
newPassword == null ? null : newPassword.toCharArray(), true);
|
||||
|
||||
if(newPassword != null) {
|
||||
writeBinaryHeader(walletFile);
|
||||
}
|
||||
|
||||
//This sets the new password on the datasource for the next updatePassword check
|
||||
getDataSource(storage, newPassword);
|
||||
} catch(Exception e) {
|
||||
log.error("Error changing database password", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String getDatasourcePassword() {
|
||||
if(dataSource != null) {
|
||||
String dsPassword = dataSource.getPassword();
|
||||
if(dsPassword.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return dsPassword.substring(0, dsPassword.length() - (" " + H2_PASSWORD).length());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ECKey getEncryptionKey(CharSequence password) throws IOException {
|
||||
return getEncryptionKey(password, null, null);
|
||||
}
|
||||
|
||||
private ECKey getEncryptionKey(CharSequence password, File walletFile, ECKey alreadyDerivedKey) throws IOException {
|
||||
if(password != null && password.equals("")) {
|
||||
return Storage.NO_PASSWORD_KEY;
|
||||
}
|
||||
|
||||
AsymmetricKeyDeriver keyDeriver = getKeyDeriver(walletFile);
|
||||
if(alreadyDerivedKey != null) {
|
||||
return alreadyDerivedKey;
|
||||
}
|
||||
|
||||
return password == null ? null : keyDeriver.deriveECKey(password);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AsymmetricKeyDeriver getKeyDeriver() {
|
||||
return keyDeriver;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setKeyDeriver(AsymmetricKeyDeriver keyDeriver) {
|
||||
this.keyDeriver = keyDeriver;
|
||||
}
|
||||
|
||||
private AsymmetricKeyDeriver getKeyDeriver(File walletFile) throws IOException {
|
||||
if(keyDeriver == null) {
|
||||
keyDeriver = getWalletKeyDeriver(walletFile);
|
||||
}
|
||||
|
||||
return keyDeriver;
|
||||
}
|
||||
|
||||
private AsymmetricKeyDeriver getWalletKeyDeriver(File walletFile) throws IOException {
|
||||
if(keyDeriver == null) {
|
||||
byte[] salt = new byte[SALT_LENGTH_BYTES];
|
||||
|
||||
if(walletFile != null && walletFile.exists()) {
|
||||
try(InputStream inputStream = new FileInputStream(walletFile)) {
|
||||
inputStream.skip(H2_ENCRYPT_HEADER.length + H2_ENCRYPT_SALT_LENGTH_BYTES + HEADER_MAGIC_1.length);
|
||||
inputStream.read(salt, 0, salt.length);
|
||||
}
|
||||
} else {
|
||||
SecureRandom secureRandom = new SecureRandom();
|
||||
secureRandom.nextBytes(salt);
|
||||
}
|
||||
|
||||
return new Argon2KeyDeriver(salt);
|
||||
}
|
||||
|
||||
return keyDeriver;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEncrypted(File walletFile) throws IOException {
|
||||
byte[] header = new byte[H2_ENCRYPT_HEADER.length];
|
||||
try(InputStream inputStream = new FileInputStream(walletFile)) {
|
||||
inputStream.read(header, 0, H2_ENCRYPT_HEADER.length);
|
||||
return Arrays.equals(H2_ENCRYPT_HEADER, header);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getWalletId(Storage storage, Wallet wallet) {
|
||||
return storage.getWalletFile().getParentFile().getAbsolutePath() + File.separator + getWalletName(storage.getWalletFile(), null) + ":" + (wallet == null || wallet.isMasterWallet() ? "master" : wallet.getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getWalletName(File walletFile, Wallet wallet) {
|
||||
if(wallet != null && wallet.getMasterWallet() != null) {
|
||||
return wallet.getName();
|
||||
}
|
||||
|
||||
String name = walletFile.getName();
|
||||
if(name.endsWith("." + getType().getExtension())) {
|
||||
name = name.substring(0, name.length() - getType().getExtension().length() - 1);
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PersistenceType getType() {
|
||||
return PersistenceType.DB;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void copyWallet(File walletFile, OutputStream outputStream) throws IOException {
|
||||
if(dataSource != null && !dataSource.isClosed()) {
|
||||
dataSource.close();
|
||||
}
|
||||
|
||||
com.google.common.io.Files.copy(walletFile, outputStream);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
EventManager.get().unregister(this);
|
||||
if(dataSource != null && !dataSource.isClosed()) {
|
||||
dataSource.close();
|
||||
}
|
||||
}
|
||||
|
||||
private Jdbi getJdbi(Storage storage, String password) throws StorageException {
|
||||
Jdbi jdbi = Jdbi.create(getDataSource(storage, password));
|
||||
jdbi.installPlugin(new H2DatabasePlugin());
|
||||
jdbi.installPlugin(new SqlObjectPlugin());
|
||||
|
||||
return jdbi;
|
||||
}
|
||||
|
||||
private Flyway getFlyway(Storage storage, String schema, String password) throws StorageException {
|
||||
return Flyway.configure().dataSource(getDataSource(storage, password)).locations("com/sparrowwallet/sparrow/sql").schemas(schema).load();
|
||||
}
|
||||
|
||||
private HikariDataSource getDataSource(Storage storage, String password) throws StorageException {
|
||||
if(dataSource == null || dataSource.isClosed()) {
|
||||
dataSource = createDataSource(storage.getWalletFile(), password);
|
||||
}
|
||||
|
||||
return dataSource;
|
||||
}
|
||||
|
||||
private HikariDataSource createDataSource(File walletFile, String password) throws StorageException {
|
||||
try {
|
||||
HikariConfig config = new HikariConfig();
|
||||
config.setJdbcUrl(getUrl(walletFile, password));
|
||||
config.setUsername(H2_USER);
|
||||
config.setPassword(password == null ? H2_PASSWORD : password + " " + H2_PASSWORD);
|
||||
return new HikariDataSource(config);
|
||||
} catch(HikariPool.PoolInitializationException e) {
|
||||
if(e.getMessage() != null && e.getMessage().contains("Database may be already in use")) {
|
||||
log.error("Wallet file may already be in use. Make sure the application is not running elsewhere.", e);
|
||||
throw new StorageException("Wallet file may already be in use. Make sure the application is not running elsewhere.", e);
|
||||
} else if(e.getMessage() != null && (e.getMessage().contains("Wrong user name or password") || e.getMessage().contains("Encryption error in file"))) {
|
||||
throw new InvalidPasswordException("Incorrect password for wallet file.", e);
|
||||
} else {
|
||||
log.error("Failed to open database file", e);
|
||||
throw new StorageException("Failed to open database file.\n" + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String getUrl(File walletFile, String password) {
|
||||
return "jdbc:h2:" + walletFile.getAbsolutePath().replace("." + getType().getExtension(), "") + ";INIT=SET TRACE_LEVEL_FILE=4;TRACE_LEVEL_FILE=4;DATABASE_TO_UPPER=false" + (password == null ? "" : ";CIPHER=AES");
|
||||
}
|
||||
|
||||
private boolean persistsFor(Wallet wallet) {
|
||||
if(masterWallet != null) {
|
||||
if(wallet == masterWallet) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return masterWallet.getChildWallets().contains(wallet);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletHistoryCleared(WalletHistoryClearedEvent event) {
|
||||
if(persistsFor(event.getWallet())) {
|
||||
dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).clearHistory = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletHistoryChanged(WalletHistoryChangedEvent event) {
|
||||
if(persistsFor(event.getWallet())) {
|
||||
dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).historyNodes.addAll(event.getHistoryChangedNodes());
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletBlockHeightChanged(WalletBlockHeightChangedEvent event) {
|
||||
if(persistsFor(event.getWallet())) {
|
||||
dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).blockHeight = event.getBlockHeight();
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletEntryLabelsChanged(WalletEntryLabelsChangedEvent event) {
|
||||
if(persistsFor(event.getWallet())) {
|
||||
dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).labelEntries.addAll(event.getEntries());
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletUtxoStatusChanged(WalletUtxoStatusChangedEvent event) {
|
||||
if(persistsFor(event.getWallet())) {
|
||||
dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).utxoStatuses.add(event.getUtxo());
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void keystoreLabelsChanged(KeystoreLabelsChangedEvent event) {
|
||||
if(persistsFor(event.getWallet())) {
|
||||
dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).labelKeystores.addAll(event.getChangedKeystores());
|
||||
}
|
||||
}
|
||||
|
||||
private static class DirtyPersistables {
|
||||
public boolean clearHistory;
|
||||
public final List<WalletNode> historyNodes = new ArrayList<>();
|
||||
public Integer blockHeight = null;
|
||||
public final List<Entry> labelEntries = new ArrayList<>();
|
||||
public final List<BlockTransactionHashIndex> utxoStatuses = new ArrayList<>();
|
||||
public final List<Keystore> labelKeystores = new ArrayList<>();
|
||||
|
||||
public String toString() {
|
||||
return "Dirty Persistables" +
|
||||
"\nClear history:" + clearHistory +
|
||||
"\nNodes:" + historyNodes +
|
||||
"\nBlockHeight:" + blockHeight +
|
||||
"\nTx labels:" + labelEntries.stream().filter(entry -> entry instanceof TransactionEntry).map(entry -> ((TransactionEntry)entry).getBlockTransaction().getHash().toString()).collect(Collectors.toList()) +
|
||||
"\nAddress labels:" + labelEntries.stream().filter(entry -> entry instanceof NodeEntry).map(entry -> ((NodeEntry)entry).getNode().toString() + " " + entry.getLabel()).collect(Collectors.toList()) +
|
||||
"\nUTXO labels:" + labelEntries.stream().filter(entry -> entry instanceof HashIndexEntry).map(entry -> ((HashIndexEntry)entry).getHashIndex().toString()).collect(Collectors.toList()) +
|
||||
"\nUTXO statuses:" + utxoStatuses +
|
||||
"\nKeystore labels:" + labelKeystores.stream().map(Keystore::getLabel).collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.sparrowwallet.sparrow.io.db;
|
||||
|
||||
import com.sparrowwallet.drongo.crypto.EncryptedData;
|
||||
import com.sparrowwallet.drongo.wallet.DeterministicSeed;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.MasterPrivateExtendedKey;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import org.jdbi.v3.sqlobject.config.RegisterRowMapper;
|
||||
import org.jdbi.v3.sqlobject.statement.GetGeneratedKeys;
|
||||
import org.jdbi.v3.sqlobject.statement.SqlQuery;
|
||||
import org.jdbi.v3.sqlobject.statement.SqlUpdate;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface KeystoreDao {
|
||||
@SqlQuery("select keystore.id, keystore.label, keystore.source, keystore.walletModel, keystore.masterFingerprint, keystore.derivationPath, keystore.extendedPublicKey, " +
|
||||
"masterPrivateExtendedKey.id, masterPrivateExtendedKey.privateKey, masterPrivateExtendedKey.chainCode, masterPrivateExtendedKey.initialisationVector, masterPrivateExtendedKey.encryptedBytes, masterPrivateExtendedKey.keySalt, masterPrivateExtendedKey.deriver, masterPrivateExtendedKey.crypter, " +
|
||||
"seed.id, seed.type, seed.mnemonicString, seed.initialisationVector, seed.encryptedBytes, seed.keySalt, seed.deriver, seed.crypter, seed.needsPassphrase, seed.creationTimeSeconds " +
|
||||
"from keystore left join masterPrivateExtendedKey on keystore.masterPrivateExtendedKey = masterPrivateExtendedKey.id left join seed on keystore.seed = seed.id where keystore.wallet = ? order by keystore.index asc")
|
||||
@RegisterRowMapper(KeystoreMapper.class)
|
||||
List<Keystore> getForWalletId(Long id);
|
||||
|
||||
@SqlUpdate("insert into keystore (label, source, walletModel, masterFingerprint, derivationPath, extendedPublicKey, masterPrivateExtendedKey, seed, wallet, index) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
|
||||
@GetGeneratedKeys("id")
|
||||
long insert(String label, int source, int walletModel, String masterFingerprint, String derivationPath, String extendedPublicKey, Long masterPrivateExtendedKey, Long seed, long wallet, int index);
|
||||
|
||||
@SqlUpdate("insert into masterPrivateExtendedKey (privateKey, chainCode, initialisationVector, encryptedBytes, keySalt, deriver, crypter, creationTimeSeconds) values (?, ?, ?, ?, ?, ?, ?, ?)")
|
||||
@GetGeneratedKeys("id")
|
||||
long insertMasterPrivateExtendedKey(byte[] privateKey, byte[] chainCode, byte[] initialisationVector, byte[] encryptedBytes, byte[] keySalt, Integer deriver, Integer crypter, long creationTimeSeconds);
|
||||
|
||||
@SqlUpdate("insert into seed (type, mnemonicString, initialisationVector, encryptedBytes, keySalt, deriver, crypter, needsPassphrase, creationTimeSeconds) values (?, ?, ?, ?, ?, ?, ?, ?, ?)")
|
||||
@GetGeneratedKeys("id")
|
||||
long insertSeed(int type, String mnemonicString, byte[] initialisationVector, byte[] encryptedBytes, byte[] keySalt, Integer deriver, Integer crypter, boolean needsPassphrase, long creationTimeSeconds);
|
||||
|
||||
@SqlUpdate("update keystore set label = ? where id = ?")
|
||||
void updateLabel(String label, long id);
|
||||
|
||||
default void addKeystores(Wallet wallet) {
|
||||
for(int i = 0; i < wallet.getKeystores().size(); i++) {
|
||||
Keystore keystore = wallet.getKeystores().get(i);
|
||||
if(keystore.getMasterPrivateExtendedKey() != null) {
|
||||
MasterPrivateExtendedKey mpek = keystore.getMasterPrivateExtendedKey();
|
||||
if(mpek.isEncrypted()) {
|
||||
EncryptedData data = mpek.getEncryptedData();
|
||||
long id = insertMasterPrivateExtendedKey(null, null, data.getInitialisationVector(), data.getEncryptedBytes(), data.getKeySalt(), data.getEncryptionType().getDeriver().ordinal(), data.getEncryptionType().getCrypter().ordinal(), mpek.getCreationTimeSeconds());
|
||||
mpek.setId(id);
|
||||
} else {
|
||||
long id = insertMasterPrivateExtendedKey(mpek.getPrivateKey().getPrivKeyBytes(), mpek.getPrivateKey().getChainCode(), null, null, null, null, null, mpek.getCreationTimeSeconds());
|
||||
mpek.setId(id);
|
||||
}
|
||||
}
|
||||
|
||||
if(keystore.getSeed() != null) {
|
||||
DeterministicSeed seed = keystore.getSeed();
|
||||
if(seed.isEncrypted()) {
|
||||
EncryptedData data = seed.getEncryptedData();
|
||||
long id = insertSeed(seed.getType().ordinal(), null, data.getInitialisationVector(), data.getEncryptedBytes(), data.getKeySalt(), data.getEncryptionType().getDeriver().ordinal(), data.getEncryptionType().getCrypter().ordinal(), seed.needsPassphrase(), seed.getCreationTimeSeconds());
|
||||
seed.setId(id);
|
||||
} else {
|
||||
long id = insertSeed(seed.getType().ordinal(), seed.getMnemonicString().asString(), null, null, null, null, null, seed.needsPassphrase(), seed.getCreationTimeSeconds());
|
||||
seed.setId(id);
|
||||
}
|
||||
}
|
||||
|
||||
long id = insert(keystore.getLabel(), keystore.getSource().ordinal(), keystore.getWalletModel().ordinal(),
|
||||
keystore.hasPrivateKey() ? null : keystore.getKeyDerivation().getMasterFingerprint(),
|
||||
keystore.getKeyDerivation().getDerivationPath(),
|
||||
keystore.hasPrivateKey() ? null : keystore.getExtendedPublicKey().toString(),
|
||||
keystore.getMasterPrivateExtendedKey() == null ? null : keystore.getMasterPrivateExtendedKey().getId(),
|
||||
keystore.getSeed() == null ? null : keystore.getSeed().getId(), wallet.getId(), i);
|
||||
keystore.setId(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.sparrowwallet.sparrow.io.db;
|
||||
|
||||
import com.sparrowwallet.drongo.ExtendedKey;
|
||||
import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.crypto.EncryptedData;
|
||||
import com.sparrowwallet.drongo.crypto.EncryptionType;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import org.jdbi.v3.core.mapper.RowMapper;
|
||||
import org.jdbi.v3.core.statement.StatementContext;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public class KeystoreMapper implements RowMapper<Keystore> {
|
||||
|
||||
@Override
|
||||
public Keystore map(ResultSet rs, StatementContext ctx) throws SQLException {
|
||||
Keystore keystore = new Keystore(rs.getString("keystore.label"));
|
||||
keystore.setId(rs.getLong("keystore.id"));
|
||||
keystore.setSource(KeystoreSource.values()[rs.getInt("keystore.source")]);
|
||||
keystore.setWalletModel(WalletModel.values()[rs.getInt("keystore.walletModel")]);
|
||||
keystore.setKeyDerivation(new KeyDerivation(rs.getString("keystore.masterFingerprint"), rs.getString("keystore.derivationPath")));
|
||||
keystore.setExtendedPublicKey(rs.getString("keystore.extendedPublicKey") == null ? null : ExtendedKey.fromDescriptor(rs.getString("keystore.extendedPublicKey")));
|
||||
|
||||
if(rs.getBytes("masterPrivateExtendedKey.privateKey") != null) {
|
||||
MasterPrivateExtendedKey masterPrivateExtendedKey = new MasterPrivateExtendedKey(rs.getBytes("masterPrivateExtendedKey.privateKey"), rs.getBytes("masterPrivateExtendedKey.chainCode"));
|
||||
masterPrivateExtendedKey.setId(rs.getLong("masterPrivateExtendedKey.id"));
|
||||
keystore.setMasterPrivateExtendedKey(masterPrivateExtendedKey);
|
||||
} else if(rs.getBytes("masterPrivateExtendedKey.encryptedBytes") != null) {
|
||||
EncryptedData encryptedData = new EncryptedData(rs.getBytes("masterPrivateExtendedKey.initialisationVector"),
|
||||
rs.getBytes("masterPrivateExtendedKey.encryptedBytes"), rs.getBytes("masterPrivateExtendedKey.keySalt"),
|
||||
EncryptionType.Deriver.values()[rs.getInt("masterPrivateExtendedKey.deriver")],
|
||||
EncryptionType.Crypter.values()[rs.getInt("masterPrivateExtendedKey.crypter")]);
|
||||
MasterPrivateExtendedKey masterPrivateExtendedKey = new MasterPrivateExtendedKey(encryptedData);
|
||||
masterPrivateExtendedKey.setId(rs.getLong("masterPrivateExtendedKey.id"));
|
||||
keystore.setMasterPrivateExtendedKey(masterPrivateExtendedKey);
|
||||
}
|
||||
|
||||
if(rs.getString("seed.mnemonicString") != null) {
|
||||
List<String> mnemonicCode = Arrays.asList(rs.getString("seed.mnemonicString").split(" "));
|
||||
DeterministicSeed seed = new DeterministicSeed(mnemonicCode, rs.getBoolean("seed.needsPassphrase"), rs.getLong("seed.creationTimeSeconds"), DeterministicSeed.Type.values()[rs.getInt("seed.type")]);
|
||||
seed.setId(rs.getLong("seed.id"));
|
||||
keystore.setSeed(seed);
|
||||
} else if(rs.getBytes("seed.encryptedBytes") != null) {
|
||||
EncryptedData encryptedData = new EncryptedData(rs.getBytes("seed.initialisationVector"),
|
||||
rs.getBytes("seed.encryptedBytes"), rs.getBytes("seed.keySalt"),
|
||||
EncryptionType.Deriver.values()[rs.getInt("seed.deriver")],
|
||||
EncryptionType.Crypter.values()[rs.getInt("seed.crypter")]);
|
||||
DeterministicSeed seed = new DeterministicSeed(encryptedData, rs.getBoolean("seed.needsPassphrase"), rs.getLong("seed.creationTimeSeconds"), DeterministicSeed.Type.values()[rs.getInt("seed.type")]);
|
||||
seed.setId(rs.getLong("seed.id"));
|
||||
keystore.setSeed(seed);
|
||||
}
|
||||
|
||||
return keystore;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.sparrowwallet.sparrow.io.db;
|
||||
|
||||
import com.sparrowwallet.drongo.policy.Policy;
|
||||
import org.jdbi.v3.sqlobject.config.RegisterRowMapper;
|
||||
import org.jdbi.v3.sqlobject.statement.GetGeneratedKeys;
|
||||
import org.jdbi.v3.sqlobject.statement.SqlQuery;
|
||||
import org.jdbi.v3.sqlobject.statement.SqlUpdate;
|
||||
|
||||
public interface PolicyDao {
|
||||
@SqlQuery("select * from policy where id = ?")
|
||||
@RegisterRowMapper(PolicyMapper.class)
|
||||
Policy getPolicy(long id);
|
||||
|
||||
@SqlUpdate("insert into policy (name, script) values (?, ?)")
|
||||
@GetGeneratedKeys("id")
|
||||
long insert(String name, String script);
|
||||
|
||||
default void addPolicy(Policy policy) {
|
||||
long id = insert(policy.getName(), policy.getMiniscript().getScript());
|
||||
policy.setId(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.sparrowwallet.sparrow.io.db;
|
||||
|
||||
import com.sparrowwallet.drongo.policy.Miniscript;
|
||||
import com.sparrowwallet.drongo.policy.Policy;
|
||||
import org.jdbi.v3.core.mapper.RowMapper;
|
||||
import org.jdbi.v3.core.statement.StatementContext;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
|
||||
public class PolicyMapper implements RowMapper<Policy> {
|
||||
@Override
|
||||
public Policy map(ResultSet rs, StatementContext ctx) throws SQLException {
|
||||
Policy policy = new Policy(rs.getString("name"), new Miniscript(rs.getString("script")));
|
||||
policy.setId(rs.getLong("id"));
|
||||
return policy;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package com.sparrowwallet.sparrow.io.db;
|
||||
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransaction;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletNode;
|
||||
import org.jdbi.v3.sqlobject.CreateSqlObject;
|
||||
import org.jdbi.v3.sqlobject.config.RegisterRowMapper;
|
||||
import org.jdbi.v3.sqlobject.customizer.Bind;
|
||||
import org.jdbi.v3.sqlobject.statement.GetGeneratedKeys;
|
||||
import org.jdbi.v3.sqlobject.statement.SqlQuery;
|
||||
import org.jdbi.v3.sqlobject.statement.SqlUpdate;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public interface WalletDao {
|
||||
@CreateSqlObject
|
||||
PolicyDao createPolicyDao();
|
||||
|
||||
@CreateSqlObject
|
||||
KeystoreDao createKeystoreDao();
|
||||
|
||||
@CreateSqlObject
|
||||
WalletNodeDao createWalletNodeDao();
|
||||
|
||||
@CreateSqlObject
|
||||
BlockTransactionDao createBlockTransactionDao();
|
||||
|
||||
@SqlQuery("select wallet.id, wallet.name, wallet.network, wallet.policyType, wallet.scriptType, wallet.storedBlockHeight, wallet.gapLimit, wallet.birthDate, policy.id, policy.name, policy.script from wallet left join policy on wallet.defaultPolicy = policy.id")
|
||||
@RegisterRowMapper(WalletMapper.class)
|
||||
List<Wallet> loadAllWallets();
|
||||
|
||||
@SqlQuery("select wallet.id, wallet.name, wallet.network, wallet.policyType, wallet.scriptType, wallet.storedBlockHeight, wallet.gapLimit, wallet.birthDate, policy.id, policy.name, policy.script from wallet left join policy on wallet.defaultPolicy = policy.id where wallet.id = 1")
|
||||
@RegisterRowMapper(WalletMapper.class)
|
||||
Wallet loadMainWallet();
|
||||
|
||||
@SqlQuery("select wallet.id, wallet.name, wallet.network, wallet.policyType, wallet.scriptType, wallet.storedBlockHeight, wallet.gapLimit, wallet.birthDate, policy.id, policy.name, policy.script from wallet left join policy on wallet.defaultPolicy = policy.id where wallet.id != 1")
|
||||
@RegisterRowMapper(WalletMapper.class)
|
||||
List<Wallet> loadChildWallets();
|
||||
|
||||
@SqlUpdate("insert into wallet (name, network, policyType, scriptType, storedBlockHeight, gapLimit, birthDate, defaultPolicy) values (?, ?, ?, ?, ?, ?, ?, ?)")
|
||||
@GetGeneratedKeys("id")
|
||||
long insert(String name, int network, int policyType, int scriptType, Integer storedBlockHeight, Integer gapLimit, Date birthDate, long defaultPolicy);
|
||||
|
||||
@SqlUpdate("update wallet set storedBlockHeight = :blockHeight where id = :id")
|
||||
void updateStoredBlockHeight(@Bind("id") long id, @Bind("blockHeight") Integer blockHeight);
|
||||
|
||||
@SqlUpdate("set schema ?")
|
||||
int setSchema(String schema);
|
||||
|
||||
default Wallet getMainWallet(String schema) {
|
||||
try {
|
||||
setSchema(schema);
|
||||
Wallet mainWallet = loadMainWallet();
|
||||
if(mainWallet != null) {
|
||||
loadWallet(mainWallet);
|
||||
}
|
||||
|
||||
return mainWallet;
|
||||
} finally {
|
||||
setSchema(DbPersistence.DEFAULT_SCHEMA);
|
||||
}
|
||||
}
|
||||
|
||||
default List<Wallet> getChildWallets(String schema) {
|
||||
try {
|
||||
List<Wallet> childWallets = loadChildWallets();
|
||||
for(Wallet childWallet : childWallets) {
|
||||
loadWallet(childWallet);
|
||||
}
|
||||
|
||||
return childWallets;
|
||||
} finally {
|
||||
setSchema(DbPersistence.DEFAULT_SCHEMA);
|
||||
}
|
||||
}
|
||||
|
||||
default void loadWallet(Wallet wallet) {
|
||||
wallet.getKeystores().addAll(createKeystoreDao().getForWalletId(wallet.getId()));
|
||||
|
||||
List<WalletNode> walletNodes = createWalletNodeDao().getForWalletId(wallet.getId());
|
||||
wallet.getPurposeNodes().addAll(walletNodes.stream().filter(walletNode -> walletNode.getDerivation().size() == 1).collect(Collectors.toList()));
|
||||
|
||||
Map<Sha256Hash, BlockTransaction> blockTransactions = createBlockTransactionDao().getForWalletId(wallet.getId()); //.stream().collect(Collectors.toMap(BlockTransaction::getHash, Function.identity(), (existing, replacement) -> existing, LinkedHashMap::new));
|
||||
wallet.updateTransactions(blockTransactions);
|
||||
}
|
||||
|
||||
default void addWallet(String schema, Wallet wallet) {
|
||||
try {
|
||||
setSchema(schema);
|
||||
createPolicyDao().addPolicy(wallet.getDefaultPolicy());
|
||||
|
||||
long id = insert(wallet.getName(), wallet.getNetwork().ordinal(), wallet.getPolicyType().ordinal(), wallet.getScriptType().ordinal(), wallet.getStoredBlockHeight(), wallet.gapLimit(), wallet.getBirthDate(), wallet.getDefaultPolicy().getId());
|
||||
wallet.setId(id);
|
||||
|
||||
createKeystoreDao().addKeystores(wallet);
|
||||
createWalletNodeDao().addWalletNodes(wallet);
|
||||
createBlockTransactionDao().addBlockTransactions(wallet);
|
||||
} finally {
|
||||
setSchema(DbPersistence.DEFAULT_SCHEMA);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.sparrowwallet.sparrow.io.db;
|
||||
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.drongo.policy.Miniscript;
|
||||
import com.sparrowwallet.drongo.policy.Policy;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import org.jdbi.v3.core.mapper.RowMapper;
|
||||
import org.jdbi.v3.core.statement.StatementContext;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
|
||||
public class WalletMapper implements RowMapper<Wallet> {
|
||||
@Override
|
||||
public Wallet map(ResultSet rs, StatementContext ctx) throws SQLException {
|
||||
Wallet wallet = new Wallet(rs.getString("wallet.name"));
|
||||
wallet.setId(rs.getLong("wallet.id"));
|
||||
wallet.setNetwork(Network.values()[rs.getInt("wallet.network")]);
|
||||
wallet.setPolicyType(PolicyType.values()[rs.getInt("wallet.policyType")]);
|
||||
wallet.setScriptType(ScriptType.values()[rs.getInt("wallet.scriptType")]);
|
||||
|
||||
Policy policy = new Policy(rs.getString("policy.name"), new Miniscript(rs.getString("policy.script")));
|
||||
policy.setId(rs.getLong("policy.id"));
|
||||
wallet.setDefaultPolicy(policy);
|
||||
|
||||
int storedBlockHeight = rs.getInt("wallet.storedBlockHeight");
|
||||
wallet.setStoredBlockHeight(rs.wasNull() ? null : storedBlockHeight);
|
||||
|
||||
int gapLimit = rs.getInt("wallet.gapLimit");
|
||||
wallet.gapLimit(rs.wasNull() ? null : gapLimit);
|
||||
wallet.setBirthDate(rs.getTimestamp("wallet.birthDate"));
|
||||
|
||||
return wallet;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package com.sparrowwallet.sparrow.io.db;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletNode;
|
||||
import org.jdbi.v3.sqlobject.config.RegisterRowMapper;
|
||||
import org.jdbi.v3.sqlobject.customizer.Bind;
|
||||
import org.jdbi.v3.sqlobject.customizer.BindList;
|
||||
import org.jdbi.v3.sqlobject.statement.GetGeneratedKeys;
|
||||
import org.jdbi.v3.sqlobject.statement.SqlQuery;
|
||||
import org.jdbi.v3.sqlobject.statement.SqlUpdate;
|
||||
import org.jdbi.v3.sqlobject.statement.UseRowReducer;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
public interface WalletNodeDao {
|
||||
@SqlQuery("select walletNode.id, walletNode.derivationPath, walletNode.label, walletNode.parent, " +
|
||||
"blockTransactionHashIndex.id, blockTransactionHashIndex.hash, blockTransactionHashIndex.height, blockTransactionHashIndex.date, blockTransactionHashIndex.fee, blockTransactionHashIndex.label, " +
|
||||
"blockTransactionHashIndex.index, blockTransactionHashIndex.value, blockTransactionHashIndex.status, blockTransactionHashIndex.spentBy, blockTransactionHashIndex.node " +
|
||||
"from walletNode left join blockTransactionHashIndex on walletNode.id = blockTransactionHashIndex.node where walletNode.wallet = ? order by walletNode.parent asc nulls first, blockTransactionHashIndex.spentBy asc nulls first")
|
||||
@RegisterRowMapper(WalletNodeMapper.class)
|
||||
@RegisterRowMapper(BlockTransactionHashIndexMapper.class)
|
||||
@UseRowReducer(WalletNodeReducer.class)
|
||||
List<WalletNode> getForWalletId(Long id);
|
||||
|
||||
@SqlUpdate("insert into walletNode (derivationPath, label, wallet, parent) values (?, ?, ?, ?)")
|
||||
@GetGeneratedKeys("id")
|
||||
long insertWalletNode(String derivationPath, String label, long wallet, Long parent);
|
||||
|
||||
@SqlUpdate("insert into blockTransactionHashIndex (hash, height, date, fee, label, index, value, status, spentBy, node) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
|
||||
@GetGeneratedKeys("id")
|
||||
long insertBlockTransactionHashIndex(byte[] hash, int height, Date date, Long fee, String label, long index, long value, Integer status, Long spentBy, long node);
|
||||
|
||||
@SqlUpdate("update blockTransactionHashIndex set hash = ?, height = ?, date = ?, fee = ?, label = ?, index = ?, value = ?, status = ?, spentBy = ?, node = ? where id = ?")
|
||||
void updateBlockTransactionHashIndex(byte[] hash, int height, Date date, Long fee, String label, long index, long value, Integer status, Long spentBy, long node, long id);
|
||||
|
||||
@SqlUpdate("update walletNode set label = :label where id = :id")
|
||||
void updateNodeLabel(@Bind("id") long id, @Bind("label") String label);
|
||||
|
||||
@SqlUpdate("update blockTransactionHashIndex set label = :label where id = :id")
|
||||
void updateTxoLabel(@Bind("id") long id, @Bind("label") String label);
|
||||
|
||||
@SqlUpdate("update blockTransactionHashIndex set status = :status where id = :id")
|
||||
void updateTxoStatus(@Bind("id") long id, @Bind("status") Integer status);
|
||||
|
||||
@SqlUpdate("delete from blockTransactionHashIndex where blockTransactionHashIndex.node in (select walletNode.id from walletNode where walletNode.wallet = ?)")
|
||||
void clearHistory(long wallet);
|
||||
|
||||
@SqlUpdate("delete from blockTransactionHashIndex where blockTransactionHashIndex.node in (select walletNode.id from walletNode where walletNode.wallet = ?) and blockTransactionHashIndex.spentBy is not null")
|
||||
void clearSpentHistory(long wallet);
|
||||
|
||||
@SqlUpdate("delete from blockTransactionHashIndex where node = :nodeId and id not in (<ids>)")
|
||||
void deleteUnreferencedNodeTxos(@Bind("nodeId") Long nodeId, @BindList("ids") List<Long> ids);
|
||||
|
||||
@SqlUpdate("delete from blockTransactionHashIndex where node = :nodeId and id not in (<ids>) and spentBy is not null")
|
||||
void deleteUnreferencedNodeSpentTxos(@Bind("nodeId") Long nodeId, @BindList("ids") List<Long> ids);
|
||||
|
||||
default void addWalletNodes(Wallet wallet) {
|
||||
for(WalletNode purposeNode : wallet.getPurposeNodes()) {
|
||||
long purposeNodeId = insertWalletNode(purposeNode.getDerivationPath(), purposeNode.getLabel(), wallet.getId(), null);
|
||||
purposeNode.setId(purposeNodeId);
|
||||
for(WalletNode addressNode : purposeNode.getChildren()) {
|
||||
long addressNodeId = insertWalletNode(addressNode.getDerivationPath(), addressNode.getLabel(), wallet.getId(), purposeNodeId);
|
||||
addressNode.setId(addressNodeId);
|
||||
addTransactionOutputs(addressNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
default void addTransactionOutputs(WalletNode addressNode) {
|
||||
for(BlockTransactionHashIndex txo : addressNode.getTransactionOutputs()) {
|
||||
txo.setId(null);
|
||||
if(txo.isSpent()) {
|
||||
txo.getSpentBy().setId(null);
|
||||
}
|
||||
|
||||
addOrUpdate(addressNode, txo);
|
||||
}
|
||||
}
|
||||
|
||||
default void addOrUpdate(WalletNode addressNode, BlockTransactionHashIndex txo) {
|
||||
Long spentById = null;
|
||||
if(txo.isSpent()) {
|
||||
BlockTransactionHashIndex spentBy = txo.getSpentBy();
|
||||
if(spentBy.getId() == null) {
|
||||
spentById = insertBlockTransactionHashIndex(spentBy.getHash().getBytes(), spentBy.getHeight(), spentBy.getDate(), spentBy.getFee(), spentBy.getLabel(), spentBy.getIndex(), spentBy.getValue(),
|
||||
spentBy.getStatus() == null ? null : spentBy.getStatus().ordinal(), null, addressNode.getId());
|
||||
spentBy.setId(spentById);
|
||||
} else {
|
||||
updateBlockTransactionHashIndex(spentBy.getHash().getBytes(), spentBy.getHeight(), spentBy.getDate(), spentBy.getFee(), spentBy.getLabel(), spentBy.getIndex(), spentBy.getValue(),
|
||||
spentBy.getStatus() == null ? null : spentBy.getStatus().ordinal(), null, addressNode.getId(), spentBy.getId());
|
||||
spentById = spentBy.getId();
|
||||
}
|
||||
}
|
||||
|
||||
if(txo.getId() == null) {
|
||||
long txoId = insertBlockTransactionHashIndex(txo.getHash().getBytes(), txo.getHeight(), txo.getDate(), txo.getFee(), txo.getLabel(), txo.getIndex(), txo.getValue(),
|
||||
txo.getStatus() == null ? null : txo.getStatus().ordinal(), spentById, addressNode.getId());
|
||||
txo.setId(txoId);
|
||||
} else {
|
||||
updateBlockTransactionHashIndex(txo.getHash().getBytes(), txo.getHeight(), txo.getDate(), txo.getFee(), txo.getLabel(), txo.getIndex(), txo.getValue(),
|
||||
txo.getStatus() == null ? null : txo.getStatus().ordinal(), spentById, addressNode.getId(), txo.getId());
|
||||
}
|
||||
}
|
||||
|
||||
default void deleteNodeTxosNotInList(WalletNode addressNode, List<Long> txoIds) {
|
||||
deleteUnreferencedNodeSpentTxos(addressNode.getId(), txoIds);
|
||||
deleteUnreferencedNodeTxos(addressNode.getId(), txoIds);
|
||||
}
|
||||
|
||||
default void clearHistory(Wallet wallet) {
|
||||
clearSpentHistory(wallet.getId());
|
||||
clearHistory(wallet.getId());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.sparrowwallet.sparrow.io.db;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.WalletNode;
|
||||
import org.jdbi.v3.core.mapper.RowMapper;
|
||||
import org.jdbi.v3.core.statement.StatementContext;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
|
||||
public class WalletNodeMapper implements RowMapper<WalletNode> {
|
||||
@Override
|
||||
public WalletNode map(ResultSet rs, StatementContext ctx) throws SQLException {
|
||||
WalletNode walletNode = new WalletNode(rs.getString("walletNode.derivationPath"));
|
||||
walletNode.setId(rs.getLong("walletNode.id"));
|
||||
walletNode.setLabel(rs.getString("walletNode.label"));
|
||||
|
||||
return walletNode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.sparrowwallet.sparrow.io.db;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
|
||||
import com.sparrowwallet.drongo.wallet.WalletNode;
|
||||
import org.jdbi.v3.core.result.LinkedHashMapRowReducer;
|
||||
import org.jdbi.v3.core.result.RowView;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class WalletNodeReducer implements LinkedHashMapRowReducer<Long, WalletNode> {
|
||||
@Override
|
||||
public void accumulate(Map<Long, WalletNode> map, RowView rowView) {
|
||||
WalletNode walletNode = map.computeIfAbsent(rowView.getColumn("walletNode.id", Long.class), id -> rowView.getRow(WalletNode.class));
|
||||
|
||||
if(rowView.getColumn("walletNode.parent", Long.class) != null) {
|
||||
WalletNode parentNode = map.get(rowView.getColumn("walletNode.parent", Long.class));
|
||||
parentNode.getChildren().add(walletNode);
|
||||
}
|
||||
|
||||
if(rowView.getColumn("blockTransactionHashIndex.node", Long.class) != null) {
|
||||
BlockTransactionHashIndex blockTransactionHashIndex = rowView.getRow(BlockTransactionHashIndex.class);
|
||||
if(rowView.getColumn("blockTransactionHashIndex.spentBy", Long.class) != null) {
|
||||
BlockTransactionHashIndex spentBy = walletNode.getTransactionOutputs().stream().filter(ref -> ref.getId().equals(rowView.getColumn("blockTransactionHashIndex.spentBy", Long.class))).findFirst().orElseThrow();
|
||||
blockTransactionHashIndex.setSpentBy(spentBy);
|
||||
walletNode.getTransactionOutputs().remove(spentBy);
|
||||
}
|
||||
walletNode.getTransactionOutputs().add(blockTransactionHashIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -269,7 +269,7 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
|
||||
try {
|
||||
JsonRpcClient client = new JsonRpcClient(transport);
|
||||
return new RetryLogic<String>(MAX_RETRIES, RETRY_DELAY, IllegalStateException.class).getResult(() ->
|
||||
client.createRequest().returnAs(String.class).method("blockchain.transaction.broadcast").id(idCounter.incrementAndGet()).param("raw_tx", txHex).execute());
|
||||
client.createRequest().returnAs(String.class).method("blockchain.transaction.broadcast").id(idCounter.incrementAndGet()).params(txHex).execute());
|
||||
} catch(JsonRpcException e) {
|
||||
throw new ElectrumServerRpcException(e.getErrorMessage().getMessage(), e);
|
||||
} catch(Exception e) {
|
||||
|
||||
@@ -45,7 +45,7 @@ public class ElectrumServer {
|
||||
|
||||
private static Transport transport;
|
||||
|
||||
private static final Map<String, Set<String>> subscribedScriptHashes = Collections.synchronizedMap(new HashMap<>());
|
||||
private static final Map<String, List<String>> subscribedScriptHashes = Collections.synchronizedMap(new HashMap<>());
|
||||
|
||||
private static String previousServerAddress;
|
||||
|
||||
@@ -822,21 +822,21 @@ public class ElectrumServer {
|
||||
return Utils.bytesToHex(reversed);
|
||||
}
|
||||
|
||||
public static Map<String, Set<String>> getSubscribedScriptHashes() {
|
||||
public static Map<String, List<String>> getSubscribedScriptHashes() {
|
||||
return subscribedScriptHashes;
|
||||
}
|
||||
|
||||
public static String getSubscribedScriptHashStatus(String scriptHash) {
|
||||
Set<String> existingStatuses = subscribedScriptHashes.get(scriptHash);
|
||||
if(existingStatuses != null) {
|
||||
return Iterables.getLast(existingStatuses);
|
||||
List<String> existingStatuses = subscribedScriptHashes.get(scriptHash);
|
||||
if(existingStatuses != null && !existingStatuses.isEmpty()) {
|
||||
return existingStatuses.get(existingStatuses.size() - 1);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void updateSubscribedScriptHashStatus(String scriptHash, String status) {
|
||||
Set<String> existingStatuses = subscribedScriptHashes.computeIfAbsent(scriptHash, k -> new LinkedHashSet<>());
|
||||
List<String> existingStatuses = subscribedScriptHashes.computeIfAbsent(scriptHash, k -> new ArrayList<>());
|
||||
existingStatuses.add(status);
|
||||
}
|
||||
|
||||
@@ -1147,11 +1147,22 @@ public class ElectrumServer {
|
||||
electrumServer.calculateNodeHistory(wallet, nodeTransactionMap);
|
||||
|
||||
//Add all of the script hashes we have now fetched the history for so we don't need to fetch again until the script hash status changes
|
||||
for(WalletNode node : nodeTransactionMap.keySet()) {
|
||||
for(WalletNode node : (nodes == null ? nodeTransactionMap.keySet() : nodes)) {
|
||||
String scriptHash = getScriptHash(wallet, node);
|
||||
retrievedScriptHashes.put(scriptHash, getSubscribedScriptHashStatus(scriptHash));
|
||||
}
|
||||
|
||||
//Clear transaction outputs for nodes that have no history - this is useful when a transaction is replaced in the mempool
|
||||
if(nodes != null) {
|
||||
for(WalletNode node : nodes) {
|
||||
String scriptHash = getScriptHash(wallet, node);
|
||||
if(retrievedScriptHashes.get(scriptHash) == null && !node.getTransactionOutputs().isEmpty()) {
|
||||
log.debug("Clearing transaction history for " + node);
|
||||
node.getTransactionOutputs().clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import javafx.application.Platform;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.List;
|
||||
|
||||
@JsonRpcService
|
||||
public class SubscriptionService {
|
||||
@@ -25,16 +25,11 @@ public class SubscriptionService {
|
||||
|
||||
@JsonRpcMethod("blockchain.scripthash.subscribe")
|
||||
public void scriptHashStatusUpdated(@JsonRpcParam("scripthash") final String scriptHash, @JsonRpcOptional @JsonRpcParam("status") final String status) {
|
||||
if(status == null) {
|
||||
//Mempool transaction was replaced returning change/consolidation script hash status to null, ignore this update
|
||||
return;
|
||||
}
|
||||
|
||||
Set<String> existingStatuses = ElectrumServer.getSubscribedScriptHashes().get(scriptHash);
|
||||
List<String> existingStatuses = ElectrumServer.getSubscribedScriptHashes().get(scriptHash);
|
||||
if(existingStatuses == null) {
|
||||
log.debug("Received script hash status update for unsubscribed script hash: " + scriptHash);
|
||||
ElectrumServer.updateSubscribedScriptHashStatus(scriptHash, status);
|
||||
} else if(existingStatuses.contains(status)) {
|
||||
} else if(status != null && existingStatuses.contains(status)) {
|
||||
log.debug("Received script hash status update, but status has not changed");
|
||||
return;
|
||||
} else {
|
||||
|
||||
@@ -709,7 +709,7 @@ public class HeadersController extends TransactionFormController implements Init
|
||||
}
|
||||
|
||||
Wallet copy = headersForm.getSigningWallet().copy();
|
||||
File file = headersForm.getAvailableWallets().get(headersForm.getSigningWallet()).getWalletFile();
|
||||
String walletId = headersForm.getAvailableWallets().get(headersForm.getSigningWallet()).getWalletId(headersForm.getSigningWallet());
|
||||
|
||||
if(copy.isEncrypted()) {
|
||||
WalletPasswordDialog dlg = new WalletPasswordDialog(copy.getName(), WalletPasswordDialog.PasswordRequirement.LOAD);
|
||||
@@ -717,15 +717,15 @@ public class HeadersController extends TransactionFormController implements Init
|
||||
if(password.isPresent()) {
|
||||
Storage.DecryptWalletService decryptWalletService = new Storage.DecryptWalletService(copy, password.get());
|
||||
decryptWalletService.setOnSucceeded(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(file, TimedEvent.Action.END, "Done"));
|
||||
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Done"));
|
||||
Wallet decryptedWallet = decryptWalletService.getValue();
|
||||
signUnencryptedKeystores(decryptedWallet);
|
||||
});
|
||||
decryptWalletService.setOnFailed(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(file, TimedEvent.Action.END, "Failed"));
|
||||
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Failed"));
|
||||
AppServices.showErrorDialog("Incorrect Password", decryptWalletService.getException().getMessage());
|
||||
});
|
||||
EventManager.get().post(new StorageEvent(file, TimedEvent.Action.START, "Decrypting wallet..."));
|
||||
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.START, "Decrypting wallet..."));
|
||||
decryptWalletService.start();
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -26,8 +26,10 @@ public class HashIndexEntry extends Entry implements Comparable<HashIndexEntry>
|
||||
this.keyPurpose = keyPurpose;
|
||||
|
||||
labelProperty().addListener((observable, oldValue, newValue) -> {
|
||||
hashIndex.setLabel(newValue);
|
||||
EventManager.get().post(new WalletEntryLabelsChangedEvent(wallet, this));
|
||||
if(!Objects.equals(hashIndex.getLabel(), newValue)) {
|
||||
hashIndex.setLabel(newValue);
|
||||
EventManager.get().post(new WalletEntryLabelsChangedEvent(wallet, this));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -346,15 +346,15 @@ public class KeystoreController extends WalletFormController implements Initiali
|
||||
if(password.isPresent()) {
|
||||
Storage.DecryptWalletService decryptWalletService = new Storage.DecryptWalletService(copy, password.get());
|
||||
decryptWalletService.setOnSucceeded(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(getWalletForm().getWalletFile(), TimedEvent.Action.END, "Done"));
|
||||
EventManager.get().post(new StorageEvent(getWalletForm().getWalletId(), TimedEvent.Action.END, "Done"));
|
||||
Wallet decryptedWallet = decryptWalletService.getValue();
|
||||
showPrivate(decryptedWallet.getKeystores().get(keystoreIndex));
|
||||
});
|
||||
decryptWalletService.setOnFailed(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(getWalletForm().getWalletFile(), TimedEvent.Action.END, "Failed"));
|
||||
EventManager.get().post(new StorageEvent(getWalletForm().getWalletId(), TimedEvent.Action.END, "Failed"));
|
||||
AppServices.showErrorDialog("Incorrect Password", decryptWalletService.getException().getMessage());
|
||||
});
|
||||
EventManager.get().post(new StorageEvent(getWalletForm().getWalletFile(), TimedEvent.Action.START, "Decrypting wallet..."));
|
||||
EventManager.get().post(new StorageEvent(getWalletForm().getWalletId(), TimedEvent.Action.START, "Decrypting wallet..."));
|
||||
decryptWalletService.start();
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -2,14 +2,14 @@ package com.sparrowwallet.sparrow.wallet;
|
||||
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.protocol.Script;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletNode;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.WalletEntryLabelsChangedEvent;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class NodeEntry extends Entry implements Comparable<NodeEntry> {
|
||||
@@ -20,8 +20,10 @@ public class NodeEntry extends Entry implements Comparable<NodeEntry> {
|
||||
this.node = node;
|
||||
|
||||
labelProperty().addListener((observable, oldValue, newValue) -> {
|
||||
node.setLabel(newValue);
|
||||
EventManager.get().post(new WalletEntryLabelsChangedEvent(wallet, this));
|
||||
if(!Objects.equals(node.getLabel(), newValue)) {
|
||||
node.setLabel(newValue);
|
||||
EventManager.get().post(new WalletEntryLabelsChangedEvent(wallet, this));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -78,4 +80,45 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.control.*;
|
||||
import com.sparrowwallet.sparrow.event.*;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
import com.sparrowwallet.sparrow.io.StorageException;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.event.ActionEvent;
|
||||
@@ -399,7 +400,16 @@ public class SettingsController extends WalletFormController implements Initiali
|
||||
|
||||
Optional<Wallet> optWallet = AppServices.get().getOpenWallets().entrySet().stream().filter(entry -> walletForm.getWalletFile().equals(entry.getValue().getWalletFile())).map(Map.Entry::getKey).findFirst();
|
||||
if(optWallet.isPresent()) {
|
||||
WalletExportDialog dlg = new WalletExportDialog(optWallet.get());
|
||||
Wallet wallet = optWallet.get();
|
||||
if(!walletForm.getWallet().getName().equals(wallet.getName())) {
|
||||
wallet = wallet.getChildWallet(walletForm.getWallet().getName());
|
||||
}
|
||||
|
||||
if(wallet == null) {
|
||||
throw new IllegalStateException("Cannot find child wallet " + walletForm.getWallet().getName() + " to export");
|
||||
}
|
||||
|
||||
WalletExportDialog dlg = new WalletExportDialog(wallet);
|
||||
dlg.showAndWait();
|
||||
} else {
|
||||
AppServices.showErrorDialog("Cannot export wallet", "Wallet cannot be exported, please save it first.");
|
||||
@@ -441,25 +451,23 @@ public class SettingsController extends WalletFormController implements Initiali
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletSettingsChanged(WalletSettingsChangedEvent event) {
|
||||
if(event.getWalletFile().equals(walletForm.getWalletFile())) {
|
||||
public void walletAddressesChanged(WalletAddressesChangedEvent event) {
|
||||
if(event.getWalletId().equals(walletForm.getWalletId())) {
|
||||
export.setDisable(!event.getWallet().isValid());
|
||||
scanDescriptorQR.setVisible(!event.getWallet().isValid());
|
||||
updateBirthDate(event.getWallet());
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletAddressesChanged(WalletAddressesChangedEvent event) {
|
||||
updateBirthDate(event.getWalletFile(), event.getWallet());
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletHistoryChanged(WalletHistoryChangedEvent event) {
|
||||
updateBirthDate(event.getWalletFile(), event.getWallet());
|
||||
if(event.getWalletId().equals(walletForm.getWalletId())) {
|
||||
updateBirthDate(event.getWallet());
|
||||
}
|
||||
}
|
||||
|
||||
private void updateBirthDate(File walletFile, Wallet wallet) {
|
||||
if(walletFile.equals(walletForm.getWalletFile()) && !Objects.equals(wallet.getBirthDate(), walletForm.getWallet().getBirthDate())) {
|
||||
private void updateBirthDate(Wallet wallet) {
|
||||
if(!Objects.equals(wallet.getBirthDate(), walletForm.getWallet().getBirthDate())) {
|
||||
walletForm.getWallet().setBirthDate(wallet.getBirthDate());
|
||||
}
|
||||
}
|
||||
@@ -509,7 +517,7 @@ public class SettingsController extends WalletFormController implements Initiali
|
||||
walletForm.getStorage().setEncryptionPubKey(Storage.NO_PASSWORD_KEY);
|
||||
walletForm.saveAndRefresh();
|
||||
EventManager.get().post(new RequestOpenWalletsEvent());
|
||||
} catch (IOException e) {
|
||||
} catch (IOException | StorageException e) {
|
||||
log.error("Error saving wallet", e);
|
||||
AppServices.showErrorDialog("Error saving wallet", e.getMessage());
|
||||
revert.setDisable(false);
|
||||
@@ -518,7 +526,7 @@ public class SettingsController extends WalletFormController implements Initiali
|
||||
} else {
|
||||
Storage.KeyDerivationService keyDerivationService = new Storage.KeyDerivationService(walletForm.getStorage(), password.get());
|
||||
keyDerivationService.setOnSucceeded(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(walletForm.getWalletFile(), TimedEvent.Action.END, "Done"));
|
||||
EventManager.get().post(new StorageEvent(walletForm.getWalletId(), TimedEvent.Action.END, "Done"));
|
||||
ECKey encryptionFullKey = keyDerivationService.getValue();
|
||||
Key key = null;
|
||||
|
||||
@@ -566,12 +574,12 @@ public class SettingsController extends WalletFormController implements Initiali
|
||||
}
|
||||
});
|
||||
keyDerivationService.setOnFailed(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(walletForm.getWalletFile(), TimedEvent.Action.END, "Failed"));
|
||||
EventManager.get().post(new StorageEvent(walletForm.getWalletId(), TimedEvent.Action.END, "Failed"));
|
||||
AppServices.showErrorDialog("Error saving wallet", keyDerivationService.getException().getMessage());
|
||||
revert.setDisable(false);
|
||||
apply.setDisable(false);
|
||||
});
|
||||
EventManager.get().post(new StorageEvent(walletForm.getWalletFile(), TimedEvent.Action.START, "Encrypting wallet..."));
|
||||
EventManager.get().post(new StorageEvent(walletForm.getWalletId(), TimedEvent.Action.START, "Encrypting wallet..."));
|
||||
keyDerivationService.start();
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -5,11 +5,15 @@ import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.KeystoreLabelsChangedEvent;
|
||||
import com.sparrowwallet.sparrow.event.WalletAddressesChangedEvent;
|
||||
import com.sparrowwallet.sparrow.event.WalletSettingsChangedEvent;
|
||||
import com.sparrowwallet.sparrow.event.WalletPasswordChangedEvent;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
import com.sparrowwallet.sparrow.io.StorageException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
@@ -40,30 +44,65 @@ public class SettingsWalletForm extends WalletForm {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveAndRefresh() throws IOException {
|
||||
Wallet pastWallet = null;
|
||||
public void saveAndRefresh() throws IOException, StorageException {
|
||||
Wallet pastWallet = wallet.copy();
|
||||
|
||||
boolean refreshAll = isRefreshNecessary(wallet, walletCopy);
|
||||
if(refreshAll) {
|
||||
pastWallet = wallet.copy();
|
||||
save(); //Save here for the temp backup in case password has been changed
|
||||
if(AppServices.isConnected()) {
|
||||
getStorage().backupTempWallet();
|
||||
if(isRefreshNecessary(wallet, walletCopy)) {
|
||||
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);
|
||||
}
|
||||
|
||||
//Clear node tree
|
||||
walletCopy.clearNodes();
|
||||
}
|
||||
|
||||
wallet = walletCopy.copy();
|
||||
save();
|
||||
Integer childIndex = wallet.isMasterWallet() ? null : wallet.getMasterWallet().getChildWallets().indexOf(wallet);
|
||||
|
||||
if(refreshAll) {
|
||||
EventManager.get().post(new WalletAddressesChangedEvent(wallet, pastWallet, getWalletFile()));
|
||||
//Replace the SettingsWalletForm wallet reference - note that this reference is only shared with the WalletForm wallet with WalletAddressesChangedEvent below
|
||||
wallet = walletCopy.copy();
|
||||
|
||||
if(wallet.isMasterWallet()) {
|
||||
wallet.getChildWallets().forEach(childWallet -> childWallet.setMasterWallet(wallet));
|
||||
} else if(childIndex != null) {
|
||||
wallet.getMasterWallet().getChildWallets().set(childIndex, wallet);
|
||||
}
|
||||
|
||||
save();
|
||||
|
||||
EventManager.get().post(new WalletAddressesChangedEvent(wallet, addressChange ? null : pastWallet, getWalletId()));
|
||||
} else {
|
||||
EventManager.get().post(new WalletSettingsChangedEvent(wallet, pastWallet, getWalletFile()));
|
||||
List<Keystore> changedKeystores = new ArrayList<>();
|
||||
for(int i = 0; i < wallet.getKeystores().size(); i++) {
|
||||
Keystore keystore = wallet.getKeystores().get(i);
|
||||
Keystore keystoreCopy = walletCopy.getKeystores().get(i);
|
||||
if(!Objects.equals(keystore.getLabel(), keystoreCopy.getLabel())) {
|
||||
keystore.setLabel(keystoreCopy.getLabel());
|
||||
changedKeystores.add(keystore);
|
||||
}
|
||||
}
|
||||
|
||||
if(!changedKeystores.isEmpty()) {
|
||||
EventManager.get().post(new KeystoreLabelsChangedEvent(wallet, pastWallet, getWalletId(), changedKeystores));
|
||||
} else {
|
||||
//Can only be a password update at this point
|
||||
EventManager.get().post(new WalletPasswordChangedEvent(wallet, pastWallet, getWalletId()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Returns true for any change, other than a keystore label change, to trigger a full wallet refresh
|
||||
//Even though this is not strictly necessary for some changes, it it better to refresh on saving so background transaction history updates on the old wallet have no effect/are not lost
|
||||
private boolean isRefreshNecessary(Wallet original, Wallet changed) {
|
||||
if(!original.isValid() || !changed.isValid()) {
|
||||
return true;
|
||||
@@ -73,6 +112,27 @@ public class SettingsWalletForm extends WalletForm {
|
||||
return true;
|
||||
}
|
||||
|
||||
for(int i = 0; i < original.getKeystores().size(); i++) {
|
||||
Keystore originalKeystore = original.getKeystores().get(i);
|
||||
Keystore changedKeystore = changed.getKeystores().get(i);
|
||||
|
||||
if(originalKeystore.getSource() != changedKeystore.getSource()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if(originalKeystore.getWalletModel() != changedKeystore.getWalletModel()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if((originalKeystore.getSeed() == null && changedKeystore.getSeed() != null) || (originalKeystore.getSeed() != null && changedKeystore.getSeed() == null)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if((originalKeystore.getMasterPrivateExtendedKey() == null && changedKeystore.getMasterPrivateExtendedKey() != null) || (originalKeystore.getMasterPrivateExtendedKey() != null && changedKeystore.getMasterPrivateExtendedKey() == null)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if(original.getGapLimit() != changed.getGapLimit()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.protocol.TransactionInput;
|
||||
import com.sparrowwallet.drongo.protocol.TransactionOutput;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.WalletTabData;
|
||||
import com.sparrowwallet.sparrow.event.WalletBlockHeightChangedEvent;
|
||||
@@ -30,8 +31,10 @@ public class TransactionEntry extends Entry implements Comparable<TransactionEnt
|
||||
this.blockTransaction = blockTransaction;
|
||||
|
||||
labelProperty().addListener((observable, oldValue, newValue) -> {
|
||||
blockTransaction.setLabel(newValue);
|
||||
EventManager.get().post(new WalletEntryLabelsChangedEvent(wallet, this));
|
||||
if(!Objects.equals(blockTransaction.getLabel(), newValue)) {
|
||||
blockTransaction.setLabel(newValue);
|
||||
EventManager.get().post(new WalletEntryLabelsChangedEvent(wallet, this));
|
||||
}
|
||||
});
|
||||
|
||||
setConfirmations(calculateConfirmations());
|
||||
@@ -68,7 +71,7 @@ public class TransactionEntry extends Entry implements Comparable<TransactionEnt
|
||||
}
|
||||
|
||||
public int calculateConfirmations() {
|
||||
return blockTransaction.getConfirmations(getWallet().getStoredBlockHeight());
|
||||
return blockTransaction.getConfirmations(AppServices.getCurrentBlockHeight() == null ? getWallet().getStoredBlockHeight() : AppServices.getCurrentBlockHeight());
|
||||
}
|
||||
|
||||
public String getConfirmationsDescription() {
|
||||
|
||||
@@ -107,7 +107,7 @@ public class WalletController extends WalletFormController implements Initializa
|
||||
|
||||
@Subscribe
|
||||
public void walletAddressesChanged(WalletAddressesChangedEvent event) {
|
||||
if(event.getWalletFile().equals(walletForm.getWalletFile())) {
|
||||
if(event.getWalletId().equals(walletForm.getWalletId())) {
|
||||
configure(event.getWallet().isValid());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ 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.ElectrumServer;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
import com.sparrowwallet.sparrow.net.ServerType;
|
||||
@@ -22,6 +23,7 @@ import org.slf4j.LoggerFactory;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class WalletForm {
|
||||
private static final Logger log = LoggerFactory.getLogger(WalletForm.class);
|
||||
@@ -60,6 +62,10 @@ public class WalletForm {
|
||||
return storage;
|
||||
}
|
||||
|
||||
public String getWalletId() {
|
||||
return storage.getWalletId(wallet);
|
||||
}
|
||||
|
||||
public File getWalletFile() {
|
||||
return storage.getWalletFile();
|
||||
}
|
||||
@@ -72,11 +78,11 @@ public class WalletForm {
|
||||
throw new UnsupportedOperationException("Only SettingsWalletForm supports revert");
|
||||
}
|
||||
|
||||
public void save() throws IOException {
|
||||
public void save() throws IOException, StorageException {
|
||||
storage.saveWallet(wallet);
|
||||
}
|
||||
|
||||
public void saveAndRefresh() throws IOException {
|
||||
public void saveAndRefresh() throws IOException, StorageException {
|
||||
Wallet pastWallet = wallet.copy();
|
||||
storage.backupTempWallet();
|
||||
wallet.clearHistory();
|
||||
@@ -88,6 +94,15 @@ public class WalletForm {
|
||||
storage.backupWallet();
|
||||
}
|
||||
|
||||
protected void backgroundUpdate() {
|
||||
try {
|
||||
storage.updateWallet(wallet);
|
||||
} catch (IOException | StorageException e) {
|
||||
//Background save failed
|
||||
log.error("Background wallet save failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void deleteBackups() {
|
||||
storage.deleteBackups();
|
||||
}
|
||||
@@ -127,36 +142,61 @@ public class WalletForm {
|
||||
wallet.setStoredBlockHeight(blockHeight);
|
||||
}
|
||||
|
||||
boolean labelsChanged = false;
|
||||
//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) {
|
||||
labelsChanged = copyLabels(pastWallet);
|
||||
labelChangedEntries = copyLabels(pastWallet);
|
||||
}
|
||||
|
||||
notifyIfChanged(blockHeight, previousWallet, labelsChanged);
|
||||
notifyIfChanged(blockHeight, previousWallet, labelChangedEntries);
|
||||
}
|
||||
|
||||
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));
|
||||
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
|
||||
List<KeyPurpose> keyPurposes = List.of(KeyPurpose.RECEIVE, KeyPurpose.CHANGE);
|
||||
for(KeyPurpose keyPurpose : keyPurposes) {
|
||||
NodeEntry purposeEntry = getNodeEntry(keyPurpose);
|
||||
changedEntries.addAll(purposeEntry.copyLabels(pastWallet.getNode(purposeEntry.getNode().getKeyPurpose())));
|
||||
}
|
||||
|
||||
//Copy node and txo labels
|
||||
for(KeyPurpose keyPurpose : keyPurposes) {
|
||||
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());
|
||||
changed = true;
|
||||
changedEntries.add(getWalletTransactionsEntry());
|
||||
}
|
||||
}
|
||||
|
||||
storage.deleteTempBackups();
|
||||
return changed;
|
||||
return changedEntries;
|
||||
}
|
||||
|
||||
private void notifyIfChanged(Integer blockHeight, Wallet previousWallet, boolean labelsChanged) {
|
||||
private void notifyIfChanged(Integer blockHeight, Wallet previousWallet, Set<Entry> labelChangedEntries) {
|
||||
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 = labelsChanged;
|
||||
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;
|
||||
@@ -257,39 +297,43 @@ public class WalletForm {
|
||||
@Subscribe
|
||||
public void walletDataChanged(WalletDataChangedEvent event) {
|
||||
if(event.getWallet().equals(wallet)) {
|
||||
backgroundSaveWallet();
|
||||
}
|
||||
}
|
||||
|
||||
private void backgroundSaveWallet() {
|
||||
try {
|
||||
save();
|
||||
} catch (IOException e) {
|
||||
//Background save failed
|
||||
log.error("Background wallet save failed", e);
|
||||
backgroundUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletSettingsChanged(WalletSettingsChangedEvent event) {
|
||||
if(event.getWalletFile().equals(storage.getWalletFile())) {
|
||||
public void walletHistoryCleared(WalletHistoryClearedEvent event) {
|
||||
if(event.getWalletId().equals(getWalletId())) {
|
||||
//Replacing the WalletForm's wallet here is only possible because we immediately clear all derived structures and do a full wallet refresh
|
||||
wallet = event.getWallet();
|
||||
|
||||
if(event instanceof WalletAddressesChangedEvent) {
|
||||
walletTransactionsEntry = null;
|
||||
walletUtxosEntry = null;
|
||||
accountEntries.clear();
|
||||
EventManager.get().post(new WalletNodesChangedEvent(wallet));
|
||||
walletTransactionsEntry = null;
|
||||
walletUtxosEntry = null;
|
||||
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());
|
||||
//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());
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void keystoreLabelsChanged(KeystoreLabelsChangedEvent event) {
|
||||
if(event.getWalletId().equals(getWalletId())) {
|
||||
Platform.runLater(() -> EventManager.get().post(new WalletDataChangedEvent(wallet)));
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletPasswordChanged(WalletPasswordChangedEvent event) {
|
||||
if(event.getWalletId().equals(getWalletId())) {
|
||||
Platform.runLater(() -> EventManager.get().post(new WalletDataChangedEvent(wallet)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,7 +364,7 @@ public class WalletForm {
|
||||
|
||||
@Subscribe
|
||||
public void walletHistoryChanged(WalletHistoryChangedEvent event) {
|
||||
if(event.getWalletFile().equals(storage.getWalletFile())) {
|
||||
if(event.getWalletId().equals(getWalletId())) {
|
||||
for(WalletNode changedNode : event.getHistoryChangedNodes()) {
|
||||
if(changedNode.getLabel() != null && !changedNode.getLabel().isEmpty()) {
|
||||
List<Entry> changedLabelEntries = new ArrayList<>();
|
||||
@@ -393,14 +437,24 @@ public class WalletForm {
|
||||
|
||||
if(!labelChangedEntries.isEmpty()) {
|
||||
Platform.runLater(() -> EventManager.get().post(new WalletEntryLabelsChangedEvent(wallet, labelChangedEntries)));
|
||||
} else {
|
||||
Platform.runLater(() -> EventManager.get().post(new WalletDataChangedEvent(wallet)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletUtxoStatusChanged(WalletUtxoStatusChangedEvent event) {
|
||||
if(event.getWallet() == wallet) {
|
||||
Platform.runLater(() -> EventManager.get().post(new WalletDataChangedEvent(wallet)));
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletTabsClosed(WalletTabsClosedEvent event) {
|
||||
for(WalletTabData tabData : event.getClosedWalletTabData()) {
|
||||
if(tabData.getWalletForm() == this) {
|
||||
storage.close();
|
||||
AppServices.clearTransactionHistoryCache(wallet);
|
||||
EventManager.get().unregister(this);
|
||||
}
|
||||
|
||||
@@ -23,11 +23,16 @@ open module com.sparrowwallet.sparrow {
|
||||
requires hummingbird;
|
||||
requires centerdevice.nsmenufx;
|
||||
requires jcommander;
|
||||
requires slf4j.api;
|
||||
requires org.slf4j;
|
||||
requires bwt.jni;
|
||||
requires jtorctl;
|
||||
requires javacsv;
|
||||
requires jul.to.slf4j;
|
||||
requires bridj;
|
||||
requires com.google.gson;
|
||||
requires org.jdbi.v3.core;
|
||||
requires org.jdbi.v3.sqlobject;
|
||||
requires org.flywaydb.core;
|
||||
requires com.zaxxer.hikari;
|
||||
requires com.h2database;
|
||||
}
|
||||
Reference in New Issue
Block a user