mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-12-24 12:46:45 +00:00
introduce database persistence with automatic migration of existing wallets
This commit is contained in:
parent
600a77da3a
commit
a59d5d3086
52 changed files with 1989 additions and 283 deletions
|
@ -44,6 +44,13 @@ dependencies {
|
||||||
}
|
}
|
||||||
implementation('com.google.guava:guava:28.2-jre')
|
implementation('com.google.guava:guava:28.2-jre')
|
||||||
implementation('com.google.code.gson:gson:2.8.6')
|
implementation('com.google.code.gson:gson:2.8.6')
|
||||||
|
implementation('com.h2database:h2:1.4.200')
|
||||||
|
implementation('com.zaxxer:HikariCP:4.0.3')
|
||||||
|
implementation('org.jdbi:jdbi3-core:3.20.0') {
|
||||||
|
exclude group: 'org.slf4j'
|
||||||
|
}
|
||||||
|
implementation('org.jdbi:jdbi3-sqlobject:3.20.0')
|
||||||
|
implementation('org.flywaydb:flyway-core:7.9.1')
|
||||||
implementation('org.fxmisc.richtext:richtextfx:0.10.4')
|
implementation('org.fxmisc.richtext:richtextfx:0.10.4')
|
||||||
implementation('no.tornado:tornadofx-controls:1.0.4')
|
implementation('no.tornado:tornadofx-controls:1.0.4')
|
||||||
implementation('com.google.zxing:javase:3.4.0')
|
implementation('com.google.zxing:javase:3.4.0')
|
||||||
|
|
2
drongo
2
drongo
|
@ -1 +1 @@
|
||||||
Subproject commit 42ffeb95650c56bffbd5ec8f8e8f38d91faaab3f
|
Subproject commit 8e3d0d23c129b7fe9eedb16769827155f53c84d5
|
|
@ -762,11 +762,10 @@ public class AppController implements Initializable {
|
||||||
public void openWalletFile(File file, boolean forceSameWindow) {
|
public void openWalletFile(File file, boolean forceSameWindow) {
|
||||||
try {
|
try {
|
||||||
Storage storage = new Storage(file);
|
Storage storage = new Storage(file);
|
||||||
FileType fileType = IOUtils.getFileType(file);
|
if(!storage.isEncrypted()) {
|
||||||
if(FileType.JSON.equals(fileType)) {
|
|
||||||
WalletBackupAndKey walletBackupAndKey = storage.loadUnencryptedWallet();
|
WalletBackupAndKey walletBackupAndKey = storage.loadUnencryptedWallet();
|
||||||
openWallet(storage, walletBackupAndKey, this, forceSameWindow);
|
openWallet(storage, walletBackupAndKey, this, forceSameWindow);
|
||||||
} else if(FileType.BINARY.equals(fileType)) {
|
} else {
|
||||||
WalletPasswordDialog dlg = new WalletPasswordDialog(file.getName(), WalletPasswordDialog.PasswordRequirement.LOAD);
|
WalletPasswordDialog dlg = new WalletPasswordDialog(file.getName(), WalletPasswordDialog.PasswordRequirement.LOAD);
|
||||||
Optional<SecureString> optionalPassword = dlg.showAndWait();
|
Optional<SecureString> optionalPassword = dlg.showAndWait();
|
||||||
if(optionalPassword.isEmpty()) {
|
if(optionalPassword.isEmpty()) {
|
||||||
|
@ -776,12 +775,12 @@ public class AppController implements Initializable {
|
||||||
SecureString password = optionalPassword.get();
|
SecureString password = optionalPassword.get();
|
||||||
Storage.LoadWalletService loadWalletService = new Storage.LoadWalletService(storage, password);
|
Storage.LoadWalletService loadWalletService = new Storage.LoadWalletService(storage, password);
|
||||||
loadWalletService.setOnSucceeded(workerStateEvent -> {
|
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();
|
WalletBackupAndKey walletBackupAndKey = loadWalletService.getValue();
|
||||||
openWallet(storage, walletBackupAndKey, this, forceSameWindow);
|
openWallet(storage, walletBackupAndKey, this, forceSameWindow);
|
||||||
});
|
});
|
||||||
loadWalletService.setOnFailed(workerStateEvent -> {
|
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();
|
Throwable exception = loadWalletService.getException();
|
||||||
if(exception instanceof InvalidPasswordException) {
|
if(exception instanceof InvalidPasswordException) {
|
||||||
Optional<ButtonType> optResponse = showErrorDialog("Invalid Password", "The wallet password was invalid. Try again?", ButtonType.CANCEL, ButtonType.OK);
|
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();
|
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();
|
loadWalletService.start();
|
||||||
} else {
|
|
||||||
throw new IOException("Unsupported file type");
|
|
||||||
}
|
}
|
||||||
|
} catch(StorageException e) {
|
||||||
|
showErrorDialog("Error Opening Wallet", e.getMessage());
|
||||||
} catch(Exception e) {
|
} catch(Exception e) {
|
||||||
if(!attemptImportWallet(file, null)) {
|
if(!attemptImportWallet(file, null)) {
|
||||||
log.error("Error opening wallet", e);
|
log.error("Error opening wallet", e);
|
||||||
|
@ -822,6 +821,7 @@ public class AppController implements Initializable {
|
||||||
}
|
}
|
||||||
Platform.runLater(() -> selectTab(walletBackupAndKey.getWallet()));
|
Platform.runLater(() -> selectTab(walletBackupAndKey.getWallet()));
|
||||||
} catch(Exception e) {
|
} catch(Exception e) {
|
||||||
|
log.error("Error opening wallet", e);
|
||||||
showErrorDialog("Error Opening Wallet", e.getMessage());
|
showErrorDialog("Error Opening Wallet", e.getMessage());
|
||||||
} finally {
|
} finally {
|
||||||
walletBackupAndKey.clear();
|
walletBackupAndKey.clear();
|
||||||
|
@ -969,13 +969,13 @@ public class AppController implements Initializable {
|
||||||
storage.setEncryptionPubKey(Storage.NO_PASSWORD_KEY);
|
storage.setEncryptionPubKey(Storage.NO_PASSWORD_KEY);
|
||||||
storage.saveWallet(wallet);
|
storage.saveWallet(wallet);
|
||||||
addWalletTabOrWindow(storage, wallet, null, false);
|
addWalletTabOrWindow(storage, wallet, null, false);
|
||||||
} catch(IOException e) {
|
} catch(IOException | StorageException e) {
|
||||||
log.error("Error saving imported wallet", e);
|
log.error("Error saving imported wallet", e);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Storage.KeyDerivationService keyDerivationService = new Storage.KeyDerivationService(storage, password.get());
|
Storage.KeyDerivationService keyDerivationService = new Storage.KeyDerivationService(storage, password.get());
|
||||||
keyDerivationService.setOnSucceeded(workerStateEvent -> {
|
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();
|
ECKey encryptionFullKey = keyDerivationService.getValue();
|
||||||
Key key = null;
|
Key key = null;
|
||||||
|
|
||||||
|
@ -986,7 +986,7 @@ public class AppController implements Initializable {
|
||||||
storage.setEncryptionPubKey(encryptionPubKey);
|
storage.setEncryptionPubKey(encryptionPubKey);
|
||||||
storage.saveWallet(wallet);
|
storage.saveWallet(wallet);
|
||||||
addWalletTabOrWindow(storage, wallet, null, false);
|
addWalletTabOrWindow(storage, wallet, null, false);
|
||||||
} catch(IOException e) {
|
} catch(IOException | StorageException e) {
|
||||||
log.error("Error saving imported wallet", e);
|
log.error("Error saving imported wallet", e);
|
||||||
} finally {
|
} finally {
|
||||||
encryptionFullKey.clear();
|
encryptionFullKey.clear();
|
||||||
|
@ -996,10 +996,10 @@ public class AppController implements Initializable {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
keyDerivationService.setOnFailed(workerStateEvent -> {
|
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());
|
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();
|
keyDerivationService.start();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1080,12 +1080,12 @@ public class AppController implements Initializable {
|
||||||
walletTabData.getStorage().backupTempWallet();
|
walletTabData.getStorage().backupTempWallet();
|
||||||
wallet.clearHistory();
|
wallet.clearHistory();
|
||||||
AppServices.clearTransactionHistoryCache(wallet);
|
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) {
|
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) {
|
if(existingWalletWindow instanceof Stage) {
|
||||||
Stage existingWalletStage = (Stage)existingWalletWindow;
|
Stage existingWalletStage = (Stage)existingWalletWindow;
|
||||||
existingWalletStage.toFront();
|
existingWalletStage.toFront();
|
||||||
|
@ -1109,10 +1109,7 @@ public class AppController implements Initializable {
|
||||||
|
|
||||||
public void addWalletTab(Storage storage, Wallet wallet, Wallet backupWallet) {
|
public void addWalletTab(Storage storage, Wallet wallet, Wallet backupWallet) {
|
||||||
try {
|
try {
|
||||||
String name = storage.getWalletFile().getName();
|
String name = storage.getWalletName(wallet);
|
||||||
if(name.endsWith(".json")) {
|
|
||||||
name = name.substring(0, name.lastIndexOf('.'));
|
|
||||||
}
|
|
||||||
if(!name.equals(wallet.getName())) {
|
if(!name.equals(wallet.getName())) {
|
||||||
wallet.setName(name);
|
wallet.setName(name);
|
||||||
}
|
}
|
||||||
|
@ -1504,7 +1501,7 @@ public class AppController implements Initializable {
|
||||||
TabData tabData = (TabData)tab.getUserData();
|
TabData tabData = (TabData)tab.getUserData();
|
||||||
if(tabData instanceof WalletTabData) {
|
if(tabData instanceof WalletTabData) {
|
||||||
WalletTabData walletTabData = (WalletTabData)tabData;
|
WalletTabData walletTabData = (WalletTabData)tabData;
|
||||||
if(walletTabData.getWalletForm().getWalletFile().equals(event.getWalletFile())) {
|
if(walletTabData.getWalletForm().getWalletId().equals(event.getWalletId())) {
|
||||||
exportWallet.setDisable(!event.getWallet().isValid());
|
exportWallet.setDisable(!event.getWallet().isValid());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1819,6 +1816,17 @@ public class AppController implements Initializable {
|
||||||
@Subscribe
|
@Subscribe
|
||||||
public void viewWallet(ViewWalletEvent event) {
|
public void viewWallet(ViewWalletEvent event) {
|
||||||
if(tabs.getScene().getWindow().equals(event.getWindow())) {
|
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()) {
|
for(Tab tab : tabs.getTabs()) {
|
||||||
TabData tabData = (TabData) tab.getUserData();
|
TabData tabData = (TabData) tab.getUserData();
|
||||||
if(tabData.getType() == TabData.TabType.WALLET) {
|
if(tabData.getType() == TabData.TabType.WALLET) {
|
||||||
|
|
|
@ -469,8 +469,8 @@ public class AppServices {
|
||||||
return openWallets;
|
return openWallets;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Window getWindowForWallet(Storage storage) {
|
public Window getWindowForWallet(String walletId) {
|
||||||
Optional<Window> optWindow = walletWindows.entrySet().stream().filter(entry -> entry.getValue().stream().anyMatch(walletTabData -> walletTabData.getStorage().getWalletFile().equals(storage.getWalletFile()))).map(Map.Entry::getKey).findFirst();
|
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);
|
return optWindow.orElse(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -544,8 +544,7 @@ public class AppServices {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isWalletFile(File file) {
|
public static boolean isWalletFile(File file) {
|
||||||
FileType fileType = IOUtils.getFileType(file);
|
return Storage.isWalletFile(file);
|
||||||
return FileType.JSON.equals(fileType) || FileType.BINARY.equals(fileType);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Optional<ButtonType> showWarningDialog(String title, String content, ButtonType... buttons) {
|
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) {
|
public static void moveToWindowScreen(Window currentWindow, Dialog<?> dialog) {
|
||||||
Window newWindow = dialog.getDialogPane().getScene().getWindow();
|
Window newWindow = dialog.getDialogPane().getScene().getWindow();
|
||||||
DialogPane dialogPane = dialog.getDialogPane();
|
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.FontAwesome5;
|
||||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5Brands;
|
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5Brands;
|
||||||
import com.sparrowwallet.sparrow.io.Config;
|
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.io.Storage;
|
||||||
import com.sparrowwallet.sparrow.net.Bwt;
|
import com.sparrowwallet.sparrow.net.Bwt;
|
||||||
import com.sparrowwallet.sparrow.net.PublicElectrumServer;
|
import com.sparrowwallet.sparrow.net.PublicElectrumServer;
|
||||||
|
@ -99,9 +97,8 @@ public class MainApp extends Application {
|
||||||
|
|
||||||
List<File> recentWalletFiles = Config.get().getRecentWalletFiles();
|
List<File> recentWalletFiles = Config.get().getRecentWalletFiles();
|
||||||
if(recentWalletFiles != null) {
|
if(recentWalletFiles != null) {
|
||||||
//Re-sort to preserve wallet order as far as possible. Unencrypted wallets will still be opened first.
|
//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());
|
List<File> encryptedWalletFiles = recentWalletFiles.stream().filter(Storage::isEncrypted).collect(Collectors.toList());
|
||||||
Collections.reverse(encryptedWalletFiles);
|
|
||||||
List<File> sortedWalletFiles = new ArrayList<>(recentWalletFiles);
|
List<File> sortedWalletFiles = new ArrayList<>(recentWalletFiles);
|
||||||
sortedWalletFiles.removeAll(encryptedWalletFiles);
|
sortedWalletFiles.removeAll(encryptedWalletFiles);
|
||||||
sortedWalletFiles.addAll(encryptedWalletFiles);
|
sortedWalletFiles.addAll(encryptedWalletFiles);
|
||||||
|
|
|
@ -5,8 +5,8 @@ import com.sparrowwallet.drongo.wallet.Wallet;
|
||||||
import com.sparrowwallet.sparrow.AppServices;
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
import com.sparrowwallet.sparrow.EventManager;
|
import com.sparrowwallet.sparrow.EventManager;
|
||||||
import com.sparrowwallet.sparrow.event.WalletDataChangedEvent;
|
import com.sparrowwallet.sparrow.event.WalletDataChangedEvent;
|
||||||
|
import com.sparrowwallet.sparrow.event.WalletHistoryClearedEvent;
|
||||||
import com.sparrowwallet.sparrow.event.WalletHistoryStatusEvent;
|
import com.sparrowwallet.sparrow.event.WalletHistoryStatusEvent;
|
||||||
import com.sparrowwallet.sparrow.event.WalletAddressesChangedEvent;
|
|
||||||
import com.sparrowwallet.sparrow.io.Config;
|
import com.sparrowwallet.sparrow.io.Config;
|
||||||
import com.sparrowwallet.sparrow.io.Storage;
|
import com.sparrowwallet.sparrow.io.Storage;
|
||||||
import com.sparrowwallet.sparrow.net.ServerType;
|
import com.sparrowwallet.sparrow.net.ServerType;
|
||||||
|
@ -92,7 +92,7 @@ public class CoinTreeTable extends TreeTableView<Entry> {
|
||||||
EventManager.get().post(new WalletDataChangedEvent(wallet));
|
EventManager.get().post(new WalletDataChangedEvent(wallet));
|
||||||
//Trigger full wallet rescan
|
//Trigger full wallet rescan
|
||||||
wallet.clearHistory();
|
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) {
|
if(wallet.getBirthDate() == null) {
|
||||||
|
|
|
@ -100,10 +100,10 @@ public class FileWalletExportPane extends TitledDescriptionPane {
|
||||||
WalletPasswordDialog dlg = new WalletPasswordDialog(wallet.getName(), WalletPasswordDialog.PasswordRequirement.LOAD);
|
WalletPasswordDialog dlg = new WalletPasswordDialog(wallet.getName(), WalletPasswordDialog.PasswordRequirement.LOAD);
|
||||||
Optional<SecureString> password = dlg.showAndWait();
|
Optional<SecureString> password = dlg.showAndWait();
|
||||||
if(password.isPresent()) {
|
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());
|
Storage.DecryptWalletService decryptWalletService = new Storage.DecryptWalletService(copy, password.get());
|
||||||
decryptWalletService.setOnSucceeded(workerStateEvent -> {
|
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();
|
Wallet decryptedWallet = decryptWalletService.getValue();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -113,10 +113,10 @@ public class FileWalletExportPane extends TitledDescriptionPane {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
decryptWalletService.setOnFailed(workerStateEvent -> {
|
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());
|
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();
|
decryptWalletService.start();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -368,16 +368,16 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
if(password.isPresent()) {
|
if(password.isPresent()) {
|
||||||
Storage.DecryptWalletService decryptWalletService = new Storage.DecryptWalletService(wallet.copy(), password.get());
|
Storage.DecryptWalletService decryptWalletService = new Storage.DecryptWalletService(wallet.copy(), password.get());
|
||||||
decryptWalletService.setOnSucceeded(workerStateEvent -> {
|
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();
|
Wallet decryptedWallet = decryptWalletService.getValue();
|
||||||
signUnencryptedKeystore(decryptedWallet);
|
signUnencryptedKeystore(decryptedWallet);
|
||||||
decryptedWallet.clearPrivate();
|
decryptedWallet.clearPrivate();
|
||||||
});
|
});
|
||||||
decryptWalletService.setOnFailed(workerStateEvent -> {
|
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());
|
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();
|
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 {
|
public class StorageEvent extends TimedEvent {
|
||||||
private static boolean firstRunDone = false;
|
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);
|
super(action, status);
|
||||||
|
|
||||||
Integer keyDerivationPeriod = Config.get().getKeyDerivationPeriod();
|
Integer keyDerivationPeriod = Config.get().getKeyDerivationPeriod();
|
||||||
|
@ -19,10 +19,10 @@ public class StorageEvent extends TimedEvent {
|
||||||
}
|
}
|
||||||
|
|
||||||
if(action == Action.START) {
|
if(action == Action.START) {
|
||||||
eventTime.put(file, System.currentTimeMillis());
|
eventTime.put(walletId, System.currentTimeMillis());
|
||||||
timeMills = keyDerivationPeriod;
|
timeMills = keyDerivationPeriod;
|
||||||
} else if(action == Action.END) {
|
} else if(action == Action.END) {
|
||||||
long start = eventTime.get(file);
|
long start = eventTime.get(walletId);
|
||||||
if(firstRunDone) {
|
if(firstRunDone) {
|
||||||
keyDerivationPeriod = (int)(System.currentTimeMillis() - start);
|
keyDerivationPeriod = (int)(System.currentTimeMillis() - start);
|
||||||
Config.get().setKeyDerivationPeriod(keyDerivationPeriod);
|
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.
|
* 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.
|
* 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 class WalletAddressesChangedEvent extends WalletHistoryClearedEvent {
|
||||||
public WalletAddressesChangedEvent(Wallet wallet, Wallet pastWallet, File walletFile) {
|
public WalletAddressesChangedEvent(Wallet wallet, Wallet pastWallet, String walletId) {
|
||||||
super(wallet, pastWallet, walletFile);
|
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.
|
* 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;
|
private final List<Entry> entries;
|
||||||
|
|
||||||
public WalletEntryLabelsChangedEvent(Wallet wallet, Entry entry) {
|
public WalletEntryLabelsChangedEvent(Wallet wallet, Entry entry) {
|
||||||
|
|
|
@ -23,8 +23,8 @@ public class WalletHistoryChangedEvent extends WalletChangedEvent {
|
||||||
this.historyChangedNodes = historyChangedNodes;
|
this.historyChangedNodes = historyChangedNodes;
|
||||||
}
|
}
|
||||||
|
|
||||||
public File getWalletFile() {
|
public String getWalletId() {
|
||||||
return storage.getWalletFile();
|
return storage.getWalletId(getWallet());
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<WalletNode> getHistoryChangedNodes() {
|
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;
|
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.
|
* This is the base class for events posted when a wallet's settings are changed
|
||||||
* For example, changing the keystore source or label will trigger this event, which updates the wallet but avoids a full wallet refresh.
|
* Do not listen for this event directly - listen for a subclass, for example KeystoreLabelsChangedEvent or WalletAddressesChangedEvent
|
||||||
* It is impossible for the validity of a wallet to change on this event - listen for WalletAddressesChangedEvent for this
|
|
||||||
*/
|
*/
|
||||||
public class WalletSettingsChangedEvent extends WalletChangedEvent {
|
public class WalletSettingsChangedEvent extends WalletChangedEvent {
|
||||||
private final Wallet pastWallet;
|
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);
|
super(wallet);
|
||||||
this.pastWallet = pastWallet;
|
this.pastWallet = pastWallet;
|
||||||
this.walletFile = walletFile;
|
this.walletId = walletId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Wallet getPastWallet() {
|
public Wallet getPastWallet() {
|
||||||
return pastWallet;
|
return pastWallet;
|
||||||
}
|
}
|
||||||
|
|
||||||
public File getWalletFile() {
|
public String getWalletId() {
|
||||||
return walletFile;
|
return walletId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ package com.sparrowwallet.sparrow.event;
|
||||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
|
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
|
||||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||||
|
|
||||||
public class WalletUtxoStatusChangedEvent extends WalletDataChangedEvent {
|
public class WalletUtxoStatusChangedEvent extends WalletChangedEvent {
|
||||||
private final BlockTransactionHashIndex utxo;
|
private final BlockTransactionHashIndex utxo;
|
||||||
|
|
||||||
public WalletUtxoStatusChangedEvent(Wallet wallet, BlockTransactionHashIndex utxo) {
|
public WalletUtxoStatusChangedEvent(Wallet wallet, BlockTransactionHashIndex utxo) {
|
||||||
|
|
|
@ -17,6 +17,9 @@
|
||||||
|
|
||||||
package com.sparrowwallet.sparrow.instance;
|
package com.sparrowwallet.sparrow.instance;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.io.BufferedWriter;
|
import java.io.BufferedWriter;
|
||||||
import java.io.DataInputStream;
|
import java.io.DataInputStream;
|
||||||
|
@ -85,6 +88,7 @@ import java.nio.channels.FileLock;
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
public abstract class Instance {
|
public abstract class Instance {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(Instance.class);
|
||||||
|
|
||||||
// starting position of port check
|
// starting position of port check
|
||||||
private static final int PORT_START = 7221;
|
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
|
* @param exception exception occurring while first instance is listening for subsequent instances
|
||||||
*/
|
*/
|
||||||
protected void handleException(Exception exception) {
|
protected void handleException(Exception exception) {
|
||||||
exception.printStackTrace();
|
log.error("Error listening for instances", exception);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -16,10 +16,12 @@ public class IOUtils {
|
||||||
if(file.exists()) {
|
if(file.exists()) {
|
||||||
try(BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8))) {
|
try(BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8))) {
|
||||||
String line = br.readLine();
|
String line = br.readLine();
|
||||||
if(line.startsWith("01000000") || line.startsWith("cHNid")) {
|
if(line != null) {
|
||||||
return FileType.TEXT;
|
if(line.startsWith("01000000") || line.startsWith("cHNid")) {
|
||||||
} else if(line.startsWith("{")) {
|
return FileType.TEXT;
|
||||||
return FileType.JSON;
|
} else if(line.startsWith("{")) {
|
||||||
|
return FileType.JSON;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ import java.nio.ByteBuffer;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
import java.util.zip.DeflaterOutputStream;
|
import java.util.zip.DeflaterOutputStream;
|
||||||
import java.util.zip.InflaterInputStream;
|
import java.util.zip.InflaterInputStream;
|
||||||
|
|
||||||
|
@ -34,58 +35,77 @@ public class JsonPersistence implements Persistence {
|
||||||
this.gson = getGson();
|
this.gson = getGson();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Wallet loadWallet(File jsonFile) throws IOException {
|
@Override
|
||||||
try(Reader reader = new FileReader(jsonFile)) {
|
public WalletBackupAndKey loadWallet(Storage storage) throws IOException, StorageException {
|
||||||
return gson.fromJson(reader, Wallet.class);
|
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;
|
Wallet wallet;
|
||||||
ECKey encryptionKey;
|
ECKey encryptionKey;
|
||||||
|
|
||||||
try(InputStream fileStream = new FileInputStream(walletFile)) {
|
try(InputStream fileStream = new FileInputStream(storage.getWalletFile())) {
|
||||||
encryptionKey = getEncryptionKey(password, fileStream, null);
|
encryptionKey = getEncryptionKey(password, fileStream, alreadyDerivedKey);
|
||||||
Reader reader = new InputStreamReader(new InflaterInputStream(new ECIESInputStream(fileStream, encryptionKey, getEncryptionMagic())), StandardCharsets.UTF_8);
|
Reader reader = new InputStreamReader(new InflaterInputStream(new ECIESInputStream(fileStream, encryptionKey, getEncryptionMagic())), StandardCharsets.UTF_8);
|
||||||
wallet = gson.fromJson(reader, Wallet.class);
|
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 {
|
private Map<Storage, WalletBackupAndKey> loadChildWallets(Storage storage, Wallet masterWallet, ECKey encryptionKey) throws IOException, StorageException {
|
||||||
Map<File, Wallet> walletsMap = new LinkedHashMap<>();
|
File[] walletFiles = getChildWalletFiles(storage.getWalletFile(), masterWallet);
|
||||||
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);
|
|
||||||
Map<Storage, WalletBackupAndKey> childWallets = new LinkedHashMap<>();
|
Map<Storage, WalletBackupAndKey> childWallets = new LinkedHashMap<>();
|
||||||
Map<File, Wallet> loadedWallets = loadWallets(walletFiles, encryptionKey);
|
for(File childFile : walletFiles) {
|
||||||
for(Map.Entry<File, Wallet> entry : loadedWallets.entrySet()) {
|
Wallet childWallet = loadWallet(childFile, encryptionKey);
|
||||||
Storage storage = new Storage(entry.getKey());
|
Storage childStorage = new Storage(childFile);
|
||||||
storage.setEncryptionPubKey(encryptionKey == null ? Storage.NO_PASSWORD_KEY : ECKey.fromPublicOnly(encryptionKey));
|
childStorage.setEncryptionPubKey(encryptionKey == null ? Storage.NO_PASSWORD_KEY : ECKey.fromPublicOnly(encryptionKey));
|
||||||
storage.setKeyDeriver(getKeyDeriver());
|
childStorage.setKeyDeriver(getKeyDeriver());
|
||||||
Wallet childWallet = entry.getValue();
|
|
||||||
childWallet.setMasterWallet(masterWallet);
|
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;
|
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) {
|
private File[] getChildWalletFiles(File walletFile, Wallet masterWallet) {
|
||||||
File childDir = new File(walletFile.getParentFile(), masterWallet.getName() + "-child");
|
File childDir = new File(walletFile.getParentFile(), masterWallet.getName() + "-child");
|
||||||
if(!childDir.exists()) {
|
if(!childDir.exists()) {
|
||||||
|
@ -100,14 +120,12 @@ public class JsonPersistence implements Persistence {
|
||||||
return childFiles == null ? new File[0] : childFiles;
|
return childFiles == null ? new File[0] : childFiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
public File storeWallet(File walletFile, Wallet wallet) throws IOException {
|
@Override
|
||||||
File parent = walletFile.getParentFile();
|
public File storeWallet(Storage storage, Wallet wallet) throws IOException {
|
||||||
if(!parent.exists() && !Storage.createOwnerOnlyDirectory(parent)) {
|
File walletFile = storage.getWalletFile();
|
||||||
throw new IOException("Could not create folder " + parent);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!walletFile.getName().endsWith(".json")) {
|
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.exists()) {
|
||||||
if(!walletFile.renameTo(jsonFile)) {
|
if(!walletFile.renameTo(jsonFile)) {
|
||||||
throw new IOException("Could not rename " + walletFile.getName() + " to " + jsonFile.getName());
|
throw new IOException("Could not rename " + walletFile.getName() + " to " + jsonFile.getName());
|
||||||
|
@ -127,14 +145,12 @@ public class JsonPersistence implements Persistence {
|
||||||
return walletFile;
|
return walletFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
public File storeWallet(File walletFile, Wallet wallet, ECKey encryptionPubKey) throws IOException {
|
@Override
|
||||||
File parent = walletFile.getParentFile();
|
public File storeWallet(Storage storage, Wallet wallet, ECKey encryptionPubKey) throws IOException {
|
||||||
if(!parent.exists() && !Storage.createOwnerOnlyDirectory(parent)) {
|
File walletFile = storage.getWalletFile();
|
||||||
throw new IOException("Could not create folder " + parent);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(walletFile.getName().endsWith(".json")) {
|
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.exists()) {
|
||||||
if(!walletFile.renameTo(noJsonFile)) {
|
if(!walletFile.renameTo(noJsonFile)) {
|
||||||
throw new IOException("Could not rename " + walletFile.getName() + " to " + noJsonFile.getName());
|
throw new IOException("Could not rename " + walletFile.getName() + " to " + noJsonFile.getName());
|
||||||
|
@ -158,6 +174,16 @@ public class JsonPersistence implements Persistence {
|
||||||
return walletFile;
|
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 {
|
private void writeBinaryHeader(OutputStream outputStream) throws IOException {
|
||||||
ByteBuffer buf = ByteBuffer.allocate(21);
|
ByteBuffer buf = ByteBuffer.allocate(21);
|
||||||
buf.put(HEADER_MAGIC_1.getBytes(StandardCharsets.UTF_8));
|
buf.put(HEADER_MAGIC_1.getBytes(StandardCharsets.UTF_8));
|
||||||
|
@ -174,6 +200,7 @@ public class JsonPersistence implements Persistence {
|
||||||
return "BIE1".getBytes(StandardCharsets.UTF_8);
|
return "BIE1".getBytes(StandardCharsets.UTF_8);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public ECKey getEncryptionKey(CharSequence password) throws IOException, StorageException {
|
public ECKey getEncryptionKey(CharSequence password) throws IOException, StorageException {
|
||||||
return getEncryptionKey(password, null, null);
|
return getEncryptionKey(password, null, null);
|
||||||
}
|
}
|
||||||
|
@ -187,10 +214,12 @@ public class JsonPersistence implements Persistence {
|
||||||
return alreadyDerivedKey == null ? keyDeriver.deriveECKey(password) : alreadyDerivedKey;
|
return alreadyDerivedKey == null ? keyDeriver.deriveECKey(password) : alreadyDerivedKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public AsymmetricKeyDeriver getKeyDeriver() {
|
public AsymmetricKeyDeriver getKeyDeriver() {
|
||||||
return keyDeriver;
|
return keyDeriver;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public void setKeyDeriver(AsymmetricKeyDeriver keyDeriver) {
|
public void setKeyDeriver(AsymmetricKeyDeriver keyDeriver) {
|
||||||
this.keyDeriver = keyDeriver;
|
this.keyDeriver = keyDeriver;
|
||||||
}
|
}
|
||||||
|
@ -232,10 +261,53 @@ public class JsonPersistence implements Persistence {
|
||||||
return new Argon2KeyDeriver(salt);
|
return new Argon2KeyDeriver(salt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public PersistenceType getType() {
|
public PersistenceType getType() {
|
||||||
return PersistenceType.JSON;
|
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() {
|
public static Gson getGson() {
|
||||||
return getGson(true);
|
return getGson(true);
|
||||||
}
|
}
|
||||||
|
@ -261,7 +333,7 @@ public class JsonPersistence implements Persistence {
|
||||||
gsonBuilder.addSerializationExclusionStrategy(new ExclusionStrategy() {
|
gsonBuilder.addSerializationExclusionStrategy(new ExclusionStrategy() {
|
||||||
@Override
|
@Override
|
||||||
public boolean shouldSkipField(FieldAttributes field) {
|
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
|
@Override
|
||||||
|
|
|
@ -6,17 +6,23 @@ import com.sparrowwallet.drongo.wallet.Wallet;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Map;
|
import java.io.OutputStream;
|
||||||
|
|
||||||
public interface Persistence {
|
public interface Persistence {
|
||||||
Wallet loadWallet(File walletFile) throws IOException;
|
WalletBackupAndKey loadWallet(Storage storage) throws IOException, StorageException;
|
||||||
WalletBackupAndKey loadWallet(File walletFile, CharSequence password) throws IOException, StorageException;
|
WalletBackupAndKey loadWallet(Storage storage, CharSequence password) throws IOException, StorageException;
|
||||||
Map<File, Wallet> loadWallets(File[] walletFiles, ECKey encryptionKey) throws IOException, StorageException;
|
WalletBackupAndKey loadWallet(Storage storage, CharSequence password, ECKey alreadyDerivedKey) throws IOException, StorageException;
|
||||||
Map<Storage, WalletBackupAndKey> loadChildWallets(File walletFile, Wallet masterWallet, ECKey encryptionKey) throws IOException, StorageException;
|
File storeWallet(Storage storage, Wallet wallet) throws IOException, StorageException;
|
||||||
File storeWallet(File walletFile, Wallet wallet) throws IOException;
|
File storeWallet(Storage storage, Wallet wallet, ECKey encryptionPubKey) throws IOException, StorageException;
|
||||||
File storeWallet(File walletFile, Wallet wallet, ECKey encryptionPubKey) throws IOException;
|
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;
|
ECKey getEncryptionKey(CharSequence password) throws IOException, StorageException;
|
||||||
AsymmetricKeyDeriver getKeyDeriver();
|
AsymmetricKeyDeriver getKeyDeriver();
|
||||||
void setKeyDeriver(AsymmetricKeyDeriver keyDeriver);
|
void setKeyDeriver(AsymmetricKeyDeriver keyDeriver);
|
||||||
PersistenceType getType();
|
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;
|
package com.sparrowwallet.sparrow.io;
|
||||||
|
|
||||||
|
import com.sparrowwallet.sparrow.io.db.DbPersistence;
|
||||||
|
|
||||||
public enum PersistenceType {
|
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;
|
private final String name;
|
||||||
|
|
||||||
|
@ -13,7 +36,7 @@ public enum PersistenceType {
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getExtension() {
|
public abstract String getExtension();
|
||||||
return getName();
|
|
||||||
}
|
public abstract Persistence getInstance();
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,7 @@ public class Sparrow implements WalletExport {
|
||||||
public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException {
|
public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException {
|
||||||
try {
|
try {
|
||||||
Storage storage = AppServices.get().getOpenWallets().get(wallet);
|
Storage storage = AppServices.get().getOpenWallets().get(wallet);
|
||||||
Files.copy(storage.getWalletFile().toPath(), outputStream);
|
storage.copyWallet(outputStream);
|
||||||
outputStream.flush();
|
outputStream.flush();
|
||||||
} catch(Exception e) {
|
} catch(Exception e) {
|
||||||
log.error("Error exporting Sparrow wallet file", e);
|
log.error("Error exporting Sparrow wallet file", e);
|
||||||
|
@ -42,11 +42,7 @@ public class Sparrow implements WalletExport {
|
||||||
@Override
|
@Override
|
||||||
public String getExportFileExtension(Wallet wallet) {
|
public String getExportFileExtension(Wallet wallet) {
|
||||||
Storage storage = AppServices.get().getOpenWallets().get(wallet);
|
Storage storage = AppServices.get().getOpenWallets().get(wallet);
|
||||||
if(storage != null && (storage.getEncryptionPubKey() == null || Storage.NO_PASSWORD_KEY.equals(storage.getEncryptionPubKey()))) {
|
return storage.getWalletFileExtension();
|
||||||
return "json";
|
|
||||||
}
|
|
||||||
|
|
||||||
return "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -36,14 +36,23 @@ public class Storage {
|
||||||
public static final String WALLETS_DIR = "wallets";
|
public static final String WALLETS_DIR = "wallets";
|
||||||
public static final String WALLETS_BACKUP_DIR = "backup";
|
public static final String WALLETS_BACKUP_DIR = "backup";
|
||||||
public static final String CERTS_DIR = "certs";
|
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 File walletFile;
|
||||||
private ECKey encryptionPubKey;
|
private ECKey encryptionPubKey;
|
||||||
|
|
||||||
public Storage(File walletFile) {
|
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;
|
this.walletFile = walletFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,45 +60,63 @@ public class Storage {
|
||||||
return walletFile;
|
return walletFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
public WalletBackupAndKey loadUnencryptedWallet() throws IOException, StorageException {
|
public boolean isEncrypted() throws IOException {
|
||||||
Wallet wallet = persistence.loadWallet(walletFile);
|
return persistence.isEncrypted(walletFile);
|
||||||
Wallet backupWallet = loadBackupWallet(null);
|
}
|
||||||
Map<Storage, WalletBackupAndKey> childWallets = persistence.loadChildWallets(walletFile, wallet, null);
|
|
||||||
|
|
||||||
|
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;
|
encryptionPubKey = NO_PASSWORD_KEY;
|
||||||
return new WalletBackupAndKey(wallet, backupWallet, null, null, childWallets);
|
return migrateToDb(masterWalletAndKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
public WalletBackupAndKey loadEncryptedWallet(CharSequence password) throws IOException, StorageException {
|
public WalletBackupAndKey loadEncryptedWallet(CharSequence password) throws IOException, StorageException {
|
||||||
WalletBackupAndKey masterWalletAndKey = persistence.loadWallet(walletFile, password);
|
WalletBackupAndKey masterWalletAndKey = persistence.loadWallet(this, password);
|
||||||
Wallet backupWallet = loadBackupWallet(masterWalletAndKey.getEncryptionKey());
|
|
||||||
Map<Storage, WalletBackupAndKey> childWallets = persistence.loadChildWallets(walletFile, masterWalletAndKey.getWallet(), masterWalletAndKey.getEncryptionKey());
|
|
||||||
|
|
||||||
encryptionPubKey = ECKey.fromPublicOnly(masterWalletAndKey.getEncryptionKey());
|
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 {
|
public void saveWallet(Wallet wallet) throws IOException, StorageException {
|
||||||
Map<File, Wallet> backupWallets;
|
File parent = walletFile.getParentFile();
|
||||||
if(encryptionKey != null) {
|
if(!parent.exists() && !Storage.createOwnerOnlyDirectory(parent)) {
|
||||||
File[] backups = getBackups(TEMP_BACKUP_EXTENSION, persistence.getType().getExtension() + "." + TEMP_BACKUP_EXTENSION);
|
throw new IOException("Could not create folder " + parent);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return backupWallets.isEmpty() ? null : backupWallets.values().iterator().next();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void saveWallet(Wallet wallet) throws IOException {
|
|
||||||
if(encryptionPubKey != null && !NO_PASSWORD_KEY.equals(encryptionPubKey)) {
|
if(encryptionPubKey != null && !NO_PASSWORD_KEY.equals(encryptionPubKey)) {
|
||||||
walletFile = persistence.storeWallet(walletFile, wallet, encryptionPubKey);
|
walletFile = persistence.storeWallet(this, wallet, encryptionPubKey);
|
||||||
return;
|
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 {
|
public void backupWallet() throws IOException {
|
||||||
|
@ -100,34 +127,36 @@ public class Storage {
|
||||||
|
|
||||||
public void backupTempWallet() {
|
public void backupTempWallet() {
|
||||||
try {
|
try {
|
||||||
backupWallet(TEMP_BACKUP_EXTENSION);
|
backupWallet(TEMP_BACKUP_PREFIX);
|
||||||
} catch(IOException e) {
|
} 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();
|
File backupDir = getWalletsBackupDir();
|
||||||
|
|
||||||
Date backupDate = new Date();
|
Date backupDate = new Date();
|
||||||
String backupName = walletFile.getName();
|
String walletName = persistence.getWalletName(walletFile, null);
|
||||||
String dateSuffix = "-" + BACKUP_DATE_FORMAT.format(backupDate);
|
String dateSuffix = "-" + BACKUP_DATE_FORMAT.format(backupDate);
|
||||||
int lastDot = backupName.lastIndexOf('.');
|
String backupName = walletName + dateSuffix + walletFile.getName().substring(walletName.length());
|
||||||
if(lastDot > 0) {
|
|
||||||
backupName = backupName.substring(0, lastDot) + dateSuffix + backupName.substring(lastDot);
|
|
||||||
} else {
|
|
||||||
backupName += dateSuffix;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(extension != null) {
|
if(prefix != null) {
|
||||||
backupName += "." + extension;
|
backupName = prefix + "_" + backupName;
|
||||||
}
|
}
|
||||||
|
|
||||||
File backupFile = new File(backupDir, backupName);
|
File backupFile = new File(backupDir, backupName);
|
||||||
if(!backupFile.exists()) {
|
if(!backupFile.exists()) {
|
||||||
createOwnerOnlyFile(backupFile);
|
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() {
|
public void deleteBackups() {
|
||||||
|
@ -135,27 +164,29 @@ public class Storage {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void deleteTempBackups() {
|
public void deleteTempBackups() {
|
||||||
deleteBackups(Storage.TEMP_BACKUP_EXTENSION);
|
deleteBackups(Storage.TEMP_BACKUP_PREFIX);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void deleteBackups(String extension) {
|
private void deleteBackups(String prefix) {
|
||||||
File[] backups = getBackups(extension);
|
File[] backups = getBackups(prefix);
|
||||||
for(File backup : backups) {
|
for(File backup : backups) {
|
||||||
backup.delete();
|
backup.delete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private File[] getBackups(String extension) {
|
public File getTempBackup() {
|
||||||
return getBackups(extension, null);
|
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();
|
File backupDir = getWalletsBackupDir();
|
||||||
|
String walletName = persistence.getWalletName(walletFile, null);
|
||||||
|
String extension = walletFile.getName().substring(walletName.length());
|
||||||
File[] backups = backupDir.listFiles((dir, name) -> {
|
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 &&
|
getBackupDate(name) != null &&
|
||||||
(extension == null || name.endsWith("." + extension)) &&
|
(extension.isEmpty() || name.endsWith(extension));
|
||||||
(notExtension == null || !name.endsWith("." + notExtension));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
backups = backups == null ? new File[0] : backups;
|
backups = backups == null ? new File[0] : backups;
|
||||||
|
@ -173,6 +204,49 @@ public class Storage {
|
||||||
return null;
|
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() {
|
public ECKey getEncryptionPubKey() {
|
||||||
return encryptionPubKey;
|
return encryptionPubKey;
|
||||||
}
|
}
|
||||||
|
@ -193,6 +267,10 @@ public class Storage {
|
||||||
persistence.setKeyDeriver(keyDeriver);
|
persistence.setKeyDeriver(keyDeriver);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public PersistenceType getType() {
|
||||||
|
return persistence.getType();
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean walletExists(String walletName) {
|
public static boolean walletExists(String walletName) {
|
||||||
File encrypted = new File(getWalletsDir(), walletName.trim());
|
File encrypted = new File(getWalletsDir(), walletName.trim());
|
||||||
if(encrypted.exists()) {
|
if(encrypted.exists()) {
|
||||||
|
@ -230,6 +308,38 @@ public class Storage {
|
||||||
return new File(getWalletsDir(), walletName);
|
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() {
|
public static File getWalletsBackupDir() {
|
||||||
File walletsBackupDir = new File(getWalletsDir(), WALLETS_BACKUP_DIR);
|
File walletsBackupDir = new File(getWalletsDir(), WALLETS_BACKUP_DIR);
|
||||||
if(!walletsBackupDir.exists()) {
|
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
607
src/main/java/com/sparrowwallet/sparrow/io/db/DbPersistence.java
Normal file
607
src/main/java/com/sparrowwallet/sparrow/io/db/DbPersistence.java
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
22
src/main/java/com/sparrowwallet/sparrow/io/db/PolicyDao.java
Normal file
22
src/main/java/com/sparrowwallet/sparrow/io/db/PolicyDao.java
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
106
src/main/java/com/sparrowwallet/sparrow/io/db/WalletDao.java
Normal file
106
src/main/java/com/sparrowwallet/sparrow/io/db/WalletDao.java
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
116
src/main/java/com/sparrowwallet/sparrow/io/db/WalletNodeDao.java
Normal file
116
src/main/java/com/sparrowwallet/sparrow/io/db/WalletNodeDao.java
Normal file
|
@ -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 {
|
try {
|
||||||
JsonRpcClient client = new JsonRpcClient(transport);
|
JsonRpcClient client = new JsonRpcClient(transport);
|
||||||
return new RetryLogic<String>(MAX_RETRIES, RETRY_DELAY, IllegalStateException.class).getResult(() ->
|
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) {
|
} catch(JsonRpcException e) {
|
||||||
throw new ElectrumServerRpcException(e.getErrorMessage().getMessage(), e);
|
throw new ElectrumServerRpcException(e.getErrorMessage().getMessage(), e);
|
||||||
} catch(Exception e) {
|
} catch(Exception e) {
|
||||||
|
|
|
@ -45,7 +45,7 @@ public class ElectrumServer {
|
||||||
|
|
||||||
private static Transport transport;
|
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;
|
private static String previousServerAddress;
|
||||||
|
|
||||||
|
@ -822,21 +822,21 @@ public class ElectrumServer {
|
||||||
return Utils.bytesToHex(reversed);
|
return Utils.bytesToHex(reversed);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Map<String, Set<String>> getSubscribedScriptHashes() {
|
public static Map<String, List<String>> getSubscribedScriptHashes() {
|
||||||
return subscribedScriptHashes;
|
return subscribedScriptHashes;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getSubscribedScriptHashStatus(String scriptHash) {
|
public static String getSubscribedScriptHashStatus(String scriptHash) {
|
||||||
Set<String> existingStatuses = subscribedScriptHashes.get(scriptHash);
|
List<String> existingStatuses = subscribedScriptHashes.get(scriptHash);
|
||||||
if(existingStatuses != null) {
|
if(existingStatuses != null && !existingStatuses.isEmpty()) {
|
||||||
return Iterables.getLast(existingStatuses);
|
return existingStatuses.get(existingStatuses.size() - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void updateSubscribedScriptHashStatus(String scriptHash, String status) {
|
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);
|
existingStatuses.add(status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1147,11 +1147,22 @@ public class ElectrumServer {
|
||||||
electrumServer.calculateNodeHistory(wallet, nodeTransactionMap);
|
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
|
//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);
|
String scriptHash = getScriptHash(wallet, node);
|
||||||
retrievedScriptHashes.put(scriptHash, getSubscribedScriptHashStatus(scriptHash));
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@ import javafx.application.Platform;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.util.Set;
|
import java.util.List;
|
||||||
|
|
||||||
@JsonRpcService
|
@JsonRpcService
|
||||||
public class SubscriptionService {
|
public class SubscriptionService {
|
||||||
|
@ -25,16 +25,11 @@ public class SubscriptionService {
|
||||||
|
|
||||||
@JsonRpcMethod("blockchain.scripthash.subscribe")
|
@JsonRpcMethod("blockchain.scripthash.subscribe")
|
||||||
public void scriptHashStatusUpdated(@JsonRpcParam("scripthash") final String scriptHash, @JsonRpcOptional @JsonRpcParam("status") final String status) {
|
public void scriptHashStatusUpdated(@JsonRpcParam("scripthash") final String scriptHash, @JsonRpcOptional @JsonRpcParam("status") final String status) {
|
||||||
if(status == null) {
|
List<String> existingStatuses = ElectrumServer.getSubscribedScriptHashes().get(scriptHash);
|
||||||
//Mempool transaction was replaced returning change/consolidation script hash status to null, ignore this update
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Set<String> existingStatuses = ElectrumServer.getSubscribedScriptHashes().get(scriptHash);
|
|
||||||
if(existingStatuses == null) {
|
if(existingStatuses == null) {
|
||||||
log.debug("Received script hash status update for unsubscribed script hash: " + scriptHash);
|
log.debug("Received script hash status update for unsubscribed script hash: " + scriptHash);
|
||||||
ElectrumServer.updateSubscribedScriptHashStatus(scriptHash, status);
|
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");
|
log.debug("Received script hash status update, but status has not changed");
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -709,7 +709,7 @@ public class HeadersController extends TransactionFormController implements Init
|
||||||
}
|
}
|
||||||
|
|
||||||
Wallet copy = headersForm.getSigningWallet().copy();
|
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()) {
|
if(copy.isEncrypted()) {
|
||||||
WalletPasswordDialog dlg = new WalletPasswordDialog(copy.getName(), WalletPasswordDialog.PasswordRequirement.LOAD);
|
WalletPasswordDialog dlg = new WalletPasswordDialog(copy.getName(), WalletPasswordDialog.PasswordRequirement.LOAD);
|
||||||
|
@ -717,15 +717,15 @@ public class HeadersController extends TransactionFormController implements Init
|
||||||
if(password.isPresent()) {
|
if(password.isPresent()) {
|
||||||
Storage.DecryptWalletService decryptWalletService = new Storage.DecryptWalletService(copy, password.get());
|
Storage.DecryptWalletService decryptWalletService = new Storage.DecryptWalletService(copy, password.get());
|
||||||
decryptWalletService.setOnSucceeded(workerStateEvent -> {
|
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();
|
Wallet decryptedWallet = decryptWalletService.getValue();
|
||||||
signUnencryptedKeystores(decryptedWallet);
|
signUnencryptedKeystores(decryptedWallet);
|
||||||
});
|
});
|
||||||
decryptWalletService.setOnFailed(workerStateEvent -> {
|
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());
|
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();
|
decryptWalletService.start();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -26,8 +26,10 @@ public class HashIndexEntry extends Entry implements Comparable<HashIndexEntry>
|
||||||
this.keyPurpose = keyPurpose;
|
this.keyPurpose = keyPurpose;
|
||||||
|
|
||||||
labelProperty().addListener((observable, oldValue, newValue) -> {
|
labelProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
hashIndex.setLabel(newValue);
|
if(!Objects.equals(hashIndex.getLabel(), newValue)) {
|
||||||
EventManager.get().post(new WalletEntryLabelsChangedEvent(wallet, this));
|
hashIndex.setLabel(newValue);
|
||||||
|
EventManager.get().post(new WalletEntryLabelsChangedEvent(wallet, this));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -346,15 +346,15 @@ public class KeystoreController extends WalletFormController implements Initiali
|
||||||
if(password.isPresent()) {
|
if(password.isPresent()) {
|
||||||
Storage.DecryptWalletService decryptWalletService = new Storage.DecryptWalletService(copy, password.get());
|
Storage.DecryptWalletService decryptWalletService = new Storage.DecryptWalletService(copy, password.get());
|
||||||
decryptWalletService.setOnSucceeded(workerStateEvent -> {
|
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();
|
Wallet decryptedWallet = decryptWalletService.getValue();
|
||||||
showPrivate(decryptedWallet.getKeystores().get(keystoreIndex));
|
showPrivate(decryptedWallet.getKeystores().get(keystoreIndex));
|
||||||
});
|
});
|
||||||
decryptWalletService.setOnFailed(workerStateEvent -> {
|
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());
|
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();
|
decryptWalletService.start();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -2,14 +2,14 @@ package com.sparrowwallet.sparrow.wallet;
|
||||||
|
|
||||||
import com.sparrowwallet.drongo.address.Address;
|
import com.sparrowwallet.drongo.address.Address;
|
||||||
import com.sparrowwallet.drongo.protocol.Script;
|
import com.sparrowwallet.drongo.protocol.Script;
|
||||||
|
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
|
||||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||||
import com.sparrowwallet.drongo.wallet.WalletNode;
|
import com.sparrowwallet.drongo.wallet.WalletNode;
|
||||||
import com.sparrowwallet.sparrow.EventManager;
|
import com.sparrowwallet.sparrow.EventManager;
|
||||||
import com.sparrowwallet.sparrow.event.WalletEntryLabelsChangedEvent;
|
import com.sparrowwallet.sparrow.event.WalletEntryLabelsChangedEvent;
|
||||||
import com.sparrowwallet.sparrow.io.Config;
|
import com.sparrowwallet.sparrow.io.Config;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.*;
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public class NodeEntry extends Entry implements Comparable<NodeEntry> {
|
public class NodeEntry extends Entry implements Comparable<NodeEntry> {
|
||||||
|
@ -20,8 +20,10 @@ public class NodeEntry extends Entry implements Comparable<NodeEntry> {
|
||||||
this.node = node;
|
this.node = node;
|
||||||
|
|
||||||
labelProperty().addListener((observable, oldValue, newValue) -> {
|
labelProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
node.setLabel(newValue);
|
if(!Objects.equals(node.getLabel(), newValue)) {
|
||||||
EventManager.get().post(new WalletEntryLabelsChangedEvent(wallet, this));
|
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) {
|
public int compareTo(NodeEntry other) {
|
||||||
return node.compareTo(other.node);
|
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.control.*;
|
||||||
import com.sparrowwallet.sparrow.event.*;
|
import com.sparrowwallet.sparrow.event.*;
|
||||||
import com.sparrowwallet.sparrow.io.Storage;
|
import com.sparrowwallet.sparrow.io.Storage;
|
||||||
|
import com.sparrowwallet.sparrow.io.StorageException;
|
||||||
import javafx.beans.property.SimpleIntegerProperty;
|
import javafx.beans.property.SimpleIntegerProperty;
|
||||||
import javafx.collections.FXCollections;
|
import javafx.collections.FXCollections;
|
||||||
import javafx.event.ActionEvent;
|
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();
|
Optional<Wallet> optWallet = AppServices.get().getOpenWallets().entrySet().stream().filter(entry -> walletForm.getWalletFile().equals(entry.getValue().getWalletFile())).map(Map.Entry::getKey).findFirst();
|
||||||
if(optWallet.isPresent()) {
|
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();
|
dlg.showAndWait();
|
||||||
} else {
|
} else {
|
||||||
AppServices.showErrorDialog("Cannot export wallet", "Wallet cannot be exported, please save it first.");
|
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
|
@Subscribe
|
||||||
public void walletSettingsChanged(WalletSettingsChangedEvent event) {
|
public void walletAddressesChanged(WalletAddressesChangedEvent event) {
|
||||||
if(event.getWalletFile().equals(walletForm.getWalletFile())) {
|
if(event.getWalletId().equals(walletForm.getWalletId())) {
|
||||||
export.setDisable(!event.getWallet().isValid());
|
export.setDisable(!event.getWallet().isValid());
|
||||||
scanDescriptorQR.setVisible(!event.getWallet().isValid());
|
scanDescriptorQR.setVisible(!event.getWallet().isValid());
|
||||||
|
updateBirthDate(event.getWallet());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Subscribe
|
|
||||||
public void walletAddressesChanged(WalletAddressesChangedEvent event) {
|
|
||||||
updateBirthDate(event.getWalletFile(), event.getWallet());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Subscribe
|
@Subscribe
|
||||||
public void walletHistoryChanged(WalletHistoryChangedEvent event) {
|
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) {
|
private void updateBirthDate(Wallet wallet) {
|
||||||
if(walletFile.equals(walletForm.getWalletFile()) && !Objects.equals(wallet.getBirthDate(), walletForm.getWallet().getBirthDate())) {
|
if(!Objects.equals(wallet.getBirthDate(), walletForm.getWallet().getBirthDate())) {
|
||||||
walletForm.getWallet().setBirthDate(wallet.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.getStorage().setEncryptionPubKey(Storage.NO_PASSWORD_KEY);
|
||||||
walletForm.saveAndRefresh();
|
walletForm.saveAndRefresh();
|
||||||
EventManager.get().post(new RequestOpenWalletsEvent());
|
EventManager.get().post(new RequestOpenWalletsEvent());
|
||||||
} catch (IOException e) {
|
} catch (IOException | StorageException e) {
|
||||||
log.error("Error saving wallet", e);
|
log.error("Error saving wallet", e);
|
||||||
AppServices.showErrorDialog("Error saving wallet", e.getMessage());
|
AppServices.showErrorDialog("Error saving wallet", e.getMessage());
|
||||||
revert.setDisable(false);
|
revert.setDisable(false);
|
||||||
|
@ -518,7 +526,7 @@ public class SettingsController extends WalletFormController implements Initiali
|
||||||
} else {
|
} else {
|
||||||
Storage.KeyDerivationService keyDerivationService = new Storage.KeyDerivationService(walletForm.getStorage(), password.get());
|
Storage.KeyDerivationService keyDerivationService = new Storage.KeyDerivationService(walletForm.getStorage(), password.get());
|
||||||
keyDerivationService.setOnSucceeded(workerStateEvent -> {
|
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();
|
ECKey encryptionFullKey = keyDerivationService.getValue();
|
||||||
Key key = null;
|
Key key = null;
|
||||||
|
|
||||||
|
@ -566,12 +574,12 @@ public class SettingsController extends WalletFormController implements Initiali
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
keyDerivationService.setOnFailed(workerStateEvent -> {
|
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());
|
AppServices.showErrorDialog("Error saving wallet", keyDerivationService.getException().getMessage());
|
||||||
revert.setDisable(false);
|
revert.setDisable(false);
|
||||||
apply.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();
|
keyDerivationService.start();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -5,11 +5,15 @@ import com.sparrowwallet.drongo.wallet.Keystore;
|
||||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||||
import com.sparrowwallet.sparrow.AppServices;
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
import com.sparrowwallet.sparrow.EventManager;
|
import com.sparrowwallet.sparrow.EventManager;
|
||||||
|
import com.sparrowwallet.sparrow.event.KeystoreLabelsChangedEvent;
|
||||||
import com.sparrowwallet.sparrow.event.WalletAddressesChangedEvent;
|
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.Storage;
|
||||||
|
import com.sparrowwallet.sparrow.io.StorageException;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -40,30 +44,65 @@ public class SettingsWalletForm extends WalletForm {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void saveAndRefresh() throws IOException {
|
public void saveAndRefresh() throws IOException, StorageException {
|
||||||
Wallet pastWallet = null;
|
Wallet pastWallet = wallet.copy();
|
||||||
|
|
||||||
boolean refreshAll = isRefreshNecessary(wallet, walletCopy);
|
if(isRefreshNecessary(wallet, walletCopy)) {
|
||||||
if(refreshAll) {
|
boolean addressChange = isAddressChange();
|
||||||
pastWallet = wallet.copy();
|
|
||||||
save(); //Save here for the temp backup in case password has been changed
|
if(wallet.isValid()) {
|
||||||
if(AppServices.isConnected()) {
|
//Don't create temp backup on changing addresses - there are no labels to lose
|
||||||
getStorage().backupTempWallet();
|
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();
|
walletCopy.clearNodes();
|
||||||
}
|
|
||||||
|
|
||||||
wallet = walletCopy.copy();
|
Integer childIndex = wallet.isMasterWallet() ? null : wallet.getMasterWallet().getChildWallets().indexOf(wallet);
|
||||||
save();
|
|
||||||
|
|
||||||
if(refreshAll) {
|
//Replace the SettingsWalletForm wallet reference - note that this reference is only shared with the WalletForm wallet with WalletAddressesChangedEvent below
|
||||||
EventManager.get().post(new WalletAddressesChangedEvent(wallet, pastWallet, getWalletFile()));
|
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 {
|
} 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) {
|
private boolean isRefreshNecessary(Wallet original, Wallet changed) {
|
||||||
if(!original.isValid() || !changed.isValid()) {
|
if(!original.isValid() || !changed.isValid()) {
|
||||||
return true;
|
return true;
|
||||||
|
@ -73,6 +112,27 @@ public class SettingsWalletForm extends WalletForm {
|
||||||
return true;
|
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()) {
|
if(original.getGapLimit() != changed.getGapLimit()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import com.sparrowwallet.drongo.KeyPurpose;
|
||||||
import com.sparrowwallet.drongo.protocol.TransactionInput;
|
import com.sparrowwallet.drongo.protocol.TransactionInput;
|
||||||
import com.sparrowwallet.drongo.protocol.TransactionOutput;
|
import com.sparrowwallet.drongo.protocol.TransactionOutput;
|
||||||
import com.sparrowwallet.drongo.wallet.*;
|
import com.sparrowwallet.drongo.wallet.*;
|
||||||
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
import com.sparrowwallet.sparrow.EventManager;
|
import com.sparrowwallet.sparrow.EventManager;
|
||||||
import com.sparrowwallet.sparrow.WalletTabData;
|
import com.sparrowwallet.sparrow.WalletTabData;
|
||||||
import com.sparrowwallet.sparrow.event.WalletBlockHeightChangedEvent;
|
import com.sparrowwallet.sparrow.event.WalletBlockHeightChangedEvent;
|
||||||
|
@ -30,8 +31,10 @@ public class TransactionEntry extends Entry implements Comparable<TransactionEnt
|
||||||
this.blockTransaction = blockTransaction;
|
this.blockTransaction = blockTransaction;
|
||||||
|
|
||||||
labelProperty().addListener((observable, oldValue, newValue) -> {
|
labelProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
blockTransaction.setLabel(newValue);
|
if(!Objects.equals(blockTransaction.getLabel(), newValue)) {
|
||||||
EventManager.get().post(new WalletEntryLabelsChangedEvent(wallet, this));
|
blockTransaction.setLabel(newValue);
|
||||||
|
EventManager.get().post(new WalletEntryLabelsChangedEvent(wallet, this));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
setConfirmations(calculateConfirmations());
|
setConfirmations(calculateConfirmations());
|
||||||
|
@ -68,7 +71,7 @@ public class TransactionEntry extends Entry implements Comparable<TransactionEnt
|
||||||
}
|
}
|
||||||
|
|
||||||
public int calculateConfirmations() {
|
public int calculateConfirmations() {
|
||||||
return blockTransaction.getConfirmations(getWallet().getStoredBlockHeight());
|
return blockTransaction.getConfirmations(AppServices.getCurrentBlockHeight() == null ? getWallet().getStoredBlockHeight() : AppServices.getCurrentBlockHeight());
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getConfirmationsDescription() {
|
public String getConfirmationsDescription() {
|
||||||
|
|
|
@ -107,7 +107,7 @@ public class WalletController extends WalletFormController implements Initializa
|
||||||
|
|
||||||
@Subscribe
|
@Subscribe
|
||||||
public void walletAddressesChanged(WalletAddressesChangedEvent event) {
|
public void walletAddressesChanged(WalletAddressesChangedEvent event) {
|
||||||
if(event.getWalletFile().equals(walletForm.getWalletFile())) {
|
if(event.getWalletId().equals(walletForm.getWalletId())) {
|
||||||
configure(event.getWallet().isValid());
|
configure(event.getWallet().isValid());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ import com.sparrowwallet.sparrow.EventManager;
|
||||||
import com.sparrowwallet.sparrow.WalletTabData;
|
import com.sparrowwallet.sparrow.WalletTabData;
|
||||||
import com.sparrowwallet.sparrow.event.*;
|
import com.sparrowwallet.sparrow.event.*;
|
||||||
import com.sparrowwallet.sparrow.io.Config;
|
import com.sparrowwallet.sparrow.io.Config;
|
||||||
|
import com.sparrowwallet.sparrow.io.StorageException;
|
||||||
import com.sparrowwallet.sparrow.net.ElectrumServer;
|
import com.sparrowwallet.sparrow.net.ElectrumServer;
|
||||||
import com.sparrowwallet.sparrow.io.Storage;
|
import com.sparrowwallet.sparrow.io.Storage;
|
||||||
import com.sparrowwallet.sparrow.net.ServerType;
|
import com.sparrowwallet.sparrow.net.ServerType;
|
||||||
|
@ -22,6 +23,7 @@ import org.slf4j.LoggerFactory;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public class WalletForm {
|
public class WalletForm {
|
||||||
private static final Logger log = LoggerFactory.getLogger(WalletForm.class);
|
private static final Logger log = LoggerFactory.getLogger(WalletForm.class);
|
||||||
|
@ -60,6 +62,10 @@ public class WalletForm {
|
||||||
return storage;
|
return storage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getWalletId() {
|
||||||
|
return storage.getWalletId(wallet);
|
||||||
|
}
|
||||||
|
|
||||||
public File getWalletFile() {
|
public File getWalletFile() {
|
||||||
return storage.getWalletFile();
|
return storage.getWalletFile();
|
||||||
}
|
}
|
||||||
|
@ -72,11 +78,11 @@ public class WalletForm {
|
||||||
throw new UnsupportedOperationException("Only SettingsWalletForm supports revert");
|
throw new UnsupportedOperationException("Only SettingsWalletForm supports revert");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void save() throws IOException {
|
public void save() throws IOException, StorageException {
|
||||||
storage.saveWallet(wallet);
|
storage.saveWallet(wallet);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void saveAndRefresh() throws IOException {
|
public void saveAndRefresh() throws IOException, StorageException {
|
||||||
Wallet pastWallet = wallet.copy();
|
Wallet pastWallet = wallet.copy();
|
||||||
storage.backupTempWallet();
|
storage.backupTempWallet();
|
||||||
wallet.clearHistory();
|
wallet.clearHistory();
|
||||||
|
@ -88,6 +94,15 @@ public class WalletForm {
|
||||||
storage.backupWallet();
|
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() {
|
public void deleteBackups() {
|
||||||
storage.deleteBackups();
|
storage.deleteBackups();
|
||||||
}
|
}
|
||||||
|
@ -127,36 +142,61 @@ public class WalletForm {
|
||||||
wallet.setStoredBlockHeight(blockHeight);
|
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) {
|
if(pastWallet != null) {
|
||||||
labelsChanged = copyLabels(pastWallet);
|
labelChangedEntries = copyLabels(pastWallet);
|
||||||
}
|
}
|
||||||
|
|
||||||
notifyIfChanged(blockHeight, previousWallet, labelsChanged);
|
notifyIfChanged(blockHeight, previousWallet, labelChangedEntries);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean copyLabels(Wallet pastWallet) {
|
private Set<Entry> copyLabels(Wallet pastWallet) {
|
||||||
boolean changed = wallet.getNode(KeyPurpose.RECEIVE).copyLabels(pastWallet.getNode(KeyPurpose.RECEIVE));
|
Set<Entry> changedEntries = new LinkedHashSet<>();
|
||||||
changed |= wallet.getNode(KeyPurpose.CHANGE).copyLabels(pastWallet.getNode(KeyPurpose.CHANGE));
|
|
||||||
|
|
||||||
|
//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()) {
|
for(Map.Entry<Sha256Hash, BlockTransaction> txEntry : wallet.getTransactions().entrySet()) {
|
||||||
BlockTransaction pastBlockTransaction = pastWallet.getTransactions().get(txEntry.getKey());
|
BlockTransaction pastBlockTransaction = pastWallet.getTransactions().get(txEntry.getKey());
|
||||||
if(pastBlockTransaction != null && txEntry.getValue() != null && txEntry.getValue().getLabel() == null && pastBlockTransaction.getLabel() != null) {
|
if(pastBlockTransaction != null && txEntry.getValue() != null && txEntry.getValue().getLabel() == null && pastBlockTransaction.getLabel() != null) {
|
||||||
txEntry.getValue().setLabel(pastBlockTransaction.getLabel());
|
txEntry.getValue().setLabel(pastBlockTransaction.getLabel());
|
||||||
changed = true;
|
changedEntries.add(getWalletTransactionsEntry());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
storage.deleteTempBackups();
|
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<>();
|
List<WalletNode> historyChangedNodes = new ArrayList<>();
|
||||||
historyChangedNodes.addAll(getHistoryChangedNodes(previousWallet.getNode(KeyPurpose.RECEIVE).getChildren(), wallet.getNode(KeyPurpose.RECEIVE).getChildren()));
|
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()));
|
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()) {
|
if(!historyChangedNodes.isEmpty()) {
|
||||||
Platform.runLater(() -> EventManager.get().post(new WalletHistoryChangedEvent(wallet, storage, historyChangedNodes)));
|
Platform.runLater(() -> EventManager.get().post(new WalletHistoryChangedEvent(wallet, storage, historyChangedNodes)));
|
||||||
changed = true;
|
changed = true;
|
||||||
|
@ -257,39 +297,43 @@ public class WalletForm {
|
||||||
@Subscribe
|
@Subscribe
|
||||||
public void walletDataChanged(WalletDataChangedEvent event) {
|
public void walletDataChanged(WalletDataChangedEvent event) {
|
||||||
if(event.getWallet().equals(wallet)) {
|
if(event.getWallet().equals(wallet)) {
|
||||||
backgroundSaveWallet();
|
backgroundUpdate();
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void backgroundSaveWallet() {
|
|
||||||
try {
|
|
||||||
save();
|
|
||||||
} catch (IOException e) {
|
|
||||||
//Background save failed
|
|
||||||
log.error("Background wallet save failed", e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Subscribe
|
@Subscribe
|
||||||
public void walletSettingsChanged(WalletSettingsChangedEvent event) {
|
public void walletHistoryCleared(WalletHistoryClearedEvent event) {
|
||||||
if(event.getWalletFile().equals(storage.getWalletFile())) {
|
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();
|
wallet = event.getWallet();
|
||||||
|
|
||||||
if(event instanceof WalletAddressesChangedEvent) {
|
walletTransactionsEntry = null;
|
||||||
walletTransactionsEntry = null;
|
walletUtxosEntry = null;
|
||||||
walletUtxosEntry = null;
|
accountEntries.clear();
|
||||||
accountEntries.clear();
|
EventManager.get().post(new WalletNodesChangedEvent(wallet));
|
||||||
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
|
//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) {
|
if(Config.get().getServerType() == ServerType.BITCOIN_CORE) {
|
||||||
savedPastWallet = event.getPastWallet();
|
savedPastWallet = event.getPastWallet();
|
||||||
}
|
|
||||||
|
|
||||||
//Clear the cache - we will need to fetch everything again
|
|
||||||
AppServices.clearTransactionHistoryCache(wallet);
|
|
||||||
refreshHistory(AppServices.getCurrentBlockHeight(), 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
|
@Subscribe
|
||||||
public void walletHistoryChanged(WalletHistoryChangedEvent event) {
|
public void walletHistoryChanged(WalletHistoryChangedEvent event) {
|
||||||
if(event.getWalletFile().equals(storage.getWalletFile())) {
|
if(event.getWalletId().equals(getWalletId())) {
|
||||||
for(WalletNode changedNode : event.getHistoryChangedNodes()) {
|
for(WalletNode changedNode : event.getHistoryChangedNodes()) {
|
||||||
if(changedNode.getLabel() != null && !changedNode.getLabel().isEmpty()) {
|
if(changedNode.getLabel() != null && !changedNode.getLabel().isEmpty()) {
|
||||||
List<Entry> changedLabelEntries = new ArrayList<>();
|
List<Entry> changedLabelEntries = new ArrayList<>();
|
||||||
|
@ -393,14 +437,24 @@ public class WalletForm {
|
||||||
|
|
||||||
if(!labelChangedEntries.isEmpty()) {
|
if(!labelChangedEntries.isEmpty()) {
|
||||||
Platform.runLater(() -> EventManager.get().post(new WalletEntryLabelsChangedEvent(wallet, labelChangedEntries)));
|
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
|
@Subscribe
|
||||||
public void walletTabsClosed(WalletTabsClosedEvent event) {
|
public void walletTabsClosed(WalletTabsClosedEvent event) {
|
||||||
for(WalletTabData tabData : event.getClosedWalletTabData()) {
|
for(WalletTabData tabData : event.getClosedWalletTabData()) {
|
||||||
if(tabData.getWalletForm() == this) {
|
if(tabData.getWalletForm() == this) {
|
||||||
|
storage.close();
|
||||||
AppServices.clearTransactionHistoryCache(wallet);
|
AppServices.clearTransactionHistoryCache(wallet);
|
||||||
EventManager.get().unregister(this);
|
EventManager.get().unregister(this);
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,11 +23,16 @@ open module com.sparrowwallet.sparrow {
|
||||||
requires hummingbird;
|
requires hummingbird;
|
||||||
requires centerdevice.nsmenufx;
|
requires centerdevice.nsmenufx;
|
||||||
requires jcommander;
|
requires jcommander;
|
||||||
requires slf4j.api;
|
requires org.slf4j;
|
||||||
requires bwt.jni;
|
requires bwt.jni;
|
||||||
requires jtorctl;
|
requires jtorctl;
|
||||||
requires javacsv;
|
requires javacsv;
|
||||||
requires jul.to.slf4j;
|
requires jul.to.slf4j;
|
||||||
requires bridj;
|
requires bridj;
|
||||||
requires com.google.gson;
|
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;
|
||||||
}
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
create table blockTransaction (id identity not null, txid binary(32) not null, hash binary(32) not null, height integer not null, date timestamp, fee bigint, label varchar(255), transaction binary, blockHash binary(32), wallet bigint not null);
|
||||||
|
create table blockTransactionHashIndex (id identity not null, hash binary(32) not null, height integer not null, date timestamp, fee bigint, label varchar(255), index bigint not null, value bigint not null, status integer, spentBy bigint, node bigint not null);
|
||||||
|
create table keystore (id identity not null, label varchar(255), source integer not null, walletModel integer not null, masterFingerprint varchar(8), derivationPath varchar(255) not null, extendedPublicKey varchar(255), masterPrivateExtendedKey bigint, seed bigint, wallet bigint not null, index integer not null);
|
||||||
|
create table masterPrivateExtendedKey (id identity not null, privateKey binary(255), chainCode binary(255), initialisationVector binary(255), encryptedBytes binary(255), keySalt binary(255), deriver integer, crypter integer, creationTimeSeconds bigint);
|
||||||
|
create table policy (id identity not null, name varchar(255) not null, script varchar(2048) not null);
|
||||||
|
create table seed (id identity not null, type integer not null, mnemonicString varchar(255), initialisationVector binary(255), encryptedBytes binary(255), keySalt binary(255), deriver integer, crypter integer, needsPassphrase boolean, creationTimeSeconds bigint);
|
||||||
|
create table wallet (id identity not null, name varchar(255) not null, network integer not null, policyType integer not null, scriptType integer not null, storedBlockHeight integer, gapLimit integer, birthDate timestamp, defaultPolicy bigint not null);
|
||||||
|
create table walletNode (id identity not null, derivationPath varchar(255) not null, label varchar(255), wallet bigint not null, parent bigint);
|
||||||
|
alter table blockTransactionHashIndex add constraint blockTransactionHashIndex_spentBy_unique unique (spentBy);
|
||||||
|
alter table keystore add constraint keystore_masterPrivateExtendedKey_unique unique (masterPrivateExtendedKey);
|
||||||
|
alter table keystore add constraint keystore_seed_unique unique (seed);
|
||||||
|
alter table wallet add constraint wallet_defaultPolicy_unique unique (defaultPolicy);
|
||||||
|
alter table blockTransaction add constraint blockTransaction_wallet foreign key (wallet) references wallet;
|
||||||
|
alter table blockTransactionHashIndex add constraint blockTransactionHashIndex_spentBy foreign key (spentBy) references blockTransactionHashIndex;
|
||||||
|
alter table blockTransactionHashIndex add constraint blockTransactionHashIndex_node foreign key (node) references walletNode;
|
||||||
|
alter table keystore add constraint keystore_masterPrivateExtendedKey foreign key (masterPrivateExtendedKey) references masterPrivateExtendedKey;
|
||||||
|
alter table keystore add constraint keystore_seed foreign key (seed) references seed;
|
||||||
|
alter table keystore add constraint keystore_wallet foreign key (wallet) references wallet;
|
||||||
|
alter table wallet add constraint wallet_defaultPolicy foreign key (defaultPolicy) references policy;
|
||||||
|
alter table walletNode add constraint walletNode_wallet foreign key (wallet) references wallet;
|
||||||
|
alter table walletNode add constraint walletNode_walletNode foreign key (parent) references walletNode;
|
||||||
|
create index blockTransaction_txid on blockTransaction(txid);
|
|
@ -7,6 +7,15 @@
|
||||||
<logger name="javafx.css" level="ERROR"/>
|
<logger name="javafx.css" level="ERROR"/>
|
||||||
<logger name="javafx.scene.focus" level="INFO"/>
|
<logger name="javafx.scene.focus" level="INFO"/>
|
||||||
<logger name="sun.net.www.protocol.http.HttpURLConnection" level="INFO" />
|
<logger name="sun.net.www.protocol.http.HttpURLConnection" level="INFO" />
|
||||||
|
<logger name="h2database" level="ERROR" />
|
||||||
|
<logger name="com.zaxxer.hikari.HikariDataSource" level="WARN" />
|
||||||
|
<logger name="org.flywaydb.core.internal.command.DbValidate" level="WARN" />
|
||||||
|
<logger name="org.flywaydb.core.internal.command.DbMigrate" level="WARN" />
|
||||||
|
<logger name="org.flywaydb.core.internal.command.DbClean" level="ERROR" />
|
||||||
|
<logger name="org.flywaydb.core.internal.database.base.BaseDatabaseType" level="WARN" />
|
||||||
|
<logger name="org.flywaydb.core.internal.database.base.Schema" level="WARN" />
|
||||||
|
<logger name="org.flywaydb.core.internal.schemahistory.JdbcTableSchemaHistory" level="WARN" />
|
||||||
|
<logger name="org.flywaydb.core.internal.license.VersionPrinter" level="WARN" />
|
||||||
|
|
||||||
<contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator"/>
|
<contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator"/>
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue